跳至正文

SSE

Server-Sent Events

SSE 是一种基于 HTTP 的服务端单向推送技术。服务端通过一个不关闭的 HTTP 响应持续向客户端发送事件流。

Note

特点

  • 单向通信:仅服务端向客户端推送,客户端发数据用普通 HTTP 请求
  • 基于 HTTP:使用标准 HTTP 协议,无需协议升级,天然兼容代理和防火墙
  • 自动重连:浏览器原生支持断线自动重连,并携带 Last-Event-ID 恢复
  • 文本协议:数据格式为纯文本(text/event-stream),轻量易调试

Note

局限

  • 单向通信:客户端发数据需另发 HTTP 请求
  • EventSource 只支持 GET:不能发送 body,需要 POST 时必须用 fetch + ReadableStream 替代
  • 不支持自定义请求头EventSource 无法直接带 Authorization,需通过 URL 传参或 fetch 替代
  • 仅文本:不支持二进制数据
  • HTTP/1.1 每域名 6 连接上限是共享的:SSE 连接和该域名下的其他 HTTP 请求共享这 6 个槽位,多开 SSE 会挤占 AJAX / 图片加载。HTTP/2 的多路复用可解决

Note

适用场景

通知推送、实时数据面板、日志流、AI 流式输出——服务端单向推送的场景。客户端发数据用普通 HTTP 请求即可。

完整选型对比见 API 通信总览

SSE vs WebSocket 的通信形态

┏━━━━━━━━━━━━ WebSocket ━━━━━━━━━━━━┓
┃                                    ┃
┃  Client ◀━━━━ data ━━━━━▶ Server   ┃  全双工,双向通信
┃                                    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


┏━━━━━━━━━━━━━━ SSE ━━━━━━━━━━━━━━━━┓
┃                                    ┃
┃  Client ◀━━━━ event ━━━━ Server    ┃  单向,服务端推送
┃  Client ◀━━━━ event ━━━━ Server    ┃  基于 HTTP,自动重连
┃  Client ◀━━━━ event ━━━━ Server    ┃  纯文本流
┃                                    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

基础概念

概念一句话说明
EventSource浏览器原生 API,用于接收 SSE 事件流
event stream服务端返回的 text/event-stream 格式数据流
event事件名称,客户端通过 addEventListener 监听指定事件
data事件携带的数据,支持多行
id事件 ID,断线重连时通过 Last-Event-ID 请求头恢复
retry服务端指定客户端重连间隔(毫秒)

工作原理

SSE 本质上就是一个不会结束的 HTTP 响应

Client                                       Server
  │                                             │
  │─── GET /api/events ────────────────────────▶│  ① 普通 HTTP 请求
  │◀── 200 OK                                   │  ② 响应头:Content-Type: text/event-stream
  │    Content-Type: text/event-stream          │
  │                                             │
  │◀── data: {"msg": "hello"}\n\n ──────────────│  ③ 持续推送事件
  │◀── data: {"msg": "world"}\n\n ──────────────│     (响应不关闭)
  │◀── data: {"msg": "..."}\n\n ────────────────│
  │                                             │
  │    (连接断开)                              │  ④ 网络异常
  │                                             │
  │─── GET /api/events ────────────────────────▶│  ⑤ 浏览器自动重连
  │    Last-Event-ID: 3                         │     携带上次事件 ID
  │◀── 200 OK ...                               │  ⑥ 服务端从断点恢复

Note

要点:SSE 就是 HTTP,不是新协议

  • 不需要协议升级(没有 101 Switching Protocols
  • 服务端只需设置正确的响应头,然后不关闭响应,持续写入数据
  • 所有 HTTP 基础设施(代理、CDN、负载均衡)都天然支持
  • 这也是为什么 SSE 比 WebSocket 简单得多

协议格式

SSE 使用纯文本格式,每个字段占一行,事件之间用空行分隔。

单个事件的完整结构:

event: notification
data: {"title": "新消息"}
id: 42

连续多个事件的流(空行是事件边界):

event: message
data: {"text": "Hello"}
id: 1

event: message
data: {"text": "World"}
id: 2

特殊字段(注释、省略 event、retry):

: 这是注释,会被忽略(常用于心跳保活)

data: 没有 event 字段时,触发默认 message 事件
id: 3

retry: 5000
data: 设置重连间隔为 5 秒(retry 被浏览器记住,同时 data 仍会触发 message 事件)

Note

要点:retry 不是独立指令

retry 只是事件中的一个字段。把 retry: Ndata: ... 放在同一事件里,浏览器会同时:① 记住新的重连间隔;② 触发对应的 message 事件。如果只想下发 retry 不触发业务事件,要么单独一个只含 retry 和空行的”事件”(无 data 则不触发事件),要么放在注释行前后让它独立成组。

字段说明

字段说明是否必须
data事件数据,可以多行(最终用 \n 拼接)
event事件类型名,省略则为默认 message
id事件 ID,断线重连时通过 Last-Event-ID 恢复否(但强烈建议)
retry重连间隔(毫秒),客户端据此决定多久后重连
: 注释以冒号开头的行,被忽略

Note

格式要点

  • 每个字段格式为 field: value(冒号后有一个空格)
  • data 可以有多行,最终会用 \n 拼接
  • 事件之间用空行\n\n)分隔,这是事件边界的标志
  • : 开头的行是注释,常用于心跳保活(防止代理因超时断开连接)

