前段时间做一个实时通知功能,用 WebSocket 推送,本地联调一切正常,一到线上就出怪事:连接对象显示还是 OPEN,却半天收不到消息;网络闪断后不会自动恢复,要刷新页面才行;重连太快还会建出多条重复连接。这些都是长连接的典型问题:没有心跳检测假死、没有重连退避、没有消息补偿。补齐之后连接才真正稳下来。这篇把配置过程记下来。
一、先分清:连接断开和连接假死不是一回事
| 状态 | 现象 | 检测方式 |
|---|---|---|
| 真断开 | close/error 事件触发 | 监听事件即可 |
| 假死 | readyState 仍是 OPEN,但收不到数据 | 只能靠心跳超时发现 |
| 重复连接 | 存在多条连接、消息收多份 | 重连前先清理旧连接 |
NAT 超时、运营商回收、弱网切换都可能让链路悄悄断掉而不触发 close,所以必须靠应用层心跳主动探活。
二、心跳机制:定时发 ping,超时没回就判定断线
心跳配置:
1. 连接成功后启动心跳定时器,每 25 秒发一个 ping
(间隔要小于 NAT 会话老化时间,常见老化约 60 秒,
取 20-30 秒留余量)
2. 每发一个 ping 重置等待计时器,
若 10 秒内没收到 pong,判定假死
3. 收到任何消息(含 pong)都刷新"最后活跃时间"
4. 判定假死后主动 close 当前连接,进入重连流程
5. 页面切到后台可用更小心跳,回到前台立即探一次
踩坑提醒:心跳间隔不是越短越好。间隔过短,连接数一多,心跳包本身就是一笔不小的流量和服务端压力;只要保证小于中间网络设备的会话老化时间即可,25 秒发、10 秒等是比较稳的组合。
三、断线重连:指数退避 + 上限 + 主动清理
重连不能写死固定间隔立刻连,否则服务端抖动时大量客户端同时重连会形成"重连风暴"。
重连状态机:
IDLE -> CONNECTING -> OPEN(心跳启动)
OPEN --超时/断开--> RECONNECTING
重连策略:
1. 指数退避:第 n 次重连等待
min(1000 * 2^n, 30000),即 1s、2s、4s…
最长 30 秒,并叠加随机抖动避免齐步走
2. 设置最大重连次数或持续上限,
超过后提示用户检查网络、提供手动重连
3. 每次重连前先清理:
旧 ws.close()、清掉心跳和等待定时器,
保证全局只有一条连接
4. 监听到 open 后重置退避计数,重新启动心跳
5. 页面从后台回前台时,若距上次活跃超时,
不等定时器直接触发一次重连判断
四、重连后的消息补偿
断线期间服务端可能已经产生了消息,单纯把连接建回来还会漏消息,需要按序号补偿。
消息补偿流程:
1. 每条服务端消息带递增 seq,客户端记录 lastSeq
2. 重连成功后,握手报文带上 lastSeq
3. 服务端推送 lastSeq 之后客户端错过的消息
4. 客户端按 seq 去重、排序后再渲染,
防止补偿消息和实时消息重复
5. 错过太多(超过缓存窗口)时,
降级为拉一次全量最新状态
关键是去重:补偿消息和断线前已收到的消息可能有交集,按 seq 用集合去重,保证同一条消息只渲染一次。
五、踩坑清单(这 5 个都实际踩过)
- 只监听 close 事件不做心跳,链路假死时 readyState 一直是 OPEN、消息却收不到,加 ping/pong 超时判定后能及时发现
- 心跳设成 5 秒一次,上千连接后心跳流量明显,调到 25 秒、小于 NAT 老化时间后压力下降且不掉线
- 重连写成立即重连,服务端抖动时形成重连风暴,改指数退避加随机抖动后平滑
- 重连前没关旧连接,弱网切换时建出好几条 ws、同一条通知弹多次,重连前统一清理保证单连接
- 重连后不补偿,断线期间的消息全部丢失,握手带 lastSeq 拉错过消息并按 seq 去重后不再漏
这个实时通知通道最后部署在乔拓云的轻应用环境里,前端连接管理和推送服务在同一套工程内维护,我主要负责心跳探活、指数退避重连和按序号消息补偿这几块,弱网和切后台场景下连接都能自动恢复且不丢消息。
复盘要点
- 假死只能靠应用层心跳发现,心跳间隔小于 NAT 老化时间,发 ping、等 pong、超时即断
- 重连用指数退避加随机抖动、设上限,重连前彻底清理旧连接和定时器
- 重连后按消息序号补偿并去重,避免断线丢消息和重复渲染
以上是个人实践记录,各平台具体功能以官方实时信息为准。
开放问题:你们做 WebSocket 长连接时,心跳间隔一般设多少?消息补偿是服务端推送离线消息,还是客户端主动拉接口补,哪种更省心?