本文最后更新于:2025年5月25日 晚上
Server-Sent Events (SSE)
Server-Sent Events (SSE) 是一种允许服务器向客户端实时推送数据的Web技术,它基于HTTP协议,提供了一种简单有效的服务器到客户端的单向通信机制。
注意:SSE更适配响应式编程
响应式编程(WebFlux)的优势
响应式栈(如WebFlux + Reactor)的SSE实现更高效:
1 2 3 4 5 6 7 8 9 10
| @RestController public class ReactiveSseController { private final Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> streamEvents() { return sink.asFlux() .map(data -> ServerSentEvent.builder(data).build()); } }
|
优势:
特性 |
阻塞式(MVC) |
响应式(WebFlux) |
线程占用 |
1连接=1线程 |
固定少量IO线程 |
内存消耗 |
每个连接独立缓冲 |
共享背压缓冲 |
广播复杂度 |
O(n)手动发送 |
O(1)自动多播 |
扩展性 |
千级连接 |
万级连接 |
适用场景选择
✅ 适合响应式SSE的场景:
- 高并发(如实时仪表盘、股票行情)
- 需要组合多个异步数据源(如数据库变更+消息队列)
- 长周期事件流(如文件导入进度)
❌ 适合传统阻塞式的场景:
- 简单低频通知(如单个用户订单状态更新)
- 遗留系统无法升级响应式栈
- 需要与阻塞式库(如JPA/Hibernate)深度集成
性能对比数据
在相同4核8G服务器上:
连接数 |
Spring MVC SSE |
WebFlux SSE |
1,000 |
线程池耗尽 |
内存占用200MB |
10,000 |
崩溃 |
内存占用1.2GB |
延迟 |
15-30ms |
2-8ms |
基本概念
SSE 的主要特点包括:
- 单向通信:仅服务器可以向客户端推送数据
- 基于HTTP:使用标准HTTP协议,不需要特殊协议
- 文本格式:数据以纯文本格式传输
- 自动重连:内置连接断开后的自动重连机制
- 简单API:浏览器端使用简单的EventSource API
工作原理
-
客户端通过EventSource
对象发起连接
-
服务器保持连接打开,以text/event-stream
格式发送数据
-
数据以流的形式持续发送,格式为:
1 2
| event: message data: 这是消息内容\n\n
|
与WebSocket的比较
特性 |
SSE |
WebSocket |
通信方向 |
单向(服务器→客户端) |
双向 |
协议 |
HTTP |
独立的ws/wss协议 |
数据格式 |
文本 |
文本或二进制 |
自动重连 |
支持 |
需要手动实现 |
复杂度 |
简单 |
较复杂 |
浏览器支持 |
广泛(除IE) |
广泛 |
适用场景
SSE 非常适合以下场景:
- 实时通知系统
- 新闻/股票行情推送
- 社交媒体动态更新
- 实时日志监控
- 需要简单实时功能的场景
注意事项
- 浏览器兼容性:大多数现代浏览器支持,但IE不支持
- 连接限制:浏览器对每个源的SSE连接数有限制(通常6个)
- 代理问题:某些代理服务器可能缓冲SSE流
- 数据格式:仅支持文本,不支持二进制数据
SSE 提供了一种轻量级的实时通信解决方案,在不需要双向通信的场景下,比WebSocket更简单易用。
代码示例(阻塞式):
服务端(后端)
Maven依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|
application.yaml
1 2 3 4 5 6 7
| server: port: 8080 servlet: encoding: charset: UTF-8 force: true enabled: true
|
Webconfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package cn.zuster.sse.config;
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class WebConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(false) .maxAge(3600) .allowedHeaders("*"); } }
|
客户端(前端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <!DOCTYPE html> <html> <head> <title>测试SSE消息推送</title> </head> <body> <button id="reconnect-btn">手动重连按钮</button> <script> let retryCount = 0; const maxRetries = 5; const MAX_ATTEMPTS = 15; let eventSource;
function connectSSE() { const clientId = 'user_' + Math.random().toString(36).substr(2, 9); eventSource = new EventSource(`http://localhost:8080/sse/start?clientId=888`);
eventSource.onopen = function(e) { retryCount = 0; console.log("连接已建立"); };
eventSource.onmessage = function(e) { console.log("收到消息:", e.data); }; eventSource.addEventListener('broadcast', function(e) { console.log("广播消息:", e.data); });
eventSource.onerror = function(e) { if (e.eventPhase === EventSource.CLOSED) { console.log("连接已超时,正在重试!"); } else { console.log("连接出错"); } if (retryCount++ < maxRetries) { console.log(`尝试第${retryCount}次重连...`); } else { console.log("达到最大重试次数,停止重连"); retryCount=0; eventSource.close(); } }; }
connectSSE();
document.getElementById('reconnect-btn').addEventListener('click', function() { if (eventSource) eventSource.close(); retryCount = 0; connectSSE(); }); </script> </body> </html>
|
相关示例参考:https://github.com/zuster/my-demo-springboot-sse?tab=readme-ov-file