移动网络切换、代理空闲超时、浏览器休眠和滚动发布都会让 WebSocket 断开。若客户端只在 close 后等待一秒重新连接,它仍不知道哪些消息已经到达、哪些命令被服务器执行,也无法阻止成千上万客户端同时回连。

WebSocket 只提供一条有序双向连接,不提供跨连接的会话连续性。可靠实时功能必须在应用协议中定义游标、补放、幂等、心跳和背压。

把连接和会话分开

连接是一次可丢弃的传输;会话是用户订阅、权限和进度的逻辑状态。建立连接后先交换协议消息,而不是立即推送业务事件:

{"type":"hello","protocol":3,"resume_token":"…","last_seen":1842}
{"type":"welcome","session_id":"s_9","resumed":true,"head":1850}

resume_token 应短期有效、不可伪造,并绑定主体、租户和订阅范围。它不是永久访问令牌;每次握手仍要验证当前权限。协议版本让服务端拒绝不兼容客户端或选择明确的降级路径。

每条事件拥有连续位置

为每个可恢复流分配单调递增的序列号。客户端只有在事件通过校验并应用到本地状态后,才持久化 last_applied;收到重复序列号时忽略,发现跳号时暂停增量应用并请求补放。

client last_applied = 1842
server replay        = 1843..1850
next live event      = 1851

服务端保留有期限或有容量上限的重放日志。若游标太旧、日志已压缩或权限范围变化,返回 resync_required,客户端获取快照而不是猜测缺失事件。快照必须带 watermark,例如“状态包含至 1850”;客户端先安装快照,再应用 1851 之后的事件,避免快照与实时流之间留下缝隙。

序列号作用域必须明确;时间戳可能重复、乱序或受时钟偏差影响,不能替代序列号。

命令确认不等于恰好一次

客户端可能发送命令后断线,服务器已经提交副作用,但确认还没到达。重连后盲目重发会重复下单或发消息;完全不重发又可能丢操作。每个有副作用的命令携带由客户端生成的 command_id:

{"type":"send_message","command_id":"c_7f…","room":"r1","text":"hello"}

服务器在与业务写入一致的持久边界内记录该 ID 和结果。重复命令返回原结果,不再次执行。客户端把未确认命令保存在有界 outbox 中,收到包含同一 ID 的确认后删除。这里实现的是“至少一次传输 + 幂等效果”,不能把它宣传成网络层恰好一次。

输入状态等瞬时更新可以丢弃或合并;支付、聊天发送等操作需要确认。协议应声明每类消息的可靠性。

重连必须退避并带抖动

RFC 6455 提醒异常关闭后立即重连可能造成拒绝服务,并建议首次随机延迟、后续使用逐步增长的退避。采用有上限的指数退避和 full jitter,例如在 0..min(cap, base*2^attempt) 中随机取值;稳定连接一段时间后才重置 attempt。

网络恢复事件只能触发“可以尝试”,不能绕过退避。后台页面、电量受限设备和服务器下发的重试提示也应调整节奏。鉴权或策略拒绝不能无限重试:先刷新凭据或等待用户操作;协议错误则停止并升级客户端。

心跳检测的是可用路径

TCP 连接存在不代表代理与对端仍能传递应用消息。服务器可以使用 RFC 6455 Ping/Pong;浏览器 WebSocket API 不暴露控制帧,因此需要可观测 RTT 或业务活性时,可定义应用层 ping/pong。心跳间隔应短于已知空闲超时,同时加入抖动,避免所有连接同秒发送。

背压必须是协议的一部分

浏览器的 bufferedAmount 表示调用 send() 后尚未交给网络的应用数据字节。设低、高水位:超过高水位时暂停非关键生产者,合并可替代更新,拒绝新的大消息;降到低水位再恢复。无限数组排队只会把慢网络变成内存崩溃。

服务端也要限制每连接的入站速率、消息大小、解析时间和出站队列。慢消费者达到上限时,优先丢弃声明可丢的事件;可靠流则关闭连接并让客户端用游标恢复,不能拖慢整个广播分片。

握手仍是安全边界

浏览器发起 WebSocket 时会带 Origin,服务端必须校验允许列表,尤其使用 Cookie 认证时,以防跨站页面借用户会话建立连接。不要把长期 bearer token 放进会被代理和日志记录的查询串;可以先通过 HTTPS 获取一次性短期 ticket,再用于握手。连接建立后权限变化仍需生效,敏感订阅要重新授权或主动断开。

部署时先停止新连接,向现有连接发送迁移提示并执行 Close 握手,到 deadline 后强制关闭。

测试应模拟发送前后断线、确认丢失、重复和跳号、重放过期、快照期间新增事件、网络抖动、慢消费者、休眠、权限撤销和集中重连。用虚拟时钟验证退避分布。

监控连接数、握手失败原因、连接寿命、关闭码、重连 attempt、恢复成功率、重放条数、快照回退、序列缺口、outbox 年龄和队列字节。只有能看到恢复路径,实时系统才不只是“在线时看起来正常”。

可靠 WebSocket 的核心不是永不断线,而是断线后知道从哪里继续、什么可以重发、何时必须重建状态,以及慢连接应如何降级。把传输视为暂时的,把会话进度和副作用做成可恢复协议,重连才会从随机补丁变成可验证能力。