做即时通讯、实时行情或者在线客服类的小程序时,WebSocket几乎是绕不开的方案。但真机上跑一段时间就会发现,连接经常莫名其妙就断了:切后台被系统回收、网络从WiFi切到4G、运营商NAT超时,都会导致连接失效。如果只依赖微信提供的onSocketClose回调来判断断线,很多时候根本收不到这个事件,连接就已经是"假死"状态了。这篇文章就来聊聊如何用心跳机制检测连接状态,配合断线重连和消息队列,打造一个稳定的WebSocket通信层。

为什么必须有心跳机制
WebSocket协议本身是基于TCP的长连接,理论上只要双方不断开,连接就一直在。但现实环境远比协议复杂。最典型的问题是NAT超时:运营商或者路由器维护的地址转换表有生命周期,一般几分钟到几十分钟不等,如果这条链路上长时间没有数据传输,NAT映射就会被删除,此时客户端这边看起来连接还在,但实际上发出去的数据包会被直接丢弃,服务端也收不到任何断开通知。
微信小程序提供了wx.connectSocket建立连接,通过wx.onSocketClose监听关闭事件。问题在于,假死状态下onSocketClose并不会触发,小程序层面依然认为连接正常。这时候唯一的办法就是客户端主动发探测包,也就是心跳包。心跳的本质是给连接定时"喂"一个小数据包,一来防止NAT表项过期,二来通过探测响应判断链路是否真的可用。
心跳间隔的设置需要权衡:太短会增加服务器负担和客户端耗电,太长又无法及时发现断线。实际项目中,客户端到服务端的心跳间隔一般设置在30秒到60秒之间,同时服务端也可以设置自己的空闲超时时间(比如90秒没有收到任何数据就主动踢掉连接),两者配合既能保活又能兜底清理僵尸连接。
心跳检测的具体实现
实现思路并不复杂:连接成功后启动一个定时器,每隔固定时间向服务端发送一个心跳包(通常是约定好的JSON,比如类型为ping的消息),同时开启一个超时定时器。如果在这个超时时间内收到了服务端的pong回应,说明链路正常,重置计数;如果连续若干次没收到回应,就认定连接已经断开,主动关闭socket并进入重连流程。
先封装一个基础的socket管理类,把连接、心跳、重联的逻辑收拢到一起,避免散落在各个页面里:
class WsManager {
constructor(options) {
this.url = options.url;
this.heartbeatInterval = 30000; // 心跳间隔30秒
this.heartbeatTimeout = 10000; // 单次心跳超时10秒
this.maxReconnectTimes = 6; // 最大重连次数
this.reconnectTimes = 0;
this.pingTimer = null;
this.pongTimer = null;
this.alive = false;
this.manualClose = false;
}
connect() {
this.manualClose = false;
this.task = wx.connectSocket({ url: this.url });
this.task.onOpen(() => {
this.alive = true;
this.reconnectTimes = 0;
this.startHeartbeat();
this.flushQueue(); // 连接成功后补发离线消息
});
this.task.onMessage((res) => {
const data = JSON.parse(res.data);
if (data.type === 'pong') {
this.onPong(); // 收到心跳回应
} else {
this.onMessage && this.onMessage(data);
}
});
this.task.onClose(() => {
this.stopHeartbeat();
if (!this.manualClose) this.reconnect();
});
this.task.onError(() => {
this.stopHeartbeat();
if (!this.manualClose) this.reconnect();
});
}
startHeartbeat() {
this.pingTimer = setInterval(() => {
if (this.alive) {
this.alive = false; // 标记为待确认状态
this.send({ type: 'ping' });
this.pongTimer = setTimeout(() => {
if (!this.alive) {
// 超时未收到pong,判定连接假死
this.task.close({ code: 3000 });
}
}, this.heartbeatTimeout);
}
}, this.heartbeatInterval);
}
onPong() {
this.alive = true;
clearTimeout(this.pongTimer);
}
stopHeartbeat() {
clearInterval(this.pingTimer);
clearTimeout(this.pongTimer);
}
}这段代码里有一个关键细节:alive标志位的设计。发心跳前把它置为false,收到pong再置回true。如果下一次心跳触发时发现alive依然是false,说明上一个心跳根本没得到回应,此时不必再等,直接判定链路异常并主动close,触发onClose后自然进入重连流程。另外要注意,主动关闭时应该设置一个非正常的状态码,方便服务端日志里区分是客户端主动退出还是心跳失败。
断线重连与指数退避
重连最忌讳的就是立即重试。如果断线是因为服务端重启或者网络抖动,客户端瞬间发起大量连接请求,会把还没恢复的服务直接打垮。标准的做法是指数退避:第一次1秒后重试,失败则2秒、4秒、8秒,依次翻倍,并设置上限(比如30秒)和最大重试次数。同时要监听wx.onNetworkStatusChange,在网络恢复时立即触发一次重连,用户体验会好很多。
reconnect() {
if (this.manualClose) return;
if (this.reconnectTimes >= this.maxReconnectTimes) {
this.onDead && this.onDead(); // 彻底失联,通知上层
return;
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectTimes), 30000);
this.reconnectTimes++;
setTimeout(() => {
this.connect();
}, delay);
}
// 监听网络恢复,立即重连
wx.onNetworkStatusChange((res) => {
if (res.isConnected && !this.alive) {
this.reconnectTimes = 0; // 网络恢复,重置计数
this.reconnect();
}
});还有一个容易踩的坑:小程序切后台后,WebSocket连接会被挂起甚至断开,回到前台时需要手动检查并重连。可以在App的onShow生命周期里判断连接状态,必要时重新建立连接。另外每次重连成功后,务必重新执行握手鉴权逻辑,因为服务端此时是一条全新连接,之前的登录态已经随旧连接一起销毁了。
本地消息队列保证消息不丢
解决了连接问题,还要解决消息可靠性。用户点发送的那一刻可能恰好处于断线状态,如果直接调用send,消息就丢了。解决办法是在本地维护一个待发送队列:所有消息先入队并持久化,再尝试发送;只有收到服务端的ACK确认后才从队列中移除。连接每次建立成功时,扫描队列把积压的消息按顺序补发出去。
send(data) {
return new Promise((resolve, reject) => {
const msg = { id: this.genId(), data, time: Date.now() };
this.queue.push(msg);
this.saveQueue(); // 写入本地缓存,防止小程序被杀
this.trySend(msg, resolve, reject);
});
}
trySend(msg, resolve, reject) {
if (!this.alive) {
reject(new Error('未连接,消息已入队等待重连后补发'));
return;
}
this.task.send({
data: JSON.stringify(msg),
success: () => resolve(msg),
fail: () => reject(new Error('发送失败'))
});
}
// 收到服务端ACK后清除对应消息
handleAck(msgId) {
this.queue = this.queue.filter(m => m.id !== msgId);
this.saveQueue();
}
// 重连成功后补发
flushQueue() {
this.queue.forEach(msg => this.trySend(msg));
}队列的持久化建议直接用wx.setStorageSync,这样即使小程序进程被杀,消息也不会丢。补发时要注意顺序和幂等:消息带上唯一id和序号,服务端需要做去重处理,否则重连瞬间可能收到重复消息。对于聊天类应用,还可以在补发前给消息标记"发送中"状态,界面上的小菊花转起来,用户感知会好很多。
总结
一个生产可用的WebSocket层,核心就是三件事:用心跳探测假死连接,用指数退避做温和重连,用持久化队列保证消息不丢。三者环环相扣,心跳发现问题,重连恢复通道,队列弥补断线窗口期的数据。把这套逻辑封装成独立模块,业务层只关心send和onMessage,无论做IM、协作还是实时推送,都能有一个稳定的地基。上线前记得在真机上多测弱网和切后台场景,这些问题在开发工具里是暴露不出来的。