前言
最近,在使用 WebSocket(WS)连接时,我们遇到了频繁断开连接的问题,单个用户每天会出现数百次。尽管使用 socket.io 的自动重连功能可以让我们在断开连接后迅速恢复连接,但并不能保证每次重连都能成功接收 WS 消息。因此,我们进行了多次调查和测试。最终,我们确定了问题的根本原因:浏览器的节能机制,它无意中成为了问题的罪魁祸首。
浏览器节能机制概述
浏览器的节能机制正成为前端开发者越来越重要的考虑因素。这些机制尤其会影响定时器的精度,直接影响前端应用的用户体验,在某些情况下甚至会影响可用性。
为了降低功耗并延长电池寿命,现代浏览器引入了节能机制。这些机制包括但不限于减少空闲标签的 CPU 使用率、降低后台 JavaScript 的执行频率以及限制定时器的精度。虽然这些措施显著提高了设备效率,但也给前端开发带来了一些挑战。
频繁断开连接分析
在查看 socket.io 服务器配置中的 pingTimeout 和 pingInterval 参数时,我们发现异常的 WS 心跳导致了重连。以下是详细解释: pingInterval(心跳间隔)
- 默认值:25000(25 秒)
- 此值用于心跳机制,用于定期检查服务器与客户端之间的连接是否仍然存活。服务器每隔 pingInterval 毫秒发送一个 ping 数据包,如果客户端在 pingTimeout 毫秒内没有回复 pong,服务器就认为连接已关闭。同样,如果客户端在 pingInterval + pingTimeout 毫秒内没有收到服务器的 ping 数据包,那么客户端也认为连接已关闭。在这两种情况下,断开连接的原因都将是:ping 超时。示例代码如下:
socket.on("disconnect", (reason) => {
console.log(reason); // "ping timeout"
});
使用像 1000(每秒一次心跳)这样的小值会给服务器带来一些负载,如果有数千个连接的客户端,这可能会变得明显。
pingTimeout(超时时间)
- 默认值:20000(20 秒)
- 见上文。
- 注意事项:使用较小的值意味着暂时无响应的服务器可能会触发大量客户端重连。相反,使用较大的值意味着断开的连接需要更长时间才能被检测到(如果 pingInterval + pingTimeout 大于 60 秒,在 React Native 中可能会收到警告)。
在 WS 连接中,服务器和客户端都必须保持恒定的心跳。如果任何一方停止,只要满足以下任一条件,连接就会自动断开:服务器发送 ping,如果客户端在 pingTimeout 期间内没有回复 pong,服务器认为连接已关闭;同样,如果客户端在 pingInterval + pingTimeout 期间内没有收到服务器的 ping,客户端也认为连接已关闭。
我们发现,在较高版本的 socket.io 中,服务器会定期发起 ping。相比之下,在 socket.io 2.X 中,内置的心跳机制是由客户端发起的。当浏览器在后台运行时,即使设置了每秒触发一次的定时器,由于节能机制,它每分钟只能触发一次,超过了 pingInterval + pingTimeout 的设置。因此,日志显示每分钟都会有一次重连。
解决方案
1. 升级 socket.io 到最新版本:最新版本(4.x)由服务器发起心跳,避免了浏览器节能机制对定时器的影响。
2. 自定义 WS 心跳事件:为了尽量减少对现有业务逻辑的影响,另一种解决方案是使用自定义心跳事件。服务器定期发送 custom - ping。注意:断开连接时销毁定时器。虽然 socket.io 有内置心跳(2.x 中由客户端发起,4.x 中由服务器发起),但自定义心跳有助于保持数据交换,防止自动断开和重连。
客户端代码:
io.on('custom - ping', function () {
io.emit('custom - pong', Date.now())
})
服务器代码:
io.on('connection', (socket) => {
console.log('New client connected');
// 发送自定义 ping 消息
const pingInterval = setInterval(() => {
socket.emit('custom - ping', Date.now());
}, 10000); // 每 10 秒
// 监听自定义 pong 消息
socket.on('custom - pong', (data) => {
console.log('Pong received:', data);
});
socket.on('disconnect', () => {
clearInterval(pingInterval);
console.log('Client disconnected');
});
});
注意:断开连接时销毁定时器。
3. 使用 setTimeout:使用 setTimeout 时要谨慎,因为它仍然可能失去精度。示例代码如下:
// 这个 setTimeout 会失去精度
let _cacheTs = Date.now()
const _setTimeoutFn = () => {
console.log('setTimeout :>> ', Date.now() - _cacheTs);
_cacheTs = Date.now()
setTimeout(() => {
_setTimeoutFn()
}, 5000)
}
_setTimeoutFn()
在 setTimeout 中,执行函数栈由浏览器监控,类似于 setInterval,在后台运行时其精度会降低。但是,以下方法可以避免节能机制的限制: - 客户端代码:
// 监听服务器发送的 custom - pong 事件
socket.on('custom - pong', onHeart)
const onHeart = () => {
if (timer) {
clearTimeout(pingTime.current)
}
timer = window.setTimeout(() => {
socket.emit('custom - ping', Date.now())
}, 5000)
}
// 服务器代码
socket.on('custom - ping', ()=>{
socket.emit('custom - pong', Date.now())
})
4. 使用 Web Workers:在 Web Worker 线程中启动定时器不受浏览器节能机制的影响。
结论
随着浏览器技术的发展,节能机制将变得更加精细,这给前端开发带来了新的挑战。理解并适应这些变化,并采用正确的策略来解决相关问题,对于开发高质量的前端应用至关重要。上述方法可以有效减轻或解决浏览器节能机制导致的定时器精度降低的影响,从而提升用户体验。