SSE 与 WebSocket 怎么选
更新时间:2026-09-18。服务端推送两大方案里,SSE(Server-Sent Events)经常被低估:它就是一个永不关闭的 HTTP 响应,浏览器用原生 EventSource 接收,自带重连和断线续传,实现单向推送比 WebSocket 简单得多。本文讲清 SSE 协议细节、它和 WebSocket/gRPC 流的边界,帮你在「通知/行情/日志流」和「聊天/协同/双向交互」之间一次选对。基础见 WebSocket 与 SSE,重连细节见 WebSocket 心跳与重连。
本文要回答的问题
- SSE 协议长什么样?
text/event-stream的 data/id/event/retry 怎么用? - EventSource 的自动重连和 Last-Event-ID 续传是怎么工作的?
- SSE 有哪些硬限制(单向、HTTP/1.1 连接数、文本、代理缓冲)?
- WebSocket、SSE、gRPC 流、长轮询到底怎么选?
- Nginx/CDN 缓冲、HTTP/2、鉴权等工程坑怎么处理?
一、SSE 本质:一个永不结束的 HTTP 响应
服务端把响应头设成 Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive,然后持续往响应体写「事件块」,用空行分隔:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: 101
event: price
data: {"symbol":"AAPL","price":182.3}
id: 102
retry: 3000
data: 这是一条多行消息第一行
data: 第二行
: 这是注释/心跳字段:data 是负载(多个 data 行会用换行拼成一条);id 用于断线续传;event 指定事件类型(前端 addEventListener 对应);retry 告诉浏览器重连间隔;以 : 开头的是注释,常用来每 15~30s 发一条心跳注释,防代理超时。
二、浏览器端:EventSource 极简且自动重连
const es = new EventSource('/api/stream?token=xxx');
es.addEventListener('price', (e) => {
const data = JSON.parse(e.data);
render(data);
});
es.onerror = () => { /* 浏览器会自动重连,通常无需手写退避 */ };EventSource 的关键红利:连接断开时浏览器自动重连,并自动带上 Last-Event-ID 请求头(值为最后收到的 id)。服务端据此从该 id 之后继续推,实现断线期间不丢消息:
服务端维护每个连接的游标:
if header 'Last-Event-ID' = 102:
从 103 开始补发,再进入实时推送这比 WebSocket 要自己实现心跳、退避、ACK 续传(见 WebSocket 重连设计)省一大截。需要 POST/请求体、自定义鉴权头时,原生 EventSource 能力弱(不能自定义 header),可用 fetch + ReadableStream 手写或 polyfill(@microsoft/fetch-event-source)。
三、SSE 的硬限制
- 单向:只能服务端→浏览器。客户端要回传就再发普通 HTTP 请求;频繁双向交互就不适合。
- 默认走文本(UTF-8):二进制要用 Base64 或转码,有开销;音视频/二进制流不合适。
- HTTP/1.1 同域 6 连接限制:浏览器对同域名 HTTP/1.1 连接数有限制(约 6 个),多个 SSE 标签页会占满并阻塞同域其他请求;走 HTTP/2 后所有流复用一条连接,该限制消失,部署上应优先 HTTP/2。
- 代理/CDN 缓冲与压缩:Nginx 默认
proxy_buffering on会攒一批才转发,导致推送延迟,必须proxy_buffering off; proxy_cache off;,并放开proxy_read_timeout;CDN 也要确认支持流式不缓冲。 - 鉴权:EventSource 不便设 Authorization 头,常用短期 token 走 query string 或 cookie,注意别在日志泄露。
四、决策对照表
| 维度 | SSE | WebSocket | 长轮询 | gRPC 流 |
|---|---|---|---|---|
| 方向 | 服务端→客户端(单向) | 全双工双向 | 准单向 | 单/双向 |
| 协议 | 普通 HTTP | HTTP 升级后的 WS 协议 | HTTP 反复请求 | HTTP/2 + 二进制 |
| 自动重连 | 浏览器原生+Last-Event-ID | 需自己实现 | 天然循环 | stub 重连 |
| 二进制 | 仅文本 | 支持 | 支持 | 支持 |
| 穿透/兼容 | 极好(就是 HTTP) | 需代理支持 Upgrade | 最好 | 浏览器需 gRPC-Web |
| 典型场景 | 通知、行情、日志、AI 流式输出 | 聊天、协同编辑、游戏、双向实时 | 老旧兼容兜底 | 微服务间流式 |
经验法则:只需要服务端往下推——通知中心、股票/监控看板、任务进度、大模型 token 流式输出——优先 SSE(尤其 HTTP/2 下),简单且自带续传;需要客户端高频上行或双方实时交互(IM、白板、协同、实时对战)才用 WebSocket;服务到服务、强类型多语言用 gRPC 流(见 四种流模式)。
五、坑与对照
- 用 WebSocket 做纯通知。 单向推送却要自己维护心跳/重连/补发,SSE 开箱即用。
- 忘关 Nginx buffering。 表现为「消息攒一堆突然一起来」,关 proxy_buffering、配长 read timeout。
- HTTP/1.1 多标签占满 6 连接。 启用 HTTP/2 或收敛连接。
- 不发心跳。 中间设备静默断连,SSE 也应每 20~30s 发注释心跳。
- 以为 SSE 能传任意二进制/客户端能推。 文本单向是其边界。
- 续传 id 不用单调值。 Last-Event-ID 依赖有序、可比较的 id,乱序会补发错。
WebSocket 的完整保活重连与消息补偿见 WebSocket 心跳与重连;REST 分页/接口风格见 REST API 设计。
总结
SSE 与 WebSocket 选型:SSE 是一个 Content-Type: text/event-stream 的不关闭 HTTP 响应,用 data/id/event/retry 字段推消息,浏览器原生 EventSource 自动重连并带 Last-Event-ID 让服务端从游标补发,配合注释心跳和 Nginx 关 proxy_buffering,做通知、行情、日志、AI token 流式等单向推送最简单,HTTP/2 下还消除了 HTTP/1.1 同域 6 连接限制;但它只支持服务端到客户端的文本,需要双向高频交互、二进制或客户端实时上行(聊天、协同、游戏)才用 WebSocket,微服务间强类型流用 gRPC-Stream——单向选 SSE、双向选 WS、服务间选 gRPC。