一、 SSE
SSE(Server-Sent Events,服务器推送事件) 是一种基于 HTTP 的轻量级实时通信技术。它允许服务端主动向客户端(通常是浏览器)推送数据,而客户端只需通过标准的 EventSource API 接收即可。
与传统 轮询(Polling) 相比,SSE 将"客户端反复拉取"变为"服务端按需推送",极大降低了无效请求和网络延迟;与 WebSocket 相比,SSE 更轻量、更易用,尤其适合"服务器单向推送"的场景。
| 特性 | SSE | WebSocket | 轮询 |
|---|---|---|---|
| 通信方向 | 服务器 → 客户端(单向) | 双向全双工 | 客户端 → 服务器(反复请求) |
| 协议基础 | HTTP(纯文本) | 独立协议(ws/wss) | HTTP |
| 自动重连 | ✅ 内置 | ❌ 需手动实现 | ❌ 需手动实现 |
| 断点续传 | ✅ 通过 Last-Event-ID |
❌ 需手动实现 | ❌ 无 |
| 实现复杂度 | 极低 | 较高 | 低(但资源浪费大) |
| 穿透性 | 完美通过代理/防火墙 | 可能被拦截 | 完美 |
| 适用场景 | 实时监控、通知、股票行情、日志流 | 聊天、游戏、协同编辑 | 低频更新场景 |
二、SSE 协议基础 ------ data: 和 \n\n
SSE 的事件流格式非常简洁,每个事件由若干字段 组成,每个字段以 字段名: 值 形式呈现,并以 两个换行符 \n\n 作为事件结束标记。
常见的字段有:
| 字段 | 含义 | 是否必需 |
|---|---|---|
data: |
事件的数据内容(可多行,多行会合并为一个字符串) | ✅ 必需 |
event: |
事件类型,前端可针对不同类型绑定不同的监听器 | ❌ 可选 |
id: |
事件 ID,用于断线重连时恢复(浏览器会缓存最后一个 ID) | ❌ 可选 |
retry: |
重连间隔(毫秒),告诉浏览器断开后多久重新连接 | ❌ 可选 |
: |
注释行(以冒号开头),可用于维持连接的心跳 | ❌ 可选 |
一个典型的事件流文本如下:
event: ping
data: heartbeat
event: message
id: 101
data: {"count": 42}
💡 浏览器通过
EventSource接收时,会自动解析这些字段,并触发对应的事件回调。
三、Wastnet 框架
Wastnet 是一个高性能的 Java HTTP 框架,为 SSE 提供了三种不同粒度的实现方式,从"手搓协议"到"开箱即用",满足不同层级的需求。
🛠️ 方式一:原始实现
⚠️ 仅供学习协议原理,生产环境不推荐直接使用。
在原始方式中,开发者需要手动完成所有事情:
- 设置
Content-Type: text/event-stream - 添加
Cache-Control: no-cache - 启用 chunked 传输
- 拼接符合 SSE 格式的字符串(
data: ...\n\n) - 手动
write和flush
java
router.get("/events/raw", new HttpRoute() {
@Override
public void handle(String path, HttpRequest request, HttpResponse response) throws Throwable {
response.contentType("text/event-stream; charset=utf-8")
.header("Cache-Control", "no-cache")
.chunked();
for (int i = 0; i < 5; ++i) {
String data = "data: {\"count\":" + i + "}\n\n";
response.write(data.getBytes(StandardCharsets.UTF_8));
response.flush();
Thread.sleep(1000);
}
}
});
** SSE 的本质**:它就是一段符合特定格式的 HTTP 响应体,通过 chunked 编码持续输出。
🌀 方式二:loop 模式 ------ response.sse() 一行搞定
这是最直观的用法,在 Handler 线程内循环推送数据。框架替你封装了 Header 设置、格式拼接、编码和刷新,你只需传入数据内容。
java
router.get("/events", new HttpRoute() {
@Override
public void handle(String path, HttpRequest request, HttpResponse response) throws Throwable {
for (int i = 0; i < 10; ++i) {
response.sse("{\"count\":" + i + "}");
Thread.sleep(1000);
}
}
});
对比原始实现,response.sse() 一行代码替代了 4~5 行 底层操作,且保持清晰的推送逻辑。
适用场景:Handler 线程内可独立完成的定时推送,例如模拟数据流、简单状态更新。
⚡ 方式三:emitter 模式 ------ 多线程安全,即时返回
当推送逻辑需要异步执行 或跨线程协作 时,router.sse() + SseEmitter 是你的不二之选。
基本用法
java
HttpRouterHandler router = new HttpRouterHandler();
router.sse("/news", emitter -> {
for (int i = 1; i <= 10; ++i) {
emitter.emit("{\"id\":" + i + "}");
Thread.sleep(1000);
}
emitter.close();
});
注意:这里的 emitter.emit() 是线程安全的,你可以从任意线程调用它。Handler 本身可以立即返回,连接由框架保持。
完整参数传递
java
emitter.emit("chat", "hello", "msg-001", 3000);
| 参数 | 类型 | 说明 |
|---|---|---|
event |
String | 事件类型,对应前端 onmessage 或自定义 onevent(若为 null 则省略) |
data |
String | 事件数据(必需) |
id |
String | 事件 ID,断线重连时浏览器通过 Last-Event-ID 头告知服务端(若为 null 则省略) |
retry |
long | 重连间隔(毫秒),告诉浏览器多久后重试(若 ≤0 则省略) |
自定义超时
默认超时为 30 分钟(适合长连接监控)。可通过第二个参数指定(单位毫秒):
java
router.sse("/events", 60000L, emitter -> { ... }); // 60 秒超时
多线程安全演示
java
router.sse("/events", 60000L, emitter -> {
for (int i = 1; i <= 10; ++i) {
final int seq = i;
new Thread(() -> {
emitter.emit("data", "msg-" + seq, "id-" + seq, 3000);
}).start();
}
});
每个线程独立推送,emit() 内部已做好同步,你无需担心并发问题。
连接关闭与回调
java
emitter.onClose(() -> {
System.out.println("连接已关闭,清理资源...");
});
当客户端断开或服务端主动 close() 时,注册的回调会触发。你还可以通过 emitter.isClosed() 检查连接状态。
四、API 速查表
HttpResponse 的 SSE 方法
| 方法签名 | 说明 |
|---|---|
sse(String data) |
仅发送数据,自动生成 data: <data>\n\n |
sse(String event, String data) |
发送带事件类型的 SSE 事件 |
sse(String event, String data, String id, long retry) |
发送完整参数(event, data, id, retry) |
HttpRouterHandler 的 SSE 注册方法
| 方法签名 | 说明 |
|---|---|
sse(String path, SseHandler handler) |
注册 SSE 端点,默认超时 30 分钟 |
sse(String path, long timeoutMs, SseHandler handler) |
注册 SSE 端点,指定超时(毫秒) |
SseEmitter 实例方法
| 方法 | 说明 |
|---|---|
emit(String data) |
推送 data-only 事件 |
emit(String event, String data, String id, long retry) |
推送完整事件(所有可选字段) |
close() |
主动关闭 SSE 连接(框架自动提交结束标记) |
onClose(Runnable callback) |
注册连接关闭回调 |
isClosed() |
检查连接是否已关闭 |
五、三种方式对比
| 对比维度 | 原始实现 | loop 模式(response.sse()) |
emitter 模式(router.sse()) |
|---|---|---|---|
| 封装程度 | 最低(手写协议) | 中等(自动处理 Header/格式/Flush) | 最高(分离推送逻辑与请求线程) |
| 线程模型 | 阻塞在 Handler 线程 | 阻塞在 Handler 线程 | Handler 立即返回,推送在其他线程 |
| 线程安全 | 不涉及 | 不涉及(单线程) | ✅ 完全线程安全 |
| 适用场景 | 仅用于学习协议 | 简单、短期的循环推送 | 异步、多线程、长连接、复杂业务 |
| 超时控制 | 手动管理 | 依赖框架默认 | 可灵活指定 |
| 推荐度 | ❌ 不推荐生产 | ⭐⭐⭐ 适合快速原型 | ⭐⭐⭐⭐⭐ 生产首选 |
六、实战场景 ------ 运维监控系统中的 SSE 应用
SSE 在运维监控领域有着天然的优势,因为它完美契合"服务器持续推送状态变化"的需求。
🎯 SSE 的核心价值:化"轮询"为"推送"
在传统监控中,前端每几秒发送一次 AJAX 请求去拉取最新指标,这不仅浪费带宽,还增加服务器压力,且无法做到真正的实时。SSE 将模式转变为:数据一旦产生,服务端立即推送到看板,运维人员再也不用"手动 F5"。
📊 监控看板
| 数据类型 | 具体指标示例 |
|---|---|
| 系统级指标 | CPU 使用率、内存占用、磁盘 I/O、网络吞吐量 |
| 应用性能指标 | 请求响应时间(P99)、每秒请求数(RPS)、错误率、GC 暂停时间 |
| 错误与日志追踪 | 错误日志流(含状态码、耗时、堆栈摘要),快速定位异常 |
| 服务健康状态 | 服务 Up/Down 状态、运行时长(Uptime)、端口连通性 |
这些数据通常以 JSON 格式封装,通过 SSE 流式推送,前端结合 ECharts / Chart.js 实时渲染图表。
🏗️ 典型架构
监控看板(图表渲染) 浏览器(EventSource) 后端服务(SSE推送层) 监控Agent(采集器) 监控看板(图表渲染) 浏览器(EventSource) 后端服务(SSE推送层) 监控Agent(采集器) #mermaid-svg-3XmFGYfpeM4rNasO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3XmFGYfpeM4rNasO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3XmFGYfpeM4rNasO .error-icon{fill:#552222;}#mermaid-svg-3XmFGYfpeM4rNasO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3XmFGYfpeM4rNasO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3XmFGYfpeM4rNasO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3XmFGYfpeM4rNasO .marker.cross{stroke:#333333;}#mermaid-svg-3XmFGYfpeM4rNasO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3XmFGYfpeM4rNasO p{margin:0;}#mermaid-svg-3XmFGYfpeM4rNasO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3XmFGYfpeM4rNasO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-3XmFGYfpeM4rNasO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3XmFGYfpeM4rNasO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-3XmFGYfpeM4rNasO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-3XmFGYfpeM4rNasO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-3XmFGYfpeM4rNasO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-3XmFGYfpeM4rNasO .sequenceNumber{fill:white;}#mermaid-svg-3XmFGYfpeM4rNasO #sequencenumber{fill:#333;}#mermaid-svg-3XmFGYfpeM4rNasO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-3XmFGYfpeM4rNasO .messageText{fill:#333;stroke:none;}#mermaid-svg-3XmFGYfpeM4rNasO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3XmFGYfpeM4rNasO .labelText,#mermaid-svg-3XmFGYfpeM4rNasO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-3XmFGYfpeM4rNasO .loopText,#mermaid-svg-3XmFGYfpeM4rNasO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-3XmFGYfpeM4rNasO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3XmFGYfpeM4rNasO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3XmFGYfpeM4rNasO .noteText,#mermaid-svg-3XmFGYfpeM4rNasO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-3XmFGYfpeM4rNasO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3XmFGYfpeM4rNasO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3XmFGYfpeM4rNasO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3XmFGYfpeM4rNasO .actorPopupMenu{position:absolute;}#mermaid-svg-3XmFGYfpeM4rNasO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-3XmFGYfpeM4rNasO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3XmFGYfpeM4rNasO .actor-man circle,#mermaid-svg-3XmFGYfpeM4rNasO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-3XmFGYfpeM4rNasO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop每2秒采集一次 loop每当有新数据 如果连接断开,浏览器自动重连并携带 Last-Event-ID 续传 上报 CPU/内存/网络等指标GET /api/stream (建立SSE连接)连接成功 (200 OK, text/event-stream)data: {"cpu": 45.2, "mem": 68.7, ...}\n\n解析JSON,更新图表
🆚 SSE 对比 WebSocket
| 对比项 | SSE | WebSocket |
|---|---|---|
| 适用方向 | 服务端 → 客户端(单向) | 双向实时通信 |
| 开发成本 | 前端原生 EventSource,后端简单组装 |
需要实现握手、帧协议、心跳等 |
| 运维友好度 | 天然兼容 HTTP 代理、负载均衡 | 需要额外配置支持(如 Nginx 升级协议) |
| 可靠机制 | 自带自动重连 + Last-Event-ID 续传 |
需自行实现断线重连和消息缓存 |
| 消息头开销 | 纯文本,HTTP 头部较小 | 二进制帧,额外控制帧开销 |
| 适用协议 | 仅支持 UTF-8 文本(JSON/XML) | 支持文本和二进制(Protobuf/MessagePack) |
监控场景几乎全是"服务器推送指标,客户端只负责展示",没有双向交互需求,因此 SSE 是更轻量、更可靠、更省心的选择。
💡 运维监控中的 SSE 最佳实践
-
合理设置超时时间
监控看板需长时间保持连接,服务器(如 Nginx)的
proxy_read_timeout应大于客户端预期最大空闲时间,或配合心跳保持活性。 -
实现心跳机制(Keep-alive)
如果长时间没有实际数据,可定期发送注释行(
: heartbeat)或空data:,防止连接因超时被中间代理关闭。 -
利用
id和retry实现断线续传每个事件携带递增 ID,客户端重连时会在请求头中带上
Last-Event-ID,服务端可根据该 ID 从断点继续推送,避免数据丢失。 -
注意浏览器连接数限制
每个域名下浏览器允许的 SSE 连接数通常为 6 个。如果监控看板需要同时监控多个服务,可考虑使用子域名或通过 Web Worker 聚合连接。
-
数据编码与格式
SSE 仅支持 UTF-8 文本,请确保所有 JSON 序列化使用 UTF-8。如需传输二进制(如 Protobuf),可考虑 Base64 编码或改用 WebSocket。
-
异常处理与重试策略
服务端应捕获推送过程中的异常,记录日志并优雅关闭 emitter,避免资源泄漏。
七、实际案例参考
| 应用场景 | 技术实现 | 优势 |
|---|---|---|
轻量级嵌入式监控 (如 loadflux) |
单行代码集成 SSE 仪表盘 | 极简接入,开箱即用 |
| 工业管道监测 | SSE + 边缘网关,自动重连 + 事件重放 | 恶劣网络下保障告警不丢失 |
| AI 辅助运维 | AI Agent 流式输出诊断推理过程 | 运维人员实时了解 AI 的"思考链" |
八、总结
SSE 是一项被低估却极其实用的技术。它既不像轮询那样低效,又不像 WebSocket 那样笨重,在 "服务端推送数据" 这个垂直领域里,SSE 做到了简单、可靠、低成本 。
结合运维监控这一典型场景,SSE 的价值被进一步放大 ------ 它让实时看板变得轻盈,让运维人员从"刷新焦虑"中解放出来。