实时 Web:SSE 与 WebSocket 深度对比
服务端推送需求一来,就纠结 SSE 还是 WebSocket?本文讲清两者的本质区别(SSE 单向/HTTP/简单,WebSocket 双向/独立协议/灵活)、各自适合的场景、代理/防火墙兼容性、连接数限制,给一份"什么场景选哪个"的明确结论。
本文要点
- 主题聚焦:Next.js、TypeScript
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
但凡做过实时功能,这道选择题就绕不开:服务端要往浏览器推数据,用 SSE(Server-Sent Events) 还是 WebSocket?
网上大部分文章给的是"两者都能用,看需求"——这句正确但没用。真正要决策的时候,你想知道的是:它俩到底在协议层差在哪?穿代理谁更稳?为什么 LLM 流式输出几乎清一色用 SSE?什么时候必须上 WebSocket?本文把这些问题一次讲透。
一、本质区别:协议层面的根本不同
先抛开 API,看协议。这是所有差异的源头。
SSE 不是新协议,它就是 HTTP。 客户端发起一个普通 HTTP GET 请求,服务端不关连接,持续往响应体里写数据,格式是简单的 data: xxx\n\n。浏览器通过 EventSource 这个原生 API 接收。
GET /events HTTP/1.1
Accept: text/event-stream
---
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"price": 4200}
data: {"price": 4201}
WebSocket 是独立协议。 它借 HTTP 做一次"升级握手"(101 Switching Protocols),握手成功后,那条 TCP 连接就被"劫持"了,从此不再说 HTTP,改说 WebSocket 自己的二进制帧协议,而且是全双工——客户端和服务端可以同时、随时互发消息。
GET /ws HTTP/1.1
Upgrade: websocket
---
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
(此后该 TCP 连接脱离 HTTP,进入双向帧通信)
一句话总结:SSE 是"服务端单向往浏览器推"的 HTTP 长连接;WebSocket 是"双向实时"的独立协议。 方向性、协议、复杂度,三者一体。
二、SSE 的优势:简单、HTTP 友好
SSE 的优点几乎全部来自"它就是 HTTP"这一点。
第一,穿防火墙、反向代理、CDN 几乎零成本。 因为它就是普通 HTTP 请求/响应,nginx、Cloudflare、企业内网代理默认都能正确处理。不需要额外配置协议升级,不需要担心某个中间盒不认识 WebSocket 握手包。
第二,浏览器原生自动重连。 EventSource 连接断了,浏览器会自动尝试重连,还会带上 Last-Event-ID 头,服务端据此恢复断点续传。这是协议内置能力,你不用自己写重连逻辑。
// 客户端:三行代码搞定
const es = new EventSource('/api/events');
es.onmessage = (e) => console.log(JSON.parse(e.data));
es.onerror = () => { /* 浏览器会自动重连,不用手动处理 */ };
第三,API 极简。 服务端只要设置正确的响应头,然后往流里写文本即可;客户端 EventSource 用 onmessage/addEventListener 就能收消息。心智负担极低。
第四,HTTP/2 下天然多路复用。 多个 SSE 流可以复用同一条 HTTP/2 连接,不会像 HTTP/1.1 那样撞连接数上限(详见下一节)。
三、SSE 的劣势:单向、文本、连接数限制
劣势也来自"单向 HTTP"这个本质。
第一,单向。 服务端→客户端可以推,客户端→服务端只能走普通 HTTP(发个 POST)。如果你想做聊天、协作编辑这种"双方互发"的场景,SSE 会非常别扭——得开两条路,上行 HTTP、下行 SSE,状态还得自己同步。
第二,文本为主。 SSE 传的是 UTF-8 文本,二进制数据要 Base64 编码,有膨胀开销。传图片、音视频流不合适。
第三,同域连接数限制(HTTP/1.1 下)。 浏览器对同一域名的 HTTP/1.1 连接数有上限(Chrome/Firefox 一般是 6 个)。每个 SSE 占一条连接,开 6 个 SSE 就可能把 HTTP/1.1 的连接池占满,连普通请求都卡住。这个限制在 HTTP/2 下消失,所以生产环境务必走 HTTP/2。
另外,EventSource 只支持 GET、不能自定义请求头(比如塞 Bearer token 进去),鉴权通常得靠 Cookie 或 URL 参数,稍微有点别扭。
四、WebSocket 的优势:双向、二进制、低延迟
WebSocket 的优点,正是 SSE 的短板所在。
第一,全双工双向。 这是它存在的核心理由。客户端、服务端可以随时互发消息,天然适合聊天、协作、多人游戏、实时白板这类"你来我往"的场景。
第二,二进制友好。 WebSocket 帧可以传 ArrayBuffer/Blob,做游戏同步、音视频传输、大量结构化二进制数据没有编码膨胀。
第三,协议开销低、延迟低。 握手之后,每个数据帧头部只有 2-14 字节,比每次 HTTP 请求带一坨头要轻得多。高频小消息(比如位置同步)用 WebSocket 明显更省。
// 客户端
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send(JSON.stringify({ type: 'chat', text: 'hi' })); // 双向!
五、WebSocket 的劣势:穿代理复杂、状态管理全靠自己
第一,独立协议,穿代理和 CDN 麻烦。 虽然握手借了 HTTP,但中间任何一环(企业代理、某些 CDN、负载均衡)不支持 Upgrade 就断链。nginx 要单独配 proxy_set_header Upgrade $http_upgrade;,Cloudflare 有一堆 WebSocket 专属注意事项,HTTP/2 代理还涉及 :protocol 扩展(Connect/扩展协商)。SSE 在这方面几乎不用操心。
第二,基础设施成本高。 WebSocket 是有状态长连接,服务端要维持每个连接的状态(用户、订阅、心跳)。这意味着负载均衡不能简单轮询,水平扩展要引入 Redis Pub/Sub 之类的消息总线来跨节点广播,部署复杂度上一个台阶。
第三,所有"可靠性"都得自己造。 浏览器原生 WebSocket 不会自动重连,你得自己写重连+退避策略;协议层没有心跳,你得定期 ping/pong 探活,防止连接被中间网络设备悄悄掐断;鉴权也完全自定义(常见做法是握手时带 token 或在子协议里塞)。这些 SSE 大多开箱即用。
六、选型决策:什么场景选哪个
把上面的差异压成一张对比表:
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端→客户端(单向) | 全双工双向 |
| 底层协议 | HTTP | 独立协议(借 HTTP 握手) |
| 数据格式 | 文本(UTF-8) | 文本 + 二进制 |
| 浏览器 API | EventSource(原生,自动重连) | WebSocket(需自己重连) |
| 代理/CDN/防火墙 | 友好(就是 HTTP) | 需额外配置 |
| 连接数限制 | HTTP/1.1 同域约 6 个(HTTP/2 无此限) | 无浏览器层面的硬性限制 |
| 心跳/重连/鉴权 | 协议内置或简单 | 全部自己实现 |
| 服务端复杂度 | 低(就是写 HTTP 响应流) | 高(有状态长连接) |
| 水平扩展 | 相对简单 | 需消息总线 |
| 典型场景 | 通知、行情、日志流、AI 流式 | 聊天、协作、多人游戏 |
决策结论,三句话:
- 单向推送 → SSE。 站内通知、股票行情、构建日志流、AI token 流式输出,服务端只管推、客户端只管收,选 SSE。简单、可靠、基础设施零负担。
- 双向实时 → WebSocket。 即时聊天、协作编辑、多人游戏、实时同步,客户端也要频繁往服务端发消息,选 WebSocket。别用两条 SSE/HTTP 去硬凑,状态同步会让你写到怀疑人生。
- 不确定就先上 SSE。 SSE 的实现成本是 WebSocket 的零头。先用 SSE 跑起来,真撞到"客户端要高频回传"的天花板,再升级到 WebSocket。大部分实时需求,你根本走不到升级那一步。
七、AI 应用的启示:为什么 LLM 流式输出都选 SSE
这一波 AI 应用,几乎重新定义了 SSE 的存在感。你打开 OpenAI、Anthropic、Google、Vercel AI SDK 的流式接口,底层清一色是 SSE——这不是巧合。
原因正是 SSE 的本质特征和 LLM 流式输出完美匹配:
- LLM 流式就是单向推送。 模型生成一个 token、推一个 token 给前端,前端不需要在生成过程中反向发消息。单向 = SSE 的主场。
- 基于 HTTP = 基础设施免费。 LLM 服务通常跑在标准 HTTP 网关后面,用 SSE 等于直接复用现有 CDN、鉴权、限流、可观测性体系,不用为 WebSocket 单独搭一套。
- 简单可靠。 断线自动重连、
text/event-stream格式易调试、浏览器原生支持。对于一个本身就有不确定性(模型可能超时、限流、内容审核)的系统,传输层越简单越好。 - 生态标准化。 OpenAI 的
stream: true、Vercel AI SDK 的useChat、各种 LangChain 流式封装,默认就是 SSE。选 SSE = 站在主流生态这边。
典型的 Next.js 流式响应,服务端就是一个 Route Handler 往下吐 SSE:
// app/api/chat/route.ts (示意)
export async function POST(req: Request) {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
for await (const token of llmStream(await req.json())) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ token })}\n\n`));
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
});
}
前端拿到流以后,边读边渲染打字机效果。整条链路没有一个 WebSocket。这也是为什么——AI 时代,SSE 迎来了第二春。
小结
- 本质差异在协议:SSE 是 HTTP 长连接(单向),WebSocket 是独立协议(全双工)。所有优缺点都从这里派生。
- SSE 胜在简单和 HTTP 友好:穿代理/CDN 零配置、自动重连、API 极简,基础设施成本最低。
- WebSocket 胜在双向和性能:全双工、二进制、低帧开销,是聊天/协作/游戏的必需品。
- WebSocket 代价不低:独立协议穿代理要配置、有状态长连接水平扩展难、重连/心跳/鉴权全自己造。
- AI 流式输出来救场:LLM 流式本质是单向推送,加上生态标准化,SSE 成为事实主流。
金句:需求是单向的,SSE 就够了;只有双向才需要 WebSocket——而 90% 的"实时"需求,其实是单向的。
附带一句实操提醒:SSE 务必配 HTTP/2,绕开 HTTP/1.1 那 6 个连接的天花板;WebSocket 务必配好心跳和重连,别让连接在网络中间悄无声息地死掉。这两条踩稳,选型就不会翻车。
Related