你做过这种页面吗:订单状态、构建日志、监控告警、AI 生成进度条。
用户一边盯着页面,一边希望“它自己更新”,而不是疯狂点刷新。
这时很多人第一反应是 WebSocket。它当然强大,但有些场景属于“高配打蚊子”。
如果你的需求是服务端持续推消息给浏览器,浏览器几乎不需要回传,SSE 往往更省事。
先把名字说清楚:本文的 SSE 是 Server-Sent Events。
它不是 CPU 指令集里的 SSE,也不是一个脱离 HTTP 的新传输协议;它是基于 HTTP 的单向实时推送机制。
先把核心概念掰开:SSE 到底是什么
术语解释(SSE)
SSE 是浏览器通过 EventSource 发起一个 HTTP 请求,服务端把连接保持住,按 text/event-stream 格式持续写入事件的通信方式。
生活类比
你可以把它想成“商场广播”。顾客(浏览器)只负责听广播,广播站(服务端)持续播报“3 楼打折”“停车位已满”。
迷你案例
一个物流页面里,后端每 3 秒推送一次 运输中 -> 派送中 -> 已签收,前端不用轮询,状态自动跳。
SSE 的关键特征可以一句话记住:
单向(服务端 -> 客户端)、长连接、基于 HTTP、浏览器原生支持自动重连。
连接是怎么跑起来的:从请求到重连
前端发起 EventSource('/events') 后,服务端返回:
Content-Type: text/event-streamCache-Control: no-cache- 持续写
data:行,并用空行分隔事件
下面这张流程图可以直接当你排查问题时的“脑内地图”。
浏览器打开页面
-> 创建 EventSource('/events')
-> 发送 HTTP GET (Accept: text/event-stream)
-> 服务端返回 200 + text/event-stream
-> 服务端持续 write: id/event/data + 空行
-> 浏览器触发 onmessage / addEventListener
-> 网络抖动或服务重启
-> 浏览器按 retry 自动重连并携带 Last-Event-ID
-> 服务端按 ID 续传或从最新状态继续推送
看完这张图,你下一步应该先检查服务端响应头和事件分隔空行,这两处最容易导致“连上了但前端没反应”。
再看一条标准事件长什么样:
id: 42
event: progress
retry: 3000
data: {"taskId":"A1001","percent":78}
id:事件编号,便于断线续传event:自定义事件名,前端可按类型监听retry:建议重连间隔(毫秒)data:真正业务数据,可多行
跟轮询和 WebSocket 比,SSE 在哪一档
很多人卡在“到底选哪个”。先看差异,再做选择。
| 方案 | 通信方向 | 连接形态 | 实现复杂度 | 最适合 | 主要风险 |
|---|---|---|---|---|---|
| 短轮询 | 客户端主动拉取 | 每次请求新建 | 低 | 低频更新页面 | 延迟高、请求浪费 |
| 长轮询 | 客户端拉取,服务端延迟返回 | 频繁重建 | 中 | 过渡方案 | 服务端连接管理复杂 |
| SSE | 服务端单向推送 | 单连接持续输出 | 低到中 | 通知流、进度流、日志流 | 仅单向,代理缓冲需处理 |
| WebSocket | 双向实时 | 全双工长连接 | 中到高 | 聊天、协同编辑、游戏 | 协议与运维复杂度更高 |
看完这张表,你下一步应该先问自己一句:前端是否真的需要“高频上行写回”?如果不需要,优先从 SSE 开始。
一眼决策:你该不该先用 SSE
你不需要“技术信仰”,需要可执行决策。用这个矩阵就够了。
| 当前条件 | 结论 |
|---|---|
| 主要是服务端推送,客户端只展示 | 优先选 SSE |
| 需要浏览器原生、快速上线、少改造 | 优先选 SSE |
| 需要客户端频繁上报(如实时协作光标) | 选 WebSocket |
| 需要二进制帧、超低延迟双向互动 | 选 WebSocket |
| 只要偶尔刷新、实时性不强 | 选短轮询即可 |
看完这个矩阵,你下一步应该把业务接口分成“只读推送”和“双向互动”两类,再决定 SSE 或 WebSocket。
可复现实战:20 分钟搭一个 SSE 进度推送
下面用 Node + Express 做一个可跑通的最小示例。
场景:后端每 2 秒推一次任务进度,前端实时显示百分比。
第 1 步:服务端(server.js)
import express from "express";
const app = express();
app.use(express.static("public"));
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
if (res.flushHeaders) res.flushHeaders();
let id = 0;
let percent = 0;
const ticker = setInterval(() => {
id += 1;
percent = Math.min(percent + 10, 100);
const payload = JSON.stringify({
taskId: "A1001",
percent,
status: percent === 100 ? "done" : "running"
});
res.write(`id: ${id}\n`);
res.write("event: progress\n");
res.write(`data: ${payload}\n\n`);
if (percent === 100) {
clearInterval(ticker);
}
}, 2000);
// 心跳包,避免中间代理长时间无数据时断开
const heartbeat = setInterval(() => {
res.write(": ping\n\n");
}, 15000);
req.on("close", () => {
clearInterval(ticker);
clearInterval(heartbeat);
res.end();
});
});
app.listen(3000, () => {
console.log("SSE server running on port 3000, endpoint: /events");
});
第 2 步:前端(public/index.html)
<!doctype html>
<html lang="zh-CN">
<body>
<h1>任务进度</h1>
<p id="status">等待连接...</p>
<script>
const statusEl = document.getElementById("status");
const es = new EventSource("/events");
es.addEventListener("progress", (e) => {
const data = JSON.parse(e.data);
statusEl.textContent = `任务 ${data.taskId}: ${data.percent}% (${data.status})`;
if (data.status === "done") es.close();
});
es.onerror = () => {
statusEl.textContent = "连接异常,浏览器会自动重连...";
};
</script>
</body>
</html>
第 3 步:运行与预期结果
- 启动服务端:
node server.js - 把前端文件放进
public目录后,打开服务端首页 - 你会看到 0% -> 10% -> ... -> 100% 的实时变化
- 中途断网再恢复,浏览器会尝试重连
这套流程跑通后,你下一步应该把示例里的 taskId/percent 换成真实业务字段,并在服务端补上鉴权与限流。
最常见的 5 个坑(以及怎么避开)
-
忘记事件结束空行
每条事件必须以空行结束,不然前端可能一直不触发回调。 -
被反向代理缓冲
某些网关会缓存响应,导致“服务端在推,浏览器收不到”。要关闭缓冲或放行流式响应。 -
没有心跳导致中间链路断开
长时间无消息时,代理可能判定连接空闲并断开。定时发送注释行: ping。 -
断线后无法续传
不维护事件id,重连后只能从当前状态开始。关键业务建议保留最近事件窗口,按Last-Event-ID续传。 -
把 SSE 当成双向通道
SSE 天生单向。客户端上行仍走普通 HTTP 接口,别把它改造成“半残 WebSocket”。
这几个坑你只要提前做一轮自测,线上告警数量通常会少一截,值班同学会真心感谢你。
收尾:把 SSE 用对,比“全栈上重武器”更重要
你现在可以把 SSE 当成一个明确工具,而不是“实时技术里的备胎”:
- 检查业务方向:先确认是不是“服务端推、客户端收”为主。
- 选择实现方案:单向实时优先 SSE,双向互动再上 WebSocket。
- 测试链路细节:重点测事件空行、代理缓冲、心跳和重连。
- 测量上线效果:对比改造前后的请求量、延迟和错误率。
- 验证恢复能力:模拟断网和服务重启,确认前端能自动恢复订阅。
如果你只记住一句话:
SSE 不是“功能缩水版 WebSocket”,它是“单向实时场景里的效率解”。