梦亦同趋

SSE 和 WebSocket的使用及差异

SSE 是"服务器→客户端"的单向广播;WebSocket 是"客户端⇄服务器"的双向对讲机

#一、SSE 和 WebSocket

SSE(Server-Sent Events,服务端推送事件),顾名思义,是服务器单向往客户端"塞"数据的一种机制。它底层还是 HTTP,只是把连接保持住,服务器想什么时候推就什么时候推。浏览器原生给了个 EventSource API,拿来即用。

WebSocket,则是另一种协议。它在 HTTP 握个手之后,把这条 TCP 连接"升级"成全双工通道——客户端能发、服务器能发,两边随时互怼,谁也不用等谁。

看起来都是"实时",但一个只出不进,一个能进能出。这个本质差别,决定了后面所有的使用姿势。

#二、核心差异

二者的主要差异如下:

维度SSEWebSocket
通信模型半双工、单向(仅服务器 → 客户端)全双工、双向(客户端 ⇄ 服务器)
所处协议层应用层,承载于 HTTP(HTTP/1.1 或 HTTP/2)独立应用层协议;握手借 HTTP,之后脱离 HTTP 语义
建连方式标准 HTTP 请求,Accept: text/event-stream,响应后保持连接不关闭HTTP 请求带 Upgrade: websocket,服务器回 101 Switching Protocols 完成协议切换
传输单位文本事件流(UTF-8 必选),空行分隔消息二进制(frame):含 FIN / opcode / 掩码位,文本与二进制皆支持
客户端发消息协议层不支持;需另起 fetch / XHR原生支持
数据类型仅文本(UTF-8)文本 + 二进制(Blob / ArrayBuffer)
断线重连浏览器 EventSource 原生重连,并带 Last-Event-ID 续传无内置,需业务层自实现心跳 + 退避重连
消息序号 / 续传有:id: 字段 + Last-Event-ID 机制无内置,需业务层自定义序号 / ACK
并发连接受 HTTP/1.1 每域约 6 条连接上限约束,多路 SSE 易占满连接池单条"升级连接"承载双向,不占该限额
代理 / 防火墙走标准 HTTP 长连接;但中间代理可能缓冲响应(需关缓冲)需代理支持 Upgrade;部分老旧代理 / 安全设备会拦截
浏览器原生 APIEventSource(含自动重连)WebSocket(不含重连)
同源策略遵循标准同源 / CORS握手阶段走 CORS;之后不受同源限制,且无法自定义请求头(鉴权走 Cookie 或 query 参数)

#三、差异详解

#1. 协议与握手:一个借道,一个另起炉灶

SSE 严格说不算新协议——它只是把一次普通的 HTTP 响应"不关掉"。客户端发 GET 请求,带 Accept: text/event-stream;服务器回 Content-Type: text/event-stream不结束响应,之后想推就推。它跑在 HTTP/1.1 上是长连接,跑在 HTTP/2 上则复用单条连接做多路复用。中间的 Nginx、CDN、负载均衡全当普通 HTTP 处理,几乎零适配成本——但有个坑在第 4 节说。

WebSocket 是在 HTTP 握手阶段"叛变"。客户端请求带:

text
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13

Sec-WebSocket-Key16 字节随机数经 Base64 得到。服务器必须把它和固定魔数 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接,做 SHA-1 再 Base64,作为 Sec-WebSocket-Accept 返回(RFC 6455 给出的标准示例值正是 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=)。客户端校验通过、收到 101 Switching Protocols 后,这条 TCP 连接才正式切换为 WebSocket 协议,彻底脱离 HTTP 语义。

正因如此,老旧的代理、WAF、企业防火墙如果不理解 Upgrade,往往会直接断开这条连接——这是 WebSocket 穿透性差、常被拦的根源,不是玄学。

#2. 数据格式与分包:流 vs 帧

SSE 是纯文本事件流,且必须是 UTF-8。一条事件由若干行组成,字段前缀是硬约定:

sh
id: 1024 event: price data: {"symbol":"BTC","price":61234} data: 没有 event 名的默认消息(触发 onmessage) retry: 3000

