实时消息推送是很多业务系统的刚需,比如站内信通知、订单状态变更提醒、后台任务进度反馈等。说到推送,大部分人第一反应是WebSocket,但如果你的场景只是服务器向浏览器单向推送数据,SSE(Server-Sent Events)其实是更轻量的选择。它基于普通HTTP协议,不需要升级协议、不需要额外的依赖,Spring Boot原生就支持,几行代码就能跑起来。

SSE的基本原理是什么
SSE的本质是一个保持打开状态的HTTP长连接。浏览器端通过EventSource对象发起请求,服务器端不关闭响应流,而是按照固定的文本格式分批写入数据。这个格式由W3C规范定义,每条消息由若干行组成,以空行结尾,例如:
event: message
data: {"orderId":1001,"status":"已发货"}
id: 20250101001
其中event表示事件类型,data是消息体,id用于断线重连时告知服务器上次收到的位置。浏览器收到消息后,会根据event名称触发对应的事件回调。整个过程只有一个单向通道:服务器写,客户端读,客户端不能通过这条连接向服务器发数据。
与轮询相比,SSE避免了大量无效请求;与WebSocket相比,SSE走的是标准HTTP/1.1或HTTP/2,经过代理、网关、负载均衡时兼容性更好,而且自带断线自动重连机制(EventSource默认会在连接断开后自动重连),这一点连原生WebSocket都不具备。
Spring Boot中如何用SseEmitter实现推送
Spring MVC从4.2版本开始提供了SseEmitter类,专门用于SSE场景。控制器方法返回一个SseEmitter对象,Spring会保持响应流打开,你在其他线程中调用send()方法即可向客户端推送消息。下面是一个完整的基础示例:
@RestController
public class NoticeController {
// 保存所有在线的客户端连接
private static final List<SseEmitter> EMITTERS = new CopyOnWriteArrayList<>();
@GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
// 超时时间设为0表示永不超时,生产环境建议设置合理值
SseEmitter emitter = new SseEmitter(0L);
EMITTERS.add(emitter);
emitter.onCompletion(() -> EMITTERS.remove(emitter));
emitter.onTimeout(() -> EMITTERS.remove(emitter));
emitter.onError(e -> EMITTERS.remove(emitter));
// 发送一条欢迎消息,确认连接建立
try {
emitter.send(SseEmitter.event()
.name("connected")
.data("连接成功"));
} catch (IOException e) {
EMITTERS.remove(emitter);
}
return emitter;
}
// 模拟业务触发推送
@PostMapping("/notify")
public String notify(@RequestParam String message) {
for (SseEmitter emitter : EMITTERS) {
try {
emitter.send(SseEmitter.event()
.name("notice")
.data(message));
} catch (IOException e) {
EMITTERS.remove(emitter);
}
}
return "推送完成";
}
}有几个细节值得注意。第一,produces必须设置为text/event-stream,这是SSE规范要求的MIME类型,否则浏览器不会按事件流解析。第二,建议分别注册onCompletion、onTimeout、onError三个回调,及时把失效的连接从容器中移除,否则连接对象会不断堆积,最终导致内存泄漏。第三,保存连接的集合要使用CopyOnWriteArrayList这类并发安全容器,因为推送往往发生在其他线程。
前端接入也非常简单,原生JavaScript就够了,不需要任何第三方库:
const source = new EventSource('/subscribe');
// 监听指定名称的事件,对应后端的 event().name("connected")
source.addEventListener('connected', function(e) {
console.log('连接建立:', e.data);
});
source.addEventListener('notice', function(e) {
console.log('收到通知:', e.data);
// 这里可以做弹窗、角标提醒等UI处理
});
source.onerror = function(e) {
console.log('连接异常,浏览器将自动重连');
};生产环境必须处理的几个问题
心跳保活
长连接最大的敌人是中间网络设备的超时切断。Nginx默认对60秒内没有数据的连接会断开,一些云网关的超时时间甚至更短。解决办法是服务器定时发送一条注释行作为心跳。SSE规范规定以冒号开头的行是注释,浏览器会忽略它,但数据流本身刷新了连接的活跃时间:
@Scheduled(fixedRate = 25000)
public void heartbeat() {
for (SseEmitter emitter : EMITTERS) {
try {
// 发送注释行作为心跳,浏览器端不会触发事件
emitter.send(SseEmitter.event().comment("ping"));
} catch (IOException e) {
EMITTERS.remove(emitter);
}
}
}心跳间隔建议设置为中间件超时时间的一半,比如网关30秒超时,心跳就设为15秒左右。别忘了在启动类上加@EnableScheduling注解开启定时任务。
异步线程与超时控制
SseEmitter默认依赖Servlet容器的异步支持,Spring Boot内嵌Tomcat已经开启。但要小心一点:如果Controller方法在Servlet请求线程中创建emitter后立即返回,而后续send操作发生在业务线程,这本身就是SSE的正常用法,不需要你自己开线程去阻塞。另外,设置超时时间时不要一味使用0(永不超时),长时间无交互的死连接会占用服务器资源,生产环境一般设置为30分钟到1小时,配合客户端自动重连即可。
如果项目中同时使用了拦截器或过滤器做鉴权,注意SSE连接建立后鉴权信息不会自动刷新。token过期后连接可能还挂着,业务上需要在推送时校验用户身份是否仍然有效,失效就主动调用emitter.complete()关闭连接。
网关与反向代理配置
SSE经过Nginx时需要关闭缓冲,否则Nginx会把事件流缓冲到一定量才转发,导致消息延迟严重。配置方式如下:
location /subscribe {
proxy_pass http://backend;
# 关闭代理缓冲,保证事件实时透传
proxy_buffering off;
# 长连接读写超时加大
proxy_read_timeout 300s;
# 对HTTP/1.1保持长连接
proxy_http_version 1.1;
proxy_set_header Connection '';
}SSE与WebSocket该怎么选
两者的核心区别在通信方向。SSE是服务器到客户端的单向流,WebSocket是全双工。如果你的业务只需要服务器推(通知、行情、日志 tail、进度条),SSE在开发成本、运维复杂度、兼容性上全面占优:不需要引入额外的依赖和协议栈,普通HTTP基础设施直接可用,浏览器端还免费获得自动重连能力。
反过来,如果业务存在高频双向交互,比如聊天室、协同编辑、多人游戏,WebSocket才是正确的选择。另外需要注意浏览器对HTTP/1.1同域名的连接数限制(通常是6个),如果同一页面要开多条SSE连接,建议启用HTTP/2,它对同域多路复用没有这个瓶颈。
总的来说,SSE是“够用就好”的典型代表。在Spring Boot中配合SseEmitter,加上心跳保活和连接管理这两板斧,就能稳定支撑绝大多数单向推送场景,值得在实际项目中优先考虑。
SSEServer-Sent EventsSpring Boot实时推送修改时间:2026-09-04 05:02:37