本文会详细讲解一下如何基于 Spring Boot WebFlux 实现 SSE(Server-Sent Events)功能。
一、SSE 原理
SSE 是一种允许服务器在任何时间向客户端(通常是浏览器)推送数据的 Web 技术。它建立在单一的、长期的 HTTP 连接之上。
工作原理:
- 建立连接:客户端(如浏览器)使用
EventSourceAPI 向服务器发起一个常规的 HTTP 请求,但请求头中包含Accept: text/event-stream。 - 保持连接:服务器收到请求后,将响应头
Content-Type设置为text/event-stream,并保持这个 HTTP 连接处于打开状态,而不是在发送一次响应后立即关闭它。 - 流式传输:服务器通过这个持久连接周期性地发送数据块。每个数据块遵循特定的文本格式(以
data:、event:、id:、retry:等关键字开头,并以两个换行符\n\n结尾)。 - 处理数据:客户端的
EventSource对象会持续监听这些 incoming 的数据流,解析格式,并触发相应的事件(如onmessage,onopen)来处理数据。
与 WebSocket 的区别:
- 单向 vs 双向:SSE 是服务器到客户端的单向通信。WebSocket 是双向的。
- 协议:SSE 基于 HTTP(因此更容易融入现有 HTTP 生态,如认证、代理),WebSocket 是基于 TCP 的独立协议(
ws://或wss://)。 - 重连机制:SSE 内置了断线重连和事件 ID 跟踪的功能(通过
retry和id字段),WebSocket 需要手动实现。 - 数据格式:SSE 仅支持文本(通常使用 UTF-8 编码的 JSON),WebSocket 支持二进制和文本。
二、使用场景
SSE 非常适合服务器需要主动、持续地向客户端推送数据的场景,且不需要客户端频繁向服务器发送消息。
- 实时通知:新闻推送、站内信、价格提醒、社交网络点赞/评论通知。
- 实时监控仪表盘:服务器性能监控(CPU、内存)、实时业务数据(在线用户数、订单成交额)、物联网设备传感器数据流。
- 实时日志:将服务器后台任务的执行日志实时输出到浏览器控制台或管理页面。
- 进度更新:报告长时间运行的操作(如文件处理、数据导出)的完成进度。
三、后端实现 (Spring Boot WebFlux)
Spring WebFlux 的响应式特性使其成为实现 SSE 的绝佳选择,因为它天然支持处理异步数据流。
1. 添加依赖 (Maven pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
2. 创建控制器-关键示例 (Controller)
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.time.LocalDateTime;
@RestController
@RequestMapping("/sse")
public class SseController {
/**
* 模拟一个简单的每秒推送时间信息的 SSE 流
* produces = MediaType.TEXT_EVENT_STREAM_VALUE 是关键,它声明了响应是 SSE 流。
*/
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamEvents() {
// 创建一个每 1 秒触发一次的无限流
// 消息内容格式遵循 SSE 规范: "data: {json}\n\n"
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> "data: {"time": "" + LocalDateTime.now() + "", "sequence": " + sequence + "}\n\n");
}
/**
* 更复杂的例子:推送自定义事件和 ID
*/
@GetMapping(path = "/advanced-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamAdvancedEvents() {
Flux<Long> interval = Flux.interval(Duration.ofSeconds(2));
Flux<String> events = Flux.generate(sink -> {
// 生成一个自定义事件消息
String eventData = "This is a message at " + LocalDateTime.now();
sink.next(eventData);
});
// 合并间隔和事件流,并格式化为 SSE 格式
return Flux.zip(interval, events)
.map(tuple -> {
Long seq = tuple.getT1();
String message = tuple.getT2();
// 构建一个完整的 SSE 消息块
// 可以指定事件类型(event:)和ID(id:)
return "id: " + seq + "\n" +
"event: custom-message\n" + // 前端监听的事件名
"data: " + message + "\n" +
"retry: 10000\n\n"; // 建议客户端10秒后重连
});
}
/**
* 使用 ServerSentEvent 工具类(推荐,更清晰)
*/
@GetMapping(path = "/sse-object", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<org.springframework.http.codec.ServerSentEvent<?>> streamSseObject() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> org.springframework.http.codec.ServerSentEvent.builder()
.id(String.valueOf(sequence)) // ID
.event("periodic-event") // 事件类型
.data("SSE Message - " + sequence + " at " + LocalDateTime.now()) // 数据
.build());
}
}
关键点说明:
produces = MediaType.TEXT_EVENT_STREAM_VALUE:这是最重要的注解,它告诉 Spring 该方法产生的是 SSE 流。Flux:WebFlux 的核心响应式类型,代表一个包含 0 到 N 个元素的异步序列,完美用于表示持续的事件流。Flux.interval():创建一个按固定时间间隔发出递增数字的Flux,是生成周期性事件的常用方法。- 消息格式:返回值必须是遵循 SSE 格式的
String流,或者ServerSentEvent<?>对象流(Spring 会帮你自动转换为正确的文本格式)。格式必须以两个换行符\n\n结束一个消息块。 ServerSentEventbuilder:这是 Spring 提供的工具类,构建 SSE 消息更规范、更安全,避免了手动拼接字符串可能出现的格式错误。
四、前端实现 (Vue.js)
前端使用 Vue 3 的 Composition API 和 EventSource API。
<template>
<div>
<h1>SSE 实时数据流</h1>
<button @click="connectSSE" :disabled="isConnected">连接</button>
<button @click="disconnectSSE" :disabled="!isConnected">断开</button>
<div v-if="isConnected">状态: <span style="color: green;">已连接</span></div>
<div v-else>状态: <span style="color: red;">未连接</span></div>
<h2>收到的消息:</h2>
<ul>
<li v-for="(message, index) in messages" :key="index">
{{ message }}
</li>
</ul>
<h2>收到的 ServerSentEvent 对象:</h2>
<ul>
<li v-for="(sse, index) in sseEvents" :key="index">
<p>ID: {{ sse.id }}</p>
<p>Event: {{ sse.event || 'message' }}</p>
<p>Data: {{ sse.data }}</p>
<hr>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue';
const isConnected = ref(false);
const messages = ref([]);
const sseEvents = ref([]);
let eventSource = null;
const connectSSE = () => {
// 连接到后端 SSE 端点
// 注意 URL 与你的后端 Controller 路径匹配
eventSource = new EventSource('http://localhost:8080/sse/sse-object');
// 监听通用的 'message' 事件(服务器未指定 event 字段时默认触发)
eventSource.onmessage = (event) => {
console.log('Generic message event:', event);
messages.value.push(event.data);
// 如果数据是 JSON,可以解析
// try { const data = JSON.parse(event.data); } catch (e) { ... }
};
// 监听特定的自定义事件(对应服务器端的 `event: custom-message`)
eventSource.addEventListener('custom-message', (event) => {
console.log('Custom event received:', event);
messages.value.push(`[Custom] ${event.data}`);
});
// 监听特定的自定义事件(对应服务器端的 `event: periodic-event`)
eventSource.addEventListener('periodic-event', (event) => {
console.log('Periodic event received:', event);
// 将整个 ServerSentEvent 对象保存下来用于展示
sseEvents.value.push({
id: event.lastEventId, // 获取服务器发送的 id
event: event.type, // 事件类型
data: event.data // 数据
});
});
// 连接打开时
eventSource.onopen = (event) => {
console.log('SSE Connection opened.', event);
isConnected.value = true;
};
// 发生错误时
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
// EventSource 在出错时会自动尝试重连,onerror 触发后 onopen 可能会再次触发
// 如果连接彻底失败(如 404),需要手动关闭
// eventSource.close();
// isConnected.value = false;
};
};
const disconnectSSE = () => {
if (eventSource) {
eventSource.close(); // 关闭连接
eventSource = null;
isConnected.value = false;
console.log('SSE Connection closed.');
}
};
// 组件卸载时自动断开连接,防止内存泄漏
onUnmounted(() => {
disconnectSSE();
});
</script>
关键点说明:
EventSource:浏览器原生对象,用于创建到 SSE 端点的连接。onmessage:监听所有未指定事件类型的消息。addEventListener('event-name'):监听服务器端发出的特定类型的事件(如event: custom-message)。onopen&onerror:处理连接状态。event.data:获取服务器发送的实际数据内容。close():重要! 当不再需要连接时(例如组件销毁、用户手动断开),必须调用此方法以释放资源。- 自动重连:
EventSource在连接意外断开时会自动尝试重新连接,这是其内置的强大功能之一。 - 你应该会看到消息列表开始每秒更新一条数据。后端控制器的不同端点(
/stream,/advanced-stream,/sse-object)会产生不同格式的消息,前端需要对应地监听不同的事件。
总结
通过结合 Spring Boot WebFlux 的 Flux 响应式流和 SSE 协议,你可以非常轻松地构建出高效的服务器推送功能。Vue 前端的 EventSource API 使用起来也十分简单直观。
这种组合非常适合构建需要低延迟、高吞吐量、单向数据流的实时应用,同时享受 Spring 生态和 Vue 生态带来的开发便利。