WebSocket 实战:连接状态、心跳、重连和消息协议
本文目录
WebSocket 适合双向实时交互,例如协作编辑、客服会话和需要客户端即时控制的任务。若只需服务器推送进度,SSE 往往更简单。真正难的不是 new WebSocket,而是连接丢失、消息重放、过载、授权过期和用户快速离开页面时的清理。
明确状态机和所有权
将连接状态建模为 idle、connecting、open、reconnecting、closing、closed 与 failed。每次连接有 generation/requestId;旧连接迟到的 close/message 事件不能覆盖新连接状态。创建 socket 的组件、共享 store 或页面 service 必须明确所有权,路由卸载时只关闭自己拥有的连接。
心跳与重连
浏览器 WebSocket API 不提供发送协议层 Ping 帧的接口,常见做法是在应用协议中发送 ping/pong。服务端应在超时后关闭僵死连接。页面隐藏时可降低非关键心跳频率,但恢复可见时要检查连接状态;navigator.onLine 只是提示,不能证明服务器可达。
重连用有上限的指数退避与 full jitter,认证失败、权限撤销或明确的业务关闭不要无限重试。连接抖动要避免短时间频繁拨号;多次失败后展示可操作的恢复状态并允许手动重试。
function retryDelay(attempt, baseMs = 500, maxMs = 30000) {
const ceiling = Math.min(maxMs, baseMs * (2 ** attempt));
return Math.random() * ceiling;
}
消息契约、顺序与幂等
为每条消息定义 schemaVersion、type、messageId、sequence、sentAt 和 payload。服务端逐消息鉴权并验证 schema、长度与速率。关键写操作使用 idempotency key;客户端收到 ack 前不要假定持久化成功。重连后按游标恢复,检测 sequence 缺口并请求补发或重新拉取快照,不能无声丢弃。
文本消息渲染为纯文本或经允许列表净化的 Markdown,不能直接插入 HTML。记录连接建立、关闭码、延迟、重试次数、ack 延迟和队列长度;不要记录对话正文、认证凭证和完整 URL。
背压与客户端队列
经典 WebSocket API 没有自动 backpressure。高频服务端消息可能堆进浏览器内存或挤占主线程。根据业务选择合并最新状态、丢弃过期遥测、批处理或服务端限速;聊天等不能丢的消息要有队列上限、落盘/恢复边界和用户可见失败反馈。
发送端观察 bufferedAmount,达到阈值时暂停非关键发送或拒绝新任务。服务端也需要慢消费者保护、每连接内存上限和广播扇出限制。WebSocketStream 可用性有限且非跨浏览器通用方案,WebTransport 能力更广但复杂度和支持范围不同,先按浏览器矩阵验证再选。
关闭码与恢复策略
根据浏览器 CloseEvent.code、wasClean 和应用协议错误分类恢复策略。网络临时断开可以退避重连;鉴权过期先刷新会话或提示重新登录;权限拒绝和业务主动关闭不能循环重试;服务端滚动发布要提供合理 drain 窗口。不要只看 close event 就把服务端异常与用户主动关闭混为一谈。
重连后的第一步是重新鉴权、协商协议版本和恢复订阅游标。若服务端无法补发遗漏消息,客户端重新请求权威快照,再继续接收增量,避免把不完整状态显示成最新。客户端队列区分可丢失状态和必须确认的命令;后者要展示发送中、已确认或失败状态。
生命周期与安全
连接必须使用 wss;握手校验 Origin、会话与账号状态,注意浏览器 cookie 会让跨站 WebSocket 建立风险上升。每个消息都做对象级授权,不能只在握手时验证角色。令牌不要放 URL query,因为 URL 可能进入日志。登出、封禁、权限变更和切账号时关闭 socket、清队列、取消订阅并拒绝旧 generation 的事件。
测试弱网、断网、服务端滚动重启、代理 idle timeout、半开连接、重复/乱序消息、慢消费者、token 过期和后台标签页冻结。对页面 bfcache 和移动浏览器前后台切换做真机验证;连接不要在使用结束后一直悬挂。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。