关注我的公众号:【编程朝花夕拾】,可获取首发内容。
01 引言
最近做的业务项目总是遇到到长连接、服务端向前端推送消息的功能点。由于项目中已经集成了现成的WebSocket,也就是没有再重复造轮子。
但是如果单独搭建WebSocket可能会繁琐,能不能更加简单的SSE代替呢?我们一起来试试SSE这门技术。

02 简介
2.1 核心概念
SSE(Server-Sent Events)是一种基于 HTTP 协议的服务端推送技术。服务端通过持久的 HTTP 连接,单向向客户端推送数据,客户端通过 EventSource API 监听并处理这些事件。
主要特点:
- 单向通信:仅服务端能发送消息
- 消息仅支持文本数据
- 协议是常规的HTTP 协议
为什么选择SSE?
主要原因就是简单,通常我们遇到的场景都是单项推送:从服务器接收数据流。。这正是 EventSource 所擅长的。它还支持自动重新连接,此外,它是一个普通的旧的 HTTP,不是一个新协议。
官网地址:html.spec.whatwg.org/multipage/s...
2.2 SSE vs WebSocket 对比
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务端 → 客户端) | 双向 |
| 底层协议 | HTTP | 独立的 WS 协议 |
| 断线重连 | 浏览器自动重连 | 需手动实现 |
| 数据格式 | 纯文本(text/event-stream) | 任意二进制 |
| 复杂度 | 低,几行代码搞定 | 高,需要握手协议 |
| 代理/防火墙友好 | 好(本质就是 HTTP) | 一般 |
| 连接数限制 | 浏览器对同域名 SSE 连接数有限制(通常 6 个) | 无此限制 |
| 适用场景 | 通知推送、日志流、进度条、AI 流式输出 | 聊天室、实时游戏、协同编辑 |
选型建议 :如果你的场景只需要服务端向客户端单向推送,优先用 SSE,简单、稳定、省心。不要为了"听起来高级"就上 WebSocke。
03 SpringBoot 集成 SSE
org.springframework.web.servlet.mvc.method.annotation.SseEmitter是Spring MVC原生支持的方式,最常用。

是在Spring 4.2及以后就开始支持的。
3.1 核心控制类
java
@Slf4j
@RestController
@RequestMapping("/sse")
public class SseController {
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
/**
* @Description: 建立连接
*
* @Author: ws
* @Date: 2026/9/17 11:38
**/
@RequestMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", "*");
log.info("建立连接。。。");
SseEmitter sseEmitter = new SseEmitter(0L);
try {
sseEmitter.send(SseEmitter.event().data("连接成功"));
emitters.add(sseEmitter);
} catch (Exception e) {
e.printStackTrace();
}
return sseEmitter;
}
/**
* @Description: 推送测试
*
* @Author: ws
* @Date: 2026/9/17 17:27
**/
@RequestMapping(value = "/push")
public void push() throws Exception {
for (int i = 0; i < 10; i++) {
final int index = i;
Thread.sleep(1000);
emitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event().data("推送数据" + index));
} catch (Exception e) {
log.error("推送数据失败", e);
}
});
log.info("推送数据" + i);
}
}
}
其中connect()用来建立连接,并将客户端保存到CopyOnWriteArrayList中,而push()用来模拟数据推送。
3.2 客户单订阅
javascript
const eventSource = new EventSource('http://localhost:8080/sse/connect');
eventSource.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
eventSource.onerror = (event) => {
console.log('连接出错,浏览器会自动重连...');
};
3.3 测试
分别通过谷歌浏览器和火狐浏览器接受服务端的消息。
推送消息:http://localhost:8080/sse/push