多行 data 示例

data: 第一行
data: 第二行
data: 第三行

客户端收到的 event.data"第一行\n第二行\n第三行"

浏览器端:EventSource API

const es = new EventSource("/api/events");

// 默认 message 事件(没有 event 字段的事件)
es.onmessage = (event) => {
  console.log("收到:", event.data);
  console.log("ID:", event.lastEventId);
};

// 监听自定义事件
es.addEventListener("notification", (event) => {
  const data = JSON.parse(event.data);
  console.log("通知:", data);
});

es.onopen = () => {
  console.log("连接已建立");
};

es.onerror = () => {
  // 不需要手动重连,浏览器会自动处理
  if (es.readyState === EventSource.CLOSED) {
    console.log("连接已永久关闭");
  } else {
    console.log("连接中断,浏览器正在重连...");
  }
};

// 主动关闭(调用后不会自动重连)
es.close();

Note

readyState 三种状态

常量含义
0EventSource.CONNECTING连接中 / 重连中
1EventSource.OPEN连接已建立
2EventSource.CLOSED连接已关闭(不会重连)

只有 close() 被调用,或服务端返回非 200 / 非 text/event-stream 才会进入 CLOSED。网络中断只会进入 CONNECTING(重连中)。

服务端:响应头要求

res.writeHead(200, {
  "Content-Type": "text/event-stream", // 告诉浏览器这是 SSE
  "Cache-Control": "no-cache", // 禁止缓存
  Connection: "keep-alive", // 保持连接
});

完整 Node.js 实现见 Node.js 实现

断线重连机制

浏览器原生支持断线重连,且能从断点恢复:

① 服务端发送事件时附带 id
   → id: 42\ndata: {...}\n\n

② 连接断开(网络异常)

③ 浏览器等待 retry 时间后自动重连(默认约 3 秒)

④ 重连请求自动携带请求头 Last-Event-ID: 42

⑤ 服务端读取 Last-Event-ID,从 43 开始继续推送

Note

要点

  • id 字段是断点恢复的基础,不发 id 就无法恢复
  • retry 字段可控制重连间隔,单位毫秒(retry: 5000 = 5 秒)
  • 重连时浏览器自动在请求头中带上 Last-Event-ID,无需手动处理
  • 服务端需根据 Last-Event-ID 找到断点(内存队列、数据库、Redis 等)
  • close() 关闭的连接不会重连

POST 与 ReadableStream 替代方案

EventSource 只支持 GET 且不能带自定义请求头。需要 POST 或 Authorization 时,用 fetch + ReadableStream 手动解析:

const res = await fetch("/api/chat", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + token,
  },
  body: JSON.stringify({ prompt: "Hello" }),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  // 按 SSE 协议切分事件(空行分隔)
  const events = buffer.split("\n\n");
  buffer = events.pop() ?? "";

  for (const evt of events) {
    const data = evt
      .split("\n")
      .filter((l) => l.startsWith("data:"))
      .map((l) => l.slice(5).trim())
      .join("\n");
    console.log(data);
  }
}

Note

要点

  • 数据格式仍然遵循 SSE 协议data: / event: / id: / 空行分隔)
  • 但断线重连逻辑需要自己实现(没有 Last-Event-ID 自动行为)
  • AI 流式响应(如 ChatGPT API)几乎都是这种模式

实际应用场景

AI 流式响应

Client ─── POST /api/chat (普通 HTTP 请求) ──▶ Server
Client ◀── text/event-stream                   Server

data: {"token": "你"}
data: {"token": "好"}
data: {"token": ","}
data: {"token": "我"}
data: {"token": "是"}
data: {"token": "AI"}
data: [DONE]

其他常见场景

场景说明
通知推送新消息、系统公告,服务端有通知就推
实时数据面板监控仪表盘、股票行情,定时推送最新数据
构建 / 部署日志CI/CD 日志实时输出到浏览器
进度条长时间任务(上传、转码、导出)的进度反馈