实时通信:SSE vs WebSocket vs Polling
把 SSE/WebSocket/轮询放在一起对比:通信方向、可靠性、基础设施约束与典型场景。
[!info] related notes
- 所属 MOC: HTTP 与前端网络请求 MOC
- 概念: sse, web-socket, polling
- 流式响应: frontend-streaming-data-handling, frontend-streaming-response-patterns, AI 聊天与流式 UI
- Durable recovery: Durable SSE Recovery with after_seq
- 常见坑: blogs/dailyuse-sse-compression-buffering-debug
实时通信:SSE vs WebSocket vs Polling
范围
这篇是“选型关系笔记”,不重复解释每个概念的所有细节,只把关键差异、恢复边界与面试表达讲清楚。
心智模型:先选通信形状,再谈协议
不要先问“WebSocket 是否更高级”,先问业务的数据流是什么形状:
低频查询 / 对实时性要求不高
→ Polling
持续 server → client 文本/事件流
→ SSE / HTTP streaming
client ↔ server 双向高频通信
→ WebSocket
最关键的一句话:
SSE 和 WebSocket 的首要差别不是“谁性能更强”,而是通信方向和抽象是否匹配业务。
AI 文本生成通常是服务器持续向浏览器推送 token/event,浏览器并不需要在同一条长连接上持续高频上行,因此 SSE 往往已经足够。
抓住 4 个问题
- 你需要双向吗?客户端是否也要持续、高频发消息?
- 数据主要是文本事件还是需要二进制帧?
- 实时性要求有多高,能不能接受周期性请求延迟?
- 网络、代理、CDN、负载均衡是否会干扰长连接,断线后怎么恢复?
对比要点
SSE
- 单向:
server -> client - 基于 HTTP,典型媒体类型是
text/event-stream - 浏览器原生
EventSource提供自动重连,并支持Last-Event-ID语义 - 很适合模型流式输出、通知、日志、进度等持续服务端推送
- 原生
EventSource只支持 GET;如果业务必须 POST body 后再返回 SSE,可以使用fetch()+ReadableStream手动消费,此时重连/恢复逻辑通常要由应用自己实现
WebSocket
- 双向全双工:
client <-> server - 在 HTTP/1.1 场景通常先通过 HTTP handshake/Upgrade 建立 WebSocket 连接,之后传输 WebSocket frames
- 浏览器 WebSocket API 本身不替应用提供完整自动重连、恢复和业务 replay 语义
- 适合双方都持续高频交互的场景,例如 IM、协作编辑、实时游戏、语音控制等
[!note] 性能不要绝对化 不能简单说“WebSocket 一定比 SSE 更耗性能”。WebSocket 在高频双向、二进制、小帧通信时反而可能更合适;SSE 的优势主要是单向 HTTP 语义简单、和现有 HTTP 基础设施更自然。选型应看通信形状、恢复复杂度与基础设施,而不是只看协议名字。
Polling
- 客户端周期性主动请求服务器
- 不需要维持特殊长连接,基础设施兼容性好
- 代价是额外请求开销,以及实时性受 polling interval 限制
- 低频变化、恢复兜底、长连接环境不稳定时依然非常实用
AI 流式回答为什么常选 SSE
典型 Agent/LLM 回答:
user command
→ server starts Agent run
→ server continuously emits events/token deltas
→ browser renders progressively
主要方向是:
server → browser
因此 SSE/HTTP streaming 的抽象很贴合。WebSocket 当然也能实现,但如果客户端没有持续高频上行需求,就会承担额外的双向连接管理复杂度,却没有获得明显业务收益。
选 SSE 不等于“只要连接不断就行”
Production streaming 的关键问题不是永远不断线,而是:
连接可以断
但业务执行不能因此丢失
客户端还能恢复到服务器事实
如果架构只是:
LLM delta
→ SSE
→ Browser
连接在事件 5 后断开,而服务器继续产生 6..10,那么浏览器永久缺少 6..10。
因此可靠 Agent streaming 更适合:
Agent Event
↓
Durable Runtime Event Log
├──→ Live SSE(低延迟)
└──→ Replay API(恢复)
见 Durable SSE Recovery with after_seq。
Seq / Cursor / Replay 的恢复模型
假设客户端已经成功应用:
seq 1, 2, 3, 4, 5
本地 high-water mark:
maxSeq = 5
断线后可以请求:
GET events?after_seq=5
服务端从 durable event log 返回:
6, 7, 8, 9, 10
然后客户端继续投影。
如果恢复结果因为 race/重试带回:
4, 5, 6, 7, 8, 9, 10
客户端必须根据稳定事件身份 / (run_id, seq) 去重,跳过已经应用的 4,5,否则 text delta、tool card、citation 或 side effect 都可能重复。
这和 TCP 序号“像”,但不是在重新实现 TCP
seq + after_seq 的直觉确实像 TCP sequence/cursor:双方需要知道“我已经可靠消费到哪里”。但应用层事件恢复解决的是不同问题:
TCP
→ 字节可靠、有序地从一条连接传输
Application Event Replay
→ 连接已经断过/进程可能重启后,恢复业务事件历史和 UI projection
TCP 不能替应用回答:
Agent run 是否仍在继续?
哪些事件已经 durable commit?
刷新页面后应该恢复到哪个业务状态?
重复 replay 是否应触发 side effect?
所以更准确的说法是:借用了“有序编号 + 已确认位置 + 从位置之后追赶”的思想,但恢复的是应用层事件,不是网络字节。
BodySense 当前实践
BodySense 当前不是使用原生 EventSource,而是 POST 请求返回 SSE,并由 fetch/ReadableStream 手动解析。因此它自己维护恢复协议:
Live SSE
→ StreamEvent.seq
→ maxSeq
transport disconnect / terminal not confirmed
→ GET /conversations/:conversationId/runs/:runId/events?after_seq=maxSeq
→ durable Runtime Event Log
→ ordered replay
→ skip seq <= maxSeq
→ same handlers / reducer
服务端查询语义是:
WHERE conversation_id = ?
AND run_id = ?
AND seq > after_seq
ORDER BY seq ASC
这体现了一个非常重要的分层:
SSE connection = low-latency transport
Runtime Event Log = durable recovery authority
Reducer/read model = client-visible projection
典型建议
- 模型/Agent 回复逐字输出:优先 SSE 或 HTTP streaming
- 双向高频协作、游戏、IM、实时控制:优先 WebSocket
- 低频刷新、受限环境或恢复兜底:Polling 依然合理
- 需要 Production-grade streaming:无论 SSE 还是 WebSocket,都要另外设计 durable identity、replay、dedup、terminal authority
面试 / 交流表述
“我不会简单用 WebSocket 替代所有实时通信。选型先看通信方向:如果是 LLM/Agent 这种主要 server-to-client 的持续文本事件流,SSE 更贴合 HTTP 语义;如果双方都需要持续高频通信,再考虑 WebSocket。真正的生产问题也不只是保持长连接,而是断线恢复,所以我会把 live transport 和 durable state 分开:事件先有稳定的 run identity/sequence,并落到可恢复的 event log;客户端记录已消费 cursor,断线后用
after_seqcatch up,再对 replay 做去重。这样连接可以失败,但最终 projection 仍能收敛到服务器事实。”
最短记忆方式
Polling:我定期问“变了吗?”
SSE:服务器持续告诉我“发生了什么”
WebSocket:双方都可以持续说话
Production recovery:
transport 可以断,durable event 不应该丢;
seq 定位位置,after_seq 追赶,dedup 防重复。