规则很硬:

  • data: 可多行,客户端自动拼接;事件以空行收尾。
  • id: 给事件编号;断线重连时浏览器会把最后收到的 id 作为 Last-Event-ID 请求头带回(见第 3 节)。
  • event: 自定义事件名,客户端用 addEventListener('price', ...) 监听,不指定则走默认 onmessage
  • retry: 指定重连间隔(毫秒)。
  • 二进制不支持——要推图片、音频,SSE 直接歇菜。
  • 好处是人类可读:一把 curl 就能看流,调试极其友好。

WebSocket 是基于消息、用帧封装。一条消息可能被拆成多个 frame,每个 frame 带:FIN 位(是否为消息最后一帧)、opcode(0x1 文本 / 0x2 二进制 / 0x8 关闭 / 0x9 ping / 0xA pong)、payload 长度,以及掩码位(规范强制:客户端 → 服务器 的帧必须掩码,服务器 → 客户端 不掩码)。文本与二进制通吃(收到的是 stringBlob / ArrayBuffer)。代价是:对象得自己 JSON.stringify / parse,分包、心跳格式全得业务层定。

#3. 断线重连与续传:一个开箱即用,一个全靠自己

这是 SSE 最容易被忽略、却最香的一点。EventSource 原生断线重连:连接一掉,浏览器按内部退避(约 3 秒起,实现相关)自动重连;如果服务器下发的事件带过 id:,重连请求会带上 Last-Event-ID 头,服务器据此从断点续推——这是协议级的续传保证

WebSocket 则什么都没给:掉线就触发 onclose,没有自动重连、没有消息序号、没有续传。你要保活就得自己垒:应用层心跳、超时检测、onclose 里指数退避重连、必要时自己维护消息序号 / ACK 来做续传。喜闻乐见的"我又得自己造轮子"环节

补充一个常被误解的点:WebSocket 协议确实有 ping / pong 控制帧(opcode 0x9 / 0xA),属于协议层保活。但浏览器实现的 WebSocket API 不向你暴露发送协议级 ping 的能力——ping / pong 由浏览器在底层自动处理(收到服务器 ping 自动回 pong)。所以应用层想判断"连接是不是还活着",往往还是得自己发业务心跳消息。

#4. 两个常被忽略的硬约束(选型时真会踩)

并发连接数(SSE 的暗坑)。浏览器对同一域名的 HTTP/1.1 持久连接通常有约 6 条上限。SSE 每条流占一条长连接——如果你在前端同时开好几个 SSE(比如每个看板一个流),连接池很快被占满,连带普通接口请求都被阻塞、排不上队。WebSocket 是单条"升级连接"双向复用,不占这条限额。缓解办法:优先上 HTTP/2(多路复用,一条 TCP 承载多流),或把多个业务合并成一个 SSE 多路流(用 event: 区分)。

代理缓冲(SSE 的另一暗坑)。SSE 经过 Nginx、CDN 时,默认会缓冲响应——消息攒一批才一次性下发,表现就是"明明每秒在推,前端却一两秒才收到一次"。解决办法是在响应头声明不要缓冲:

text
Cache-Control: no-cache X-Accel-Buffering: no

并确保分块传输。WebSocket 一般不受缓冲影响,但反过来要求代理正确转发 Upgrade

text
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";

否则握手过不去。

一句话收这节:SSE 的短板不在"能力",而在"两条长连接相关的坑"(连接数、缓冲);WebSocket 的短板不在"能力",而在"什么都不给你,全得自己造"。


#四、实用场景

#用 SSE 更合适的场景

  • 服务端主动通知:站内信、系统公告、审批待办,"有新消息就推你"。
  • 实时数据看板:监控指标、日志流、CI 构建进度,本质都是"服务器单向灌"。
  • 股票/行情/比分推送:高频更新但只出不进。
  • AI 流式输出:现在大模型一个字一个字往外蹦的"打字机效果",SSE 是最顺手的载体(这也是为什么 OpenAI、各大模型网关默认选它)。

一句话:只要你的需求是"服务器单方面持续告诉客户端",SSE 基本都能优雅胜任,还白送自动重连。

#用 WebSocket 更合适的场景

  • 即时聊天 / IM:收发都是高频双向,SSE 单向撑不住。
  • 多人协同编辑:光标位置、内容变更双向实时同步。
  • 实时对战游戏:低延迟双向 + 可能传状态快照(二进制)。
  • 需要客户端频繁上报的业务:比如实时轨迹、传感器数据上传同时又要收指令。

一句话:只要"客户端也要持续往服务器发",或者要传二进制,上 WebSocket。