心跳检测需与连接状态、业务流量、重连策略深度耦合:必须在ws.readystate === websocket.open时发送原生ping帧,及时清理定时器,结合业务消息重置超时,并采用指数退避+随机抖动重连,避免竞态与重连风暴。

心跳检测不是“加个定时器发个ping”就能完事的,它必须和连接状态、业务流量、重连策略耦合起来;否则你看到的只是控制台里反复刷屏的 WebSocket is already in CLOSING or CLOSED state 错误。
为什么 ws.sendPing() 会直接报错?
因为 sendPing() 是底层 API,只在连接处于 OPEN 状态时可用。但真实场景中,你可能在 onclose 回调里还没来得及清理定时器,就又触发了重连逻辑——此时旧 WebSocket 实例已进入 CLOSING 或 CLOSED 状态,sendPing() 就会抛出异常。
- 务必在发送前检查
ws.readyState === WebSocket.OPEN,而不是只依赖ws.isOpen()(某些封装库的该方法不严谨) - 不要在
onclose中立即调用ws.sendPing()或任何发送操作 - 所有定时任务(
setInterval、setTimeout)必须在ws.close()后显式clearTimeout/clearInterval
心跳包内容该用 Ping 帧还是业务 JSON?
优先用原生 Ping 帧(操作码 0x9),不是发 {"type":"ping"} 这类文本消息。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
-
ws.sendPing()发送的是二进制控制帧,服务端收到后自动回Pong(操作码0xA),无需业务层解析 - 发 JSON 心跳需要服务端额外解析+响应逻辑,容易漏处理或延迟响应,导致误判超时
- 部分中间件(如 Nginx、Cloudflare)只识别标准 Ping/Pong 帧做连接保活,对业务心跳无感知
- 若服务端不支持原生 Ping/Pong(如某些 Spring WebSocket 配置未开启),再退而求其次用业务心跳,但必须确保服务端收到后同步调用
session.sendMessage(new PongMessage())
如何避免重连风暴和无效重试?
别用固定间隔重连(比如死守 5000ms),网络抖动时所有客户端会在同一毫秒涌向服务端。
- 采用指数退避:第 1 次重连延迟
1000ms,第 2 次2000ms,第 3 次4000ms……上限建议设为30000ms - 加入随机抖动(jitter):每次延迟乘以
0.8 ~ 1.2的随机因子,打散重连时间点 - 限制最大重试次数(如
5次),超过后暂停重连并上报监控,防止无限循环消耗资源 - 手动关闭(如用户点击“断开”)需标记
isManualClose = true,避免触发自动重连
业务消息能替代心跳吗?
不能完全替代,但可以作为心跳重置信号——这是最容易被忽略的优化点。
- 只要收到任意业务消息(
onmessage),就说明链路仍通,应立即重置心跳超时计时器 - 服务端也应如此:收到客户消息时,更新该连接的最后活跃时间(
lastHeartbeatMap.put(customerId, System.currentTimeMillis())) - 这能显著降低心跳包数量(尤其在高频率聊天场景),但前提是客户端和服务端都实现该逻辑,缺一不可
- 注意:纯心跳超时(无业务消息)仍需独立判断,不能只依赖业务流
真正难的不是写对一行 ws.sendPing(),而是让心跳、重连、业务收发三者状态始终对齐——稍有疏漏,就会出现“连接明明开着,但心跳计时器还在跑”或者“重连刚建好,上一个定时器又发了一次 ping”这类竞态问题。所有定时器必须与 WebSocket 实例生命周期严格绑定,且每个状态变更点(open/close/error)都要做清理和重置。