到这里简单的搭建就已经完成了,是不是很简单。
04 使用注意事项
上面完成简单的搭建功能,如果用在生产商可能还会有很多问题。
4.1 超时问题
SseEmitter 构造函数接收一个 Long 类型的超时时间(毫秒):
java
new SseEmitter(0L) // 永不超时(慎用)
new SseEmitter(60000L) // 60秒超时
new SseEmitter() // 使用默认超时(Spring 默认 30秒)
注意事项:
- 超时时间到了之后,Spring 会自动调用
complete(),连接会被关闭。 - 如果你的推送场景是长时间保持的(如实时通知),要么设
0L永不超时,要么自己实现心跳机制定期续命。 - 推荐做法:设置一个合理的超时时间 + 客户端自动重连 + 服务端心跳保活。
永不超时固然好用,但是客户端堆积的问题如何解决?这是首要考虑的问题。
4.2 心跳保活机制
既然选择了永不超时,为什么还要使用保活机制呢?
这是因为长时间不发送数据,连接可能被中间代理、Nginx 或浏览器断开。这就需要我们定时不断给客户端发送心跳,用来保持连接。
下面是伪代码:
java
@Scheduled(fixedRate = 30000) // 每30秒
public void sendHeartbeat() {
emitters.forEach(emitter -> {
try {
emitter.send(SseEmitter.event().comment("heartbeat"));
} catch (IOException e) {
emitter.complete();
emitters.remove(emitter);
}
});
}
4.3 连接数管理
浏览器对同一域名的 SSE 连接数有限制(HTTP/1.1 下通常为 6 个)。如果你的应用打开了多个标签页,连接数会很快耗尽。超过6个以上的标签页均无法建立连接。
解决方案:
- 使用 HTTP/2(连接复用,不受 6 个限制)
- 使用共享 Worker(
SharedWorker)让多个标签页共享一个 SSE 连接 - 后端用频道/分组管理,一个连接推送多个主题的数据
05 易错点
5.1 忘记注册事件
案例就是没有使用注册事件:onCompletion 、onTimeout 、onError。
当然案例使用的永不超时onTimeout就没有了意义。
会造成什么后果:客户端断开后,emitter 还留在列表里,每次推送都报 IOException,日志刷屏,内存泄漏。
推荐做法:
java
SseEmitter emitter = new SseEmitter(timeout);
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError(e -> emitters.remove(emitter));
emitters.add(emitter);
5.2 emitter 管理
如果在异步线程中发送数据时 emitter 已完成,就会出现这样的异常:
IllegalStateException: ResponseBodyEmitter is already set complete
主要因为,在另一个线程中调用 emitter.send() 时,连接已经被超时或客户端断开关闭了。
推荐做法:
java
try {
emitter.send(SseEmitter.event().data("hello"));
} catch (IOException | IllegalStateException e) {
emitter.complete();
emitters.remove(emitter);
log.warn("推送失败,已移除 emitter: {}", e.getMessage());
}
5.3 produces 类型
produces 类型没设置或设错,会导致客户端收到数据但 EventSource 不触发事件,或者浏览器直接拒绝连接。并不是所有的浏览器都会出现这样的问题。这个可以说是针对不同版本的浏览器的兼容性处理。
推荐做法:
java
// ✅ 必须设置 produces
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() { ... }
如果不设置 produces = "text/event-stream",Spring 可能返回 application/json,浏览器 EventSource 会直接报错。
5.4 跨域问题
前后端分离项目,前端控制台报 CORS 错误。自己在做测试的时候也遇到了,所以案例使用了 response.setHeader("Access-Control-Allow-Origin", "*");来解决跨域。
解决跨域的方法有很对可以@CrossOrigin注解、全局配置 CORS或者其他方式,只要解决跨域问题即可。
06 小结
除了上面列举的问题,还有其他很多细节,如鉴权、集群等场景。既然小细节那么多,就没有直接造好的轮子,直接使用么?
找了一圈下来,没有找到。既然找不到,那就自己造一个。后面专门建一个项目将SSE作为一个独立的推送服务,再也不用去考虑那些零碎的小细节。