1.2 选型对比:WebSocket 与轮询的账
白板第一版图省事用了轮询:客户端每隔几百毫秒问一次服务器「有什么新笔」。上线没几天就暴露出致命问题——延迟被轮询间隔焊死、服务扛不住、带宽被空查询吃掉。本篇把两种方案的账算清楚,讲明白为什么实时协作必须上长连接,以及上了之后要补哪些功课。先把结论摆出来:白板这种「高频双向」场景,轮询的延迟下限和空查开销都喂不起,主流协作产品基本都落回 WebSocket。
两种同步模型的账
| 维度 | 轮询(Polling) | 长连接(WebSocket) |
|---|---|---|
| 延迟下限 | 被轮询间隔卡死,常见 300~1000ms | 事件驱动,常见 20~100ms |
| 服务端开销 | 每次完整 HTTP 请求,空查也计费 | 连接复用,空闲几乎零开销 |
| 带宽 | 大量「无更新」空响应 | 只在有事件时推 |
| 双向性 | 只能客户端主动问 | 服务端可主动推 |
| 横向扩展 | 无状态,随便加机器 | 有状态,需会话管理 |
| 稳定性 | 简单,断了重轮询即可 | 需心跳、断线重连 |
把这张表读一遍,结论很清楚:白板的延迟下限被轮询间隔焊死,而实时协作要的是「我画一笔,对端立刻收敛」,两个订单的差距就是能不能用的分界。轮询还能不能活?能,但只适合「低频、单向、对延迟不敏感」的通知场景,白板两条都不占。
轮询为什么扛不住白板
白板是高频事件:一个人一秒画好几笔,几十人房间就是每秒成百上千个事件。轮询把这些事件塞进 500ms 一次的批次里,延迟直接翻倍;更糟的是绝大多数轮询轮次是「空」的——没笔也问,带宽和 CPU 全烧在空查询上。房间越大、笔越密,空转越夸张,服务越堆机器越救不回来。 | 场景 | 轮询延迟 | 轮询开销 | |---|---|---| | 10 人房间、每秒 50 笔 | 300~500ms | 空查占比 70% 以上 | | 100 人房间、每秒 500 笔 | 500ms+ | 空查占比 80%+ | | 1000 人热房间 | 难以满足体验 | 带宽被打爆 |
这里要强调「空查占比」这个指标:白板大部分时刻是没笔的,轮询却按固定频率问,空查占比随房间规模往上走,这是轮询成本失控的根因,不是简单调小间隔能解的——间隔调小,延迟虽降,但请求数与开销同步翻倍。
长连接省在哪、贵在哪
WebSocket 把连接建一次,之后事件随到随推,延迟从「最多一个轮询周期」降到「几乎即时」,空查询彻底消失。代价是有状态:每个连接的服务端要记得「这条连接是谁、在哪个房间」,横向扩机器时不能随便把连接打到另一台,需要会话粘滞或共享状态。再就是连接会断,必须做心跳保活和断线重连,否则状态就漂了。 | 代价 | 要补的功课 | 对应章节 | |---|---|---| | 有状态 | 会话管理、多节点共享 | 2.2 | | 会断 | 心跳保活、断线重连 | 2.1 / 2.4 | | 扩机器 | 会话粘滞 / 共享状态 | 5.1 |
「省」和「贵」是一对:省的是空转与延迟,贵的是状态与保活。选型本质是接受「用复杂度换性能」。
折中方案:SSE 与长轮询
不是所有场景都值得上 WebSocket。SSE(服务端推文本流)够单向通知用,浏览器原生、走 80/443,但只能服务端到客户端单向;长轮询是轮询的改进版——服务端 hold 住请求直到有更新才回,延迟比短轮询低、比 WebSocket 简单,但仍是 HTTP 语义、双向性差。 | 方案 | 双向 | 延迟 | 复杂度 | 适用 | |---|---|---|---|---| | SSE | 单向下行 | 即时 | 低 | 通知、公告 | | 长轮询 | 弱双向 | 中 | 中 | 简单协作 | | WebSocket | 真双向 | 低 | 高 | 高频协作白板 |
白板要「画」和「撤」双向高频,SSE 的单向不够,长轮询的延迟和开销也比不上一根真长连接,所以这类协作产品基本都选 WebSocket。这一思路在 Node.js 云开发实战 里也反复强调:先明确「双向频率」再定协议,别为了省事上轮询。
先量化再选型:延迟与开销的估算口径
选型最容易吵成「信仰之争」,根本原因是没把口径定好。建议在选型前先写下两个数字:可接受的 P95 端到端延迟(协作白板常见口径 100ms 以内,超过 200ms 用户能明显感知「卡」),以及预估的事件频率(每人每秒笔数 × 在线人数)。这两个数字一对,轮询立刻出局:500ms 的轮询间隔连 P95 延迟这关都过不了,更别提 events 事件频率上去之后的空转开销。把口径写进选型文档,团队吵架时就有了共同语言,而不是「我觉得 WebSocket 好」对「我觉得轮询简单」。
| 口径 | 轮询表现 | WebSocket 表现 |
|---|---|---|
| P95 端到端延迟 | ≥ 轮询间隔,常见 300ms+ | 20~100ms,事件驱动 |
| 空查占比 | 60% 以上,随规模恶化 | 接近 0 |
| 峰值请求数 | 频率 × 间隔倒数,线性涨 | 仅事件数,无空转 |
先量化再选型还有个好处:如果算出来 P95 口径其实只要 500ms(比如内部通知板),那确实轮询就够了,不必过度设计。选型不是越先进越好,而是口径对得上。
从轮询迁移的三个过渡手段
如果线上已经是轮询,不必一把切到 WebSocket。常见的过渡路径有三步:先把高频画笔通道单独走 WebSocket,通知、列表等低频接口保留轮询;再逐步把服务端主动推送类接口(房间人数变更、公告)迁到 SSE 或同一连接;最后统一到一根长连接。这样做的好处是风险可控,每一步都能独立回滚,不至于「一切就全崩」。
| 阶段 | 通道安排 | 风险 |
|---|---|---|
| 第一步 | 画笔走 WebSocket,其余保留轮询 | 两套协议并存,维护成本略增 |
| 第二步 | 推送类接口迁入长连接 | 注意降级与回退策略 |
| 第三步 | 统一长连接,下掉轮询 | 需配套心跳与重连到位 |
迁移期间最容易翻车的是降级策略缺失:长连接建不上时,客户端要有兜底(退回轮询或提示重试),否则弱网用户直接白屏。这一思路在 Node.js 云开发实战 的长连接迁移章节有完整对照,感兴趣的可以对着看。
选型决策的最终对照
把前面几张表压缩成一张最终决策表,选型时直接对着勾。一张表胜过十次会议:凡是「高频双向」打勾、且「P95 口径 < 200ms」打勾的场景,WebSocket 基本是唯一解;两条任意一条不满足,都可以退回 SSE 或轮询省复杂度。
| 决策项 | 勾选条件 | 指向方案 |
|---|---|---|
| 双向高频 | 画笔 + 撤销 双向 | WebSocket |
| 延迟敏感 | P95 < 200ms | WebSocket |
| 仅单向通知 | 服务端推即可 | SSE |
| 低频查询 | 分钟级更新 | 轮询 |
| 弱网兜底 | 建连失败需可用 | 长连接 + 降级轮询 |
勾完表再看成本:WebSocket 的成本是心跳、重连、会话共享三件事,但这些在本书后续章节都有现成方案,不是「不可控的复杂度」,而是「可计划的工程量」。真正不可控的是轮询的空转成本——它会随房间规模悄无声息地膨胀,等你看到账单和告警时已经晚了。
踩坑提示
上了 WebSocket 不等于万事大吉。断网、弱网、网关超时都会让连接静默断开,客户端若不做心跳检测,UI 会一直以为「在线」其实已经断了,画面卡死不更新却不报错——这是协作产品最常见的「假在线」陷阱,下一节的心跳保活就是为它设的。再一个坑:重连策略要用指数退避,千人房间同时断网同时重试会瞬间打爆服务,把雪崩留给下一轮处理。还有个隐性坑:协议升级被中间代理截断,某些公司内网代理不支持 Upgrade,表现是「手机 4G 正常、公司 WiFi 建连失败」,排查时别只盯服务端。
小结
白板这种高频双向场景,轮询的延迟下限和空查开销都喂不起;长连接用「有状态、要保活」换来「低延迟、零空转、可主动推」,是协作产品的主流选择。选型的另一面是把会话管理、心跳、重连这三件事排上日程,缺一样都会塌。把「省」和「贵」两笔账一起算,再对着折中方案逐条排除,选型才算真的定下来,不是拍脑袋。
延伸阅读
- 连接管理与心跳保活,见下一节 2.1。
- 长连接服务落地,见 Node.js 云开发实战。
- 服务治理与会话粘滞,见 Go 微服务实战。
- 实时音视频场景对照,见 基于 TRTC 的在线课堂。
