序言
你踩過嗎?瀏覽器節(jié)能機(jī)制導(dǎo)致Websocket斷連的坑~~~
近期,在使用WebSocket(WS)
連接時遇到了頻繁斷連的問題,這種情況在單個用戶上每天發(fā)生數(shù)百次。盡管利用了socket.io
的自動重連機(jī)制能夠在斷連后迅速恢復(fù)連接,但這并不保證每一次重連都能成功接收WS
消息。因此,我們進(jìn)行了一些的排查和測試工作。
最終發(fā)現(xiàn)問題的根本原因:正是瀏覽器的節(jié)能機(jī)制,不經(jīng)意間成為了這一問題的幕后黑手。
瀏覽器節(jié)能機(jī)制簡介
瀏覽器的節(jié)能機(jī)制逐漸成為前端開發(fā)者需要關(guān)注的問題。特別是這些節(jié)能機(jī)制可能會對定時器的精度產(chǎn)生影響,這直接關(guān)系到前端應(yīng)用的用戶體驗,在某些場景下甚至影響到用戶的使用。
為了減少電能消耗,提高電池續(xù)航能力,現(xiàn)代瀏覽器都引入了節(jié)能機(jī)制。這些機(jī)制包括但不限于降低空閑標(biāo)簽頁的CPU
使用率、減少后臺JavaScript
的執(zhí)行頻率、限制定時器的精確度等。雖然這些措施顯著提高了設(shè)備的能效,但也給前端開發(fā)帶來了一些挑戰(zhàn)。
WS頻繁斷連原因分析
查閱socket.io官網(wǎng)服務(wù)端配置的pingTimeout
和pingInterval
兩個參數(shù)發(fā)現(xiàn)WS心跳異常時會導(dǎo)致重連,具體說明:
WS連接中服務(wù)端和客戶端兩端必須一直保持心跳。如果有一端停止,則滿足如下條件之一就會自動斷連:
看文檔發(fā)現(xiàn)其實高版本的socket.io是由服務(wù)端定時發(fā)起ping。而在socket.io 2.X的版本中內(nèi)置的心跳機(jī)制是由客戶端定時發(fā)起。而瀏覽器在后臺運行時,即使你設(shè)置了一個每秒觸發(fā)的定時器,它也只能每分鐘觸發(fā)一次,超過了pingInterval + pingTimeout
設(shè)置的時間,最后看到的日志是很有規(guī)律的每分鐘重連一次。在之前寫的這篇文章中也有相關(guān)的介紹《掌握Web Workers:徹底解鎖前端多線程編程的潛力》
WS頻繁斷連解決方法
@升級socket.io到最新版本
上面的截圖其實就是最新版本(4.x)的,升級后由服務(wù)器定時發(fā)起心跳。在服務(wù)端定時運行,避開了瀏覽器節(jié)能機(jī)制對定時器的影響
@自定義WS心跳事件
為了減小直接升級對已有業(yè)務(wù)的影響,目前使用的也是這種方案:在服務(wù)端自定義心跳事件,定時發(fā)送心跳custom-ping
// 客戶端的CODE
io.on('custom-ping', function () {
io.emit('custom-pong', Date.now())
})
// 服務(wù)端CODE
io.on('connection', (socket) => {
console.log('New client connected');
// 發(fā)送自定義ping消息
const pingInterval = setInterval(() => {
socket.emit('custom-ping', Date.now());
}, 10000); // 每10秒發(fā)送一次
// 監(jiān)聽自定義pong消息
socket.on('custom-pong', (data) => {
console.log('Pong received:', data);
});
socket.on('disconnect', () => {
clearInterval(pingInterval);
console.log('Client disconnected');
});
});
注意:斷連時一定要銷毀定時器
其實,socket.io是有內(nèi)置心跳的(2.x版本客戶端定時發(fā)起,4.x由服務(wù)端定時發(fā)起),自定義心跳的意義主要在于保持?jǐn)?shù)據(jù)交換,在這個時間間隔內(nèi)保持?jǐn)?shù)據(jù)交換,socket就不會自動中斷重連。
@使用setTimeout
這里要注意使用setTimeout的姿勢,如果是直接這樣使用、依然會有精度問題。
setTimeout丟失精度的情況:
// 以下setTimeout仍然會丟失精度
let _cacheTs = Date.now()
const _setTimeoutFn = () => {
console.log('setTimeout :>> ', Date.now() - _cacheTs);
_cacheTs = Date.now()
setTimeout(() => {
_setTimeoutFn()
}, 5000)
}
_setTimeoutFn()
在setTimeout里面去執(zhí)行一個函數(shù)棧會被瀏覽器監(jiān)控到,會認(rèn)為和setInterval一樣,其在后臺運行時會降低其定時精度。 但如果這樣可以避開節(jié)能機(jī)制的限制:
setTimeout不丟失精度的情況:
// 客戶端CODE
// 監(jiān)聽服務(wù)端發(fā)送的custom-pong事件
socket.on('custom-pong', onHeart)
const onHeart = () => {
if (timer) {
clearTimeout(pingTime.current)
}
timer = window.setTimeout(() => {
socket.emit('custom-ping', Date.now())
}, 5000)
}
// 服務(wù)端CODE
socket.on('custom-ping', ()=>{
socket.emit('custom-pong', Date.now())
})
@使用Web-Workers
在Web-Workers線程內(nèi)發(fā)起定時不受瀏覽器節(jié)能機(jī)制的限制,相關(guān)示例在這篇文章里也有介紹《掌握Web Workers:徹底解鎖前端多線程編程的潛力》
@頁面保活(實測無效)
在后臺運行時也保持瀏覽器的活躍,用得最多的方式是在頁面隱藏一個循環(huán)播放的音頻 或者 使用nosleep.js
const noSleepInstance = new NoSleep();
document.addEventListener('click', function enableNoSleep() {
document.removeEventListener('click', enableNoSleep, false);
noSleepInstance.enable();
}, false);
實測,使用這種方式時,瀏覽器在后臺運行仍然存在定時器精度降低的問題。
小結(jié)
WS頻繁斷連的原因:
使用了低版本(2.x)的socket.io
在客戶端每5秒定時發(fā)送 心跳
瀏覽器后臺運行時觸發(fā)節(jié)能機(jī)制限制了定時器的精度,由每5秒變成了實際的每分鐘執(zhí)行一次
每分鐘執(zhí)行一次遠(yuǎn)大于socket.io設(shè)置的pingTimeout時間
WS斷開連接
socket.io內(nèi)置的重連機(jī)制,立即重連成功
查看日志發(fā)現(xiàn)每分鐘重連一次。
在實際排查中,是從第七步倒退排查發(fā)現(xiàn)是瀏覽器節(jié)能機(jī)制所引起的問題。。。
總結(jié)
作者:tager
鏈接:https://juejin.cn/post/7362576319928008755
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。