KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

01 · 选型:什么时候用 SSE 而不是 WebSocket — keel 龙骨

这一章回答:四种实时通道各自的成本与失效模式,以及一个可以直接用的决策树。

这一章回答:四种实时通道各自的成本与失效模式,以及一个可以直接用的决策树。

"实时推送"有四种常见做法,选错的代价不是性能差一点,而是在某个环节彻底不可用(例如 WebSocket 被企业代理拦掉、轮询把数据库压垮)。先分清它们的性质。

一、四种通道

通道 方向 协议 开销 断线恢复 典型失效
短轮询 客户端拉 HTTP 每次完整请求头 天然无状态 QPS 与延迟成正比,空转率高
长轮询 客户端拉 HTTP 一次请求挂起直到有数据 需自己带游标 代理超时、连接数、服务端挂起成本
SSE 服务端推 HTTP(长连接) 一个连接 + 文本帧 协议内建(Last-Event-ID) 代理缓冲、压缩、空闲超时
WebSocket 双向 WS(升级握手) 一个连接 + 二进制/文本帧 需自己实现 代理/防火墙不支持升级、负载均衡需额外配置

二、SSE 的真实优势与真实限制

优势(这些是实打实的工程收益):

  1. 就是普通 HTTP:不需要协议升级,穿过企业代理、CDN、API 网关的成功率远高于 WebSocket;
  2. 断线重连是协议自带的:浏览器 EventSource 会自动重连并带上 Last-Event-ID,服务端只要支持续播即可(第 04 章);
  3. 文本协议可调试:curl 就能看流,tcpdump/日志可读,不需要专门工具;
  4. 与 HTTP 基础设施复用:鉴权、限流、日志、追踪都在原有的 HTTP 中间件里。

限制(这些会直接约束设计):

  1. 单向:服务端 → 客户端。客户端要发数据必须另开普通请求(Agent 场景通常正好:发起是一次 POST,回传是一条 SSE);
  2. 只能是 GET,且浏览器 EventSource 不能自定义请求头 → 鉴权要用 Cookie、query 参数或一次性 ticket(第 08 章);
  3. HTTP/1.1 下同域最多 6 条连接(HTTP/2 下复用同一连接,限制解除)→ 多开几个 SSE 会挤占其它请求;
  4. 只能传文本(二进制要 base64)。

三、决策树

需要客户端高频向服务端发数据(如协作编辑、游戏)?
├─ 是 → WebSocket
└─ 否(服务端单向推为主)
   ├─ 数据是偶发的、允许秒级延迟?(如消息红点)
   │   └─ 用轮询或长轮询,别上长连接(长连接是持续成本)
   └─ 需要持续、低延迟的单向流(token 流、进度、通知)
       ├─ 环境里有没有会拦 WebSocket 的代理/网关? → 有 → SSE
       ├─ 需不需要浏览器自动重连?                 → 需要 → SSE
       ├─ 需不需要传二进制?                       → 需要 → WebSocket
       └─ 其余情况 → SSE(默认选择,成本最低)

默认选 SSE,只在明确需要双向或二进制时才上 WebSocket。这条建议的理由不是"SSE 更高级",而是它的失败模式更少、可观测性更好、与现有 HTTP 基建零摩擦。

四、成本对比:长连接不是免费的

一条长连接占用的资源(这是容量规划的基础,第 06 章会展开):

资源 每条连接的占用
服务端连接/文件描述符 1
后端 worker / 协程 1(异步下是协程,仍有栈与状态开销)
nginx 连接 1(上游 + 下游各一条)
内存 几 KB ~ 几十 KB(缓冲、上下文)
数据库连接(如果连接期间持有) 可能是 1 ——这是最危险的

最后一行是关键:如果 SSE 处理函数里持有一个数据库事务或连接,1 万条长连接 = 1 万个数据库连接,系统会在几分钟内崩溃。这也是第 03 章要强调的"推数据时不要持有连接"。

五、与业务形态的匹配

业务形态 推荐 理由
大模型 token 流 SSE 天然单向、需要断线续播、文本
Agent 中间过程(思考/工具调用) SSE + 结构化事件 多类型事件,event: 字段天然支持
后台任务进度 SSE(短生命周期)或轮询 任务短的话轮询更简单
通知/红点 轮询 或 SSE 量小就别上长连接
实时协作/白板 WebSocket 双向、高频、低延迟
音视频信令 WebSocket 双向且需要即时往返

六、什么时候不该用长连接

三条判断:

  1. 数据更新频率低于分钟级 → 轮询。长连接要持续付出心跳、连接、监控成本;
  2. 用户可能长时间挂在页面上但不需要实时数据 → 用可见性/活跃度判断,页面隐藏时断开(省资源);
  3. 一次性的结果 → 直接用普通请求 + 前端 loading,不要为了"看起来高级"上 SSE。

反模式:为了展示技术能力给所有接口套 SSE。长连接是一种负债,只有持续的数据流才值得为它付费。


动手:可观察结果

产出 判断标准
一份选型决策记录 对一个具体场景,用决策树给出结论并写明被否决方案的原因
四种通道的最小实现对比 同一个"进度推送"场景各写一份,对比代码量与失败模式
一次 WebSocket 被拦的验证 在有代理/网关的环境验证握手失败,确认 SSE 可穿透
连接成本测算 列出单条连接占用的 fd / 协程 / 内存,估算 1 万连接时的总量

完成标志:给你一个"要实时"的需求,你能在 5 分钟内说出用哪种通道、为什么,以及它在什么条件下会失效。

故障注入

注入方式 观察
用 HTTP/1.1 同时开 7 条 SSE 第 7 条是否阻塞,其它请求是否被饿死
在支持 WebSocket 的环境禁用协议升级 WS 握手是否失败,SSE 是否仍可用
把数据频率降到 5 分钟一次仍用 SSE 心跳与连接成本是否远超收益
SSE handler 里持有一个数据库事务 并发 100 条连接时数据库连接是否被耗尽
关掉心跳让连接空闲 5 分钟 负载均衡/代理是否主动断开

自测题

  1. SSE 相比 WebSocket 的三个工程优势是什么?分别对应什么失效场景?
  2. 浏览器的 EventSource 有哪三条限制?每条会如何约束你的设计?
  3. 什么情况下应该坚持用轮询而不是上长连接?给出两条判据。
  4. 一条 SSE 长连接会占用哪些资源?其中最危险的一项是什么?
  5. 你的项目里哪些场景适合 SSE、哪些不适合?各举一例并说明理由。

进入 keel 阅读