WebSocket断线重连不能依赖onclose立即新建连接,因其常因中间设备静默断连而不触发;须用心跳探测(如20s发ping、3s未pong即判定失效)、清理资源、指数退避+抖动重连,并缓存send消息防丢失。

WebSocket断线重连不能靠 onclose 一触发就立刻 new WebSocket(),那样会炸服务器、耗用户电量、丢消息、还可能堆满定时器和事件监听器。
为什么 onclose 经常不触发?
中间设备(比如 Nginx、CDN、运营商 NAT)静默断连时,TCP 层没发 FIN 包,浏览器收不到关闭信号,onclose 根本不会执行。你看到的连接状态还是 WebSocket.OPEN,但实际发不出任何消息——这就是“假连”。
- 真实断连信号往往来自心跳超时,而不是
onclose -
readyState === WebSocket.CLOSED也不能当唯一依据:闪断后它可能卡在1(OPEN)长达数秒 - 必须主动探测:用业务层
{"type":"ping"}+ 超时判断,别依赖浏览器原生 Ping/Pong(JS API 不暴露发送接口)
怎么写一个不崩客户端的重连控制器?
核心是三件事:清理旧资源、加退避、防重复触发。下面这个 ReconnectController 是生产环境跑稳的最小可行结构:
class ReconnectController {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries || 5;
this.retryDelay = options.retryDelay || 2000;
this.currentRetry = 0;
this.isForcedClose = false;
this.timer = null;
this.socket = null;
this.pingInterval = null;
this.heartbeatTimeout = null;
}
<p>connect() {
// 清理上一次残留
this.clearTimers();
if (this.socket) {
this.socket.close();
this.socket.onmessage = null;
this.socket.onopen = null;
this.socket.onclose = null;
this.socket.onerror = null;
}</p><pre class="brush:php;toolbar:false;">this.socket = new WebSocket(this.url);
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = () => {
this.currentRetry = 0;
this.startHeartbeat();
};
this.socket.onclose = (event) => {
if (this.isForcedClose) return;
this.handleDisconnect(event);
};
this.socket.onerror = () => {
// onError 常比 onclose 更早触发,尤其在 DNS 失败、连接拒绝时
this.handleDisconnect();
};
}
startHeartbeat() { this.clearTimers(); this.pingInterval = setInterval(() => { if (this.socket?.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ type: 'ping' })); } }, 20000); // 心跳间隔必须小于所有中间链路最短空闲超时(如 Nginx proxy_read_timeout=60s → 这里设 20s)
this.heartbeatTimeout = setTimeout(() => {
if (this.socket?.readyState !== WebSocket.OPEN) {
this.handleDisconnect();
}
}, 3000); // 等 pong 响应超时设为 3 秒,避免单次抖动误判
}
handleDisconnect() { this.clearTimers(); if (this.currentRetry this.currentRetry 1000 + Math.random() * 1000, 30000 ); this.timer = setTimeout(() => { this.currentRetry++; this.connect(); }, delay); } else { // 触发失败回调,交由上层 UI 处理(比如弹“重连失败,请检查网络”) this.emit('reconnect_failed'); } }
clearTimers() { if (this.pingInterval) clearInterval(this.pingInterval); if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout); if (this.timer) clearTimeout(this.timer); }
close() { this.isForcedClose = true; if (this.socket) this.socket.close(); this.clearTimers(); } }
- 每次
connect()前必须调用clearTimers()和显式close(),否则 setInterval 和事件监听器会越积越多 - 指数退避 + 抖动(
Math.random() * 1000)能有效防止惊群效应,尤其在服务端重启或大面积弱网时 - 心跳间隔选 20s 是因多数云 LB 默认空闲超时为 30–45s;设太短增加无谓流量,设太长会被中间件砍掉
send 消息断连期间怎么不丢?
业务层调用 send(data) 时,不能直接扔给 this.socket.send()。一旦连接断开或尚未建立,会抛 "Failed to execute 'send' on 'WebSocket': Still in CONNECTING state" 或静默失败。
- 必须维护一个待发队列:
this.sendQueue = [] - 所有
send()入队,只在onopen后批量清空;重连成功后也清空并重发 - 对关键消息(如光标位置、交易指令),需配合服务端 sequenceId 补偿机制,否则重连后无法知道“断了多久、漏了哪些”
- 注意内存:长期未恢复连接时,队列要设长度上限(比如最多缓存 100 条),超限则丢弃老消息或报错
真正难的不是写重连代码,而是让重连逻辑和业务消息生命周期解耦——比如光标移动、文档变更、交易确认这些操作,不该感知连接状态;它们只管往队列塞,剩下的交给控制器兜底。这点容易被忽略,但决定了系统后期能不能加功能、会不会出竞态。











