本文代碼 github、gitee、npm
在web應(yīng)用中,WebSocket是很常用的技術(shù)。通過瀏覽器的WebSocket構(gòu)造函數(shù)就可以建立一個WebSocket連接。但當(dāng)需要應(yīng)用在具體項(xiàng)目中時,幾乎都會進(jìn)行心跳檢測。
設(shè)置心跳檢測,一是讓通訊雙方確認(rèn)對方依舊活躍,二是瀏覽器端及時檢測當(dāng)前網(wǎng)絡(luò)線路可用性,保證消息推送的及時性。
你可能會想,WebSocket那么簡陋的嗎,居然不能自己判斷連接狀態(tài)?在了解前先來回顧一下計(jì)算機(jī)網(wǎng)絡(luò)知識。
相關(guān)的網(wǎng)絡(luò)知識
TCP/IP協(xié)議族四層結(jié)構(gòu):
-
應(yīng)用層:決定了向用戶提供應(yīng)用服務(wù)時通信的活動。HTTP、FTP、WebSocket都是應(yīng)用層協(xié)議
-
(TCP)傳輸控制層:控制網(wǎng)絡(luò)中兩臺主機(jī)的數(shù)據(jù)傳輸:將應(yīng)用層數(shù)據(jù)(有必要時對應(yīng)用層報(bào)文分段,例如一個完整的HTTP報(bào)文進(jìn)行分段)發(fā)送到目標(biāo)主機(jī)的特定端口的應(yīng)用程序。給每個數(shù)據(jù)標(biāo)記源端口、目標(biāo)端口、分段后的序號。
-
(IP)網(wǎng)絡(luò)層:將IP地址映射為目標(biāo)主機(jī)的MAC地址,然后將TCP數(shù)據(jù)包(有必要時對數(shù)據(jù)分片)加入源IP、目標(biāo)IP等信息后經(jīng)過鏈路層扔到網(wǎng)絡(luò)上讓其找到目標(biāo)主機(jī)。
-
鏈路層:為IP網(wǎng)絡(luò)層進(jìn)行發(fā)送、接收數(shù)據(jù)報(bào)。將二進(jìn)制數(shù)據(jù)包與在網(wǎng)線傳輸?shù)木W(wǎng)絡(luò)電信號進(jìn)行相互轉(zhuǎn)換。
TCP是可靠的連接,握手建立連接后,發(fā)送方每發(fā)送一個TCP報(bào)文(對應(yīng)用層報(bào)文分段后形成多個TCP報(bào)文),都會期望對方在指定時間里返回已收到的確認(rèn)消息,如果超時沒有回應(yīng),會重復(fù)發(fā)送,確保所有TCP報(bào)文可以到達(dá)對方,被對方按順序拼接成應(yīng)用層需要的完整報(bào)文。
WebSocket協(xié)議支持在TCP 上層引入 TLS 層,建立加密通信。
WebSocket與HTTP的異同:
-
WebSocket和HTTP一樣是應(yīng)用層協(xié)議,在傳輸層使用了TCP協(xié)議,都是可靠的連接。WebSocket在建立連接時,可以使用已有的HTTP的GET請求進(jìn)行握手:客戶端在請求頭中將WebSocket協(xié)議版本等信息發(fā)生到服務(wù)器,服務(wù)器同意的話,會響應(yīng)一個101的狀態(tài)碼。就是說一次HTTP請求和響應(yīng),即可輕松轉(zhuǎn)換協(xié)議到WebSocket。
-
WebSocket可以互相發(fā)起請求。當(dāng)有新消息時,服務(wù)器主動通知客戶端,無需客戶端主動向服務(wù)器詢問??蛻舳艘部梢韵蚝蠖税l(fā)送消息。而HTTP中請求只能由客戶端發(fā)起。
-
WebSocket是HTML5的內(nèi)容,HTTP則是超文本傳輸協(xié)議,比HTML5誕生更早。
-
在應(yīng)用層,WebSocket的每個報(bào)文(在WebSocket中叫數(shù)據(jù)幀)會比HTTP報(bào)文(必須包含請求行、請求頭、請求數(shù)據(jù))更輕量。
- WebSocket每個數(shù)據(jù)幀只有固定的頭信息,不會有cookie等或者自定義的頭信息。建立通訊后是一對一的,不需要攜帶驗(yàn)證信息。使用HTTP握手時,的握手請求會自動攜帶cookie。
- WebSocket在應(yīng)用層就會將大的數(shù)據(jù)進(jìn)行分拆,而HTTP不會。
WebSocket與與WebRTC的異同:
- WebRTC是一種通訊技術(shù),由谷歌發(fā)起,被廣大瀏覽器實(shí)現(xiàn)。用來建立瀏覽器和瀏覽器間的通訊,如視頻通話等。而WebSocket是一種經(jīng)過抽象的協(xié)議,可以實(shí)現(xiàn)為通訊技術(shù)。用來建立瀏覽器和服務(wù)器間的通訊。
協(xié)議中的心跳檢測機(jī)制
從網(wǎng)上檢索的答案,WebSocket大概有兩種從協(xié)議角度出發(fā)的,檢測對方存活的方式:
-
WebSocket只是一個應(yīng)用層協(xié)議規(guī)范,其傳輸層是TCP,而TCP為長連接提供KeepAlive機(jī)制,可以定時發(fā)送心跳報(bào)文確認(rèn)對方的存活,但一般是服務(wù)器端使用。因?yàn)槭荰CP傳輸控制層的機(jī)制,具體的實(shí)現(xiàn)要看操作系統(tǒng),也就是說應(yīng)用層接收到的連接狀態(tài)是操作系統(tǒng)通知的,不同操作系統(tǒng)的資源調(diào)度是不一樣的,例如何時發(fā)送探測報(bào)文(不包含有效數(shù)據(jù)的TCP報(bào)文)檢測對方的存活,頻率是多久,在不同的系統(tǒng)配置下存在差異??赡苁?小時進(jìn)行一次心跳檢測,或許更短。如果連續(xù)沒有收到對方的應(yīng)答包,才會通知應(yīng)用層已經(jīng)斷開連接。這就帶來了不確定性。同時也意味著其它依賴該機(jī)制的應(yīng)用層協(xié)議也會被影響。也就是說要利用這個過程進(jìn)行檢測,客戶端要修改操作系統(tǒng)的TCP配置才行,在瀏覽器環(huán)境顯然不行。參考1、參考2
-
WebSocket協(xié)議也有自身的?;顧C(jī)制,但需要通訊雙方的實(shí)現(xiàn)。WebSocket通訊的數(shù)據(jù)幀會有一個4位的OPCODE,標(biāo)記當(dāng)前傳輸?shù)臄?shù)據(jù)幀類型,例如:0x8表示關(guān)閉幀、0x9表示ping幀、0xA表示pong幀、0x1普通文本數(shù)據(jù)幀等。www.rfc-editor.org
- 關(guān)閉數(shù)據(jù)幀,在任意一方要關(guān)閉通道時,發(fā)送給對方。例如瀏覽器的WebSocket實(shí)例調(diào)用close時,就會發(fā)送一個OPCODE為連接關(guān)閉的數(shù)據(jù)幀給服務(wù)器端,服務(wù)器端接收到后同樣需要返回一個關(guān)閉數(shù)據(jù)幀,然后關(guān)閉底層的TCP連接。
- ping數(shù)據(jù)幀,用于發(fā)送方詢問對方是否存活,也就是心跳檢測包。目前只有后端可以控制ping數(shù)據(jù)幀的發(fā)送。但瀏覽器端的WebSocket實(shí)例上沒有對應(yīng)的api可用。
- pong數(shù)據(jù)幀,當(dāng)WebSocket通訊一方接收到對方發(fā)送的ping數(shù)據(jù)幀后,需要及時回復(fù)一個內(nèi)容一致,且OPCODE標(biāo)記為pong的數(shù)據(jù)幀,告訴對方我還在。但目前回復(fù)pong是瀏覽器的自動行為,意味著不同瀏覽器會有差異。而且在js中沒有相關(guān)api可以控制。
綜上所述,探測對方存活的方式都是服務(wù)器主動進(jìn)行心跳檢測。瀏覽器并沒有提供相關(guān)能力。為了能夠在瀏覽器端實(shí)時探測后端的存活,或者說連接依舊可用,只能自己實(shí)現(xiàn)心跳檢測。
瀏覽器端心跳檢測的必要性
首先我們先了解一下,目前的瀏覽器端的WebSocket何時會自動關(guān)閉WebSocket,并觸發(fā)close事件呢?
- 握手時的WebSocket地址不可用。
- 其它未知錯誤。
- 正常連接狀態(tài)下,接收到服務(wù)器端的關(guān)閉幀就會觸發(fā)關(guān)閉回調(diào)。
也就是說建立正常連接后,中途瀏覽器端斷網(wǎng)了,或者服務(wù)器沒有發(fā)送關(guān)閉幀就關(guān)了連接,總之就是在連接無法再使用的情況下,瀏覽器沒有接收到關(guān)閉幀,瀏覽器則會長時間保持連接狀態(tài)。此時業(yè)務(wù)代碼不去主動探測的話,是無法感知的。
另外通訊雙方保持連接意味著需要長時間占用對方的資源。對于服務(wù)器端來說資源是非常寶貴的。長時間不活躍的連接,可能會被服務(wù)器應(yīng)用層框架"優(yōu)化"釋放掉。
前端實(shí)現(xiàn)心跳檢測
實(shí)例化一個WebSocket:
function connectWS() {
const WS = new WebSocket("ws://127.0.0.1:7070/ws/?name=greaclar");
// WebSocket實(shí)例上的事件
// 當(dāng)連接成功打開
WS.addEventListener('open', () => {
console.log('ws連接成功');
});
// 監(jiān)聽后端的推送消息
WS.addEventListener('message', (event) => {
console.log('ws收到消息', event.data);
});
// 監(jiān)聽后端的關(guān)閉消息,如果發(fā)送意外錯誤,這里也會觸發(fā)
WS.addEventListener('close', () => {
console.log('ws連接關(guān)閉');
});
// 監(jiān)聽WS的意外錯誤消息
WS.addEventListener('error', (error) => {
console.log('ws出錯', error);
});
return WS;
}
let WS = connectWS();
心跳檢測需要用到的實(shí)例方法:
// 發(fā)送消息,用來發(fā)送心跳包
WS.send('hello');
// 關(guān)閉連接,當(dāng)發(fā)送心跳包不響應(yīng),需要重連時,最好先關(guān)閉
WS.close();
定義發(fā)送心跳包的邏輯:
準(zhǔn)備
- 申請一個變量heartbeatStatus,記錄當(dāng)前心跳檢測狀態(tài),有三個狀態(tài):等待中,已收到應(yīng)答、超時。
- 監(jiān)聽WS實(shí)例的message事件,監(jiān)聽到就將heartbeatStatus改為:已收到應(yīng)答。
- 監(jiān)聽WS實(shí)例的open事件,打開后啟動心跳檢測。
檢測
-
啟動一個定時器A。
-
定時器A執(zhí)行,1.修改當(dāng)前狀態(tài)heartbeatStatus為等待中;2.發(fā)送心跳包;3.啟動一個定時器B。
- 發(fā)送心跳包后,后端需要立刻推送一個內(nèi)容一樣的心跳應(yīng)答包給前端,觸發(fā)前端WS實(shí)例的message事件,繼而將heartbeatStatus改為已收到應(yīng)答。
-
定時器B執(zhí)行,檢測當(dāng)前heartbeatStatus狀態(tài):
-
如果是已收到應(yīng)答,證明定時器A執(zhí)行后,服務(wù)器可以及時響應(yīng)數(shù)據(jù)。繼續(xù)啟動定時器A,然后不斷循環(huán)。
-
如果是等待中,證明連接出現(xiàn)問題了,走關(guān)閉或者檢測流程。
-
let WS = connectWS();
let heartbeatStatus = 'waiting';
WS.addEventListener('open', () => {
// 啟動成功后開啟心跳檢測
startHeartbeat()
})
WS.addEventListener('message', (event) => {
const { data } = event;
console.log('心跳應(yīng)答了,要把狀態(tài)改為已收到應(yīng)答', data);
if (data === '"heartbeat"') {
heartbeatStatus = 'received';
}
})
function startHeartbeat() {
setTimeout(() => {
// 將狀態(tài)改為等待應(yīng)答,并發(fā)送心跳包
heartbeatStatus = 'waiting';
WS.send('heartbeat');
// 啟動定時任務(wù)來檢測剛才服務(wù)器有沒有應(yīng)答
waitHeartbeat();
}, 1500)
}
function waitHeartbeat() {
setTimeout(() => {
console.log('檢測服務(wù)器有沒有應(yīng)答過心跳包,當(dāng)前狀態(tài)', heartbeatStatus);
if (heartbeatStatus === 'waiting') {
// 心跳應(yīng)答超時
WS.close();
} else {
// 啟動下一輪心跳檢測
startHeartbeat();
}
}, 1500)
}
優(yōu)化心跳檢測
心跳檢測異常,但close事件沒有觸發(fā),大概率是雙方之間的網(wǎng)絡(luò)線路不佳,如果立馬進(jìn)行重連,會擠兌更多的網(wǎng)絡(luò)資源,重連的失敗概率更大,也可能阻塞用戶的其它操作。
但也不排除確實(shí)是連接的問題,如服務(wù)器宕機(jī)、意外重啟,同時沒有告知瀏覽器需要把舊連接關(guān)閉。
所以一發(fā)生心跳不應(yīng)答,個人推薦的做法是,發(fā)生延遲后,提醒用戶網(wǎng)絡(luò)異常正在修復(fù)中,讓用戶有個心理準(zhǔn)備。然后多發(fā)一兩個心跳包,連續(xù)不應(yīng)答再提示用戶掉線了,是否重連。如果中途正常了,就不需要重連,用戶體驗(yàn)更好,對服務(wù)器的壓力也更小。文章來源:http://www.zghlxwxcb.cn/news/detail-780375.html
// 以上代碼需要修改的地方
// 添加一個變量來記錄連續(xù)不應(yīng)答次數(shù)
let retryCount = 0;
WS.addEventListener('message', (event) => {
const { data } = event;
console.log('心跳應(yīng)答了,要把狀態(tài)改為已收到應(yīng)答', data);
if (data === '"heartbeat"') {
// 復(fù)位連續(xù)不應(yīng)答次數(shù)
retryCount = 0;
heartbeatStatus = 'received';
}
})
// 在等待應(yīng)答的函數(shù)中添加重試的邏輯
function waitHeartbeat() {
setTimeout(() => {
// 心跳應(yīng)答正常,啟動下一輪心跳檢測
if (heartbeatStatus === 'received') {
return startHeartbeat();
}
// 更新超時次數(shù)
retryCount ++;
// 心跳應(yīng)答超時,但沒有連續(xù)超過三次
if (retryCount < 3) {
alert('ws線路異常,正在檢測中。')
return startHeartbeat();
}
// 超時次數(shù)超過三次
WS.close();
}, 1500)
}
最后,為了方便大家共同進(jìn)步,本文已經(jīng)把相關(guān)的邏輯封裝為一個類,并且在npm中可下載玩一下,也已經(jīng)開源到github上。文章來源地址http://www.zghlxwxcb.cn/news/detail-780375.html
到了這里,關(guān)于為什么WebSocket需要前端心跳檢測,有沒有原生的檢測機(jī)制?的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!