一文搞懂网络协议里的 SSE:用最小成本做“服务器实时推送”

5 阅读6分钟

你做过这种页面吗:订单状态、构建日志、监控告警、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-stream
  • Cache-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 步:运行与预期结果

  1. 启动服务端:node server.js
  2. 把前端文件放进 public 目录后,打开服务端首页
  3. 你会看到 0% -> 10% -> ... -> 100% 的实时变化
  4. 中途断网再恢复,浏览器会尝试重连

这套流程跑通后,你下一步应该把示例里的 taskId/percent 换成真实业务字段,并在服务端补上鉴权与限流。

最常见的 5 个坑(以及怎么避开)

  1. 忘记事件结束空行
    每条事件必须以空行结束,不然前端可能一直不触发回调。

  2. 被反向代理缓冲
    某些网关会缓存响应,导致“服务端在推,浏览器收不到”。要关闭缓冲或放行流式响应。

  3. 没有心跳导致中间链路断开
    长时间无消息时,代理可能判定连接空闲并断开。定时发送注释行 : ping

  4. 断线后无法续传
    不维护事件 id,重连后只能从当前状态开始。关键业务建议保留最近事件窗口,按 Last-Event-ID 续传。

  5. 把 SSE 当成双向通道
    SSE 天生单向。客户端上行仍走普通 HTTP 接口,别把它改造成“半残 WebSocket”。

这几个坑你只要提前做一轮自测,线上告警数量通常会少一截,值班同学会真心感谢你。

收尾:把 SSE 用对,比“全栈上重武器”更重要

你现在可以把 SSE 当成一个明确工具,而不是“实时技术里的备胎”:

  • 检查业务方向:先确认是不是“服务端推、客户端收”为主。
  • 选择实现方案:单向实时优先 SSE,双向互动再上 WebSocket。
  • 测试链路细节:重点测事件空行、代理缓冲、心跳和重连。
  • 测量上线效果:对比改造前后的请求量、延迟和错误率。
  • 验证恢复能力:模拟断网和服务重启,确认前端能自动恢复订阅。

如果你只记住一句话:
SSE 不是“功能缩水版 WebSocket”,它是“单向实时场景里的效率解”。