一文吃透 WebSocket 原理
共 9389字,需瀏覽 19分鐘
·
2024-07-31 12:14
大廠技術(shù) 高級前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
大家好,我是考拉??。
通常我們開發(fā)H5,都是基于HTTP協(xié)議,典型的 請求/響應(yīng) 模式。隨著web能力越來越豐富,更多的小伙伴有機(jī)會嘗試一些新的網(wǎng)絡(luò)協(xié)議,比如 WebSocket ,這在一些實(shí)時性要求比較高的場景出現(xiàn)頻率很高。今天帶來一篇關(guān)于 WebSocket 原理的文章,對于接觸過的同學(xué),可以看看自己是否已經(jīng)掌握了這些基礎(chǔ)知識,還不了解的同學(xué)也可以擴(kuò)展下知識面,畢竟知識面越廣,將來在項(xiàng)目里進(jìn)行技術(shù)選型、決策時候,才能更加科學(xué)、嚴(yán)謹(jǐn)。
下面是正文部分。
一、前言
踩著年末的尾巴,提前布局來年,為來年的工作做個好的鋪墊,所以就開始了面試歷程,因?yàn)轫?xiàng)目中使用到了 WebSocket ,面試官在深挖項(xiàng)目經(jīng)驗(yàn)的時候,也難免提到 WebSocket 相關(guān)的知識點(diǎn),因?yàn)橹安]有考慮這么深,所以,回答的還是有所欠缺,因此,趕緊趁熱再熟悉熟悉,也借此機(jī)會,整理出來供大家咀嚼,每個項(xiàng)目都有其值得挖掘的閃光點(diǎn),要用有愛的眼睛??去發(fā)現(xiàn)。
二、什么是WebSocket
WebSocket 是一種在單個TCP連接上進(jìn)行全雙工通信的協(xié)議。WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù)。
在 WebSocket API 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接, 并進(jìn)行雙向數(shù)據(jù)傳輸。(維基百科)
WebSocket本質(zhì)上一種計(jì)算機(jī)網(wǎng)絡(luò)應(yīng)用層的協(xié)議,用來彌補(bǔ)http協(xié)議在持久通信能力上的不足。
WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)。現(xiàn)在最新版本瀏覽器都已經(jīng)支持了。
它的最大特點(diǎn)就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話,屬于服務(wù)器推送技術(shù)的一種。
WebSocket 的其他特點(diǎn)包括:
(1)建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。
(2)與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務(wù)器。
(3)數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
(4)可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
(5)沒有同源限制,客戶端可以與任意服務(wù)器通信。
(6)協(xié)議標(biāo)識符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
ws://example.com:80/some/path
為什么需要 WebSocket?
我們已經(jīng)有了 HTTP 協(xié)議,為什么還需要另一個協(xié)議?它能帶來什么好處?
因?yàn)?HTTP 協(xié)議有一個缺陷:通信只能由客戶端發(fā)起,不具備服務(wù)器推送能力。
舉例來說,我們想了解查詢今天的實(shí)時數(shù)據(jù),只能是客戶端向服務(wù)器發(fā)出請求,服務(wù)器返回查詢結(jié)果。HTTP 協(xié)議做不到服務(wù)器主動向客戶端推送信息。
這種單向請求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。我們只能使用'輪詢':每隔一段時候,就發(fā)出一個詢問,了解服務(wù)器有沒有新的信息。最典型的場景就是聊天室。輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開)。
在 WebSocket 協(xié)議出現(xiàn)以前,創(chuàng)建一個和服務(wù)端進(jìn)雙通道通信的 web 應(yīng)用,需要依賴HTTP協(xié)議,進(jìn)行不停的輪詢,這會導(dǎo)致一些問題:
服務(wù)端被迫維持來自每個客戶端的大量不同的連接
大量的輪詢請求會造成高開銷,比如會帶上多余的header,造成了無用的數(shù)據(jù)傳輸。
http協(xié)議本身是沒有持久通信能力的,但是我們在實(shí)際的應(yīng)用中,是很需要這種能力的,所以,為了解決這些問題,WebSocket協(xié)議由此而生,于2011年被IETF定為標(biāo)準(zhǔn)RFC6455,并被RFC7936所補(bǔ)充規(guī)范。
并且在HTML5標(biāo)準(zhǔn)中增加了有關(guān)WebSocket協(xié)議的相關(guān)api,所以只要實(shí)現(xiàn)了HTML5標(biāo)準(zhǔn)的客戶端,就可以與支持WebSocket協(xié)議的服務(wù)器進(jìn)行全雙工的持久通信了。
WebSocket 與 HTTP 的區(qū)別
WebSocket 與 HTTP的關(guān)系圖:
相同點(diǎn): 都是一樣基于TCP的,都是可靠性傳輸協(xié)議。都是應(yīng)用層協(xié)議。
聯(lián)系: WebSocket在建立握手時,數(shù)據(jù)是通過HTTP傳輸?shù)摹5墙⒅螅谡嬲齻鬏敃r候是不需要HTTP協(xié)議的。
下面一張圖說明了 HTTP 與 WebSocket 的主要區(qū)別:
1、WebSocket是雙向通信協(xié)議,模擬Socket協(xié)議,可以雙向發(fā)送或接受信息,而HTTP是單向的;2、WebSocket是需要瀏覽器和服務(wù)器握手進(jìn)行建立連接的,而http是瀏覽器發(fā)起向服務(wù)器的連接。
注意:雖然HTTP/2也具備服務(wù)器推送功能,但HTTP/2 只能推送靜態(tài)資源,無法推送指定的信息。
三、WebSocket協(xié)議的原理
與http協(xié)議一樣,WebSocket協(xié)議也需要通過已建立的TCP連接來傳輸數(shù)據(jù)。具體實(shí)現(xiàn)上是通過http協(xié)議建立通道,然后在此基礎(chǔ)上用真正的WebSocket協(xié)議進(jìn)行通信,所以WebSocket協(xié)議和http協(xié)議是有一定的交叉關(guān)系的。
首先,WebSocket 是一個持久化的協(xié)議,相對于 HTTP 這種非持久的協(xié)議來說。簡單的舉個例子吧,用目前應(yīng)用比較廣泛的 PHP 生命周期來解釋。
HTTP 的生命周期通過 Request 來界定,也就是一個 Request 一個 Response ,那么在 HTTP1.0 中,這次 HTTP 請求就結(jié)束了。
在 HTTP1.1 中進(jìn)行了改進(jìn),使得有一個 keep-alive,也就是說,在一個 HTTP 連接中,可以發(fā)送多個 Request,接收多個 Response。但是請記住 Request = Response, 在 HTTP 中永遠(yuǎn)是這樣,也就是說一個 Request 只能有一個 Response。而且這個 Response 也是被動的,不能主動發(fā)起。
首先 WebSocket 是基于 HTTP 協(xié)議的,或者說借用了 HTTP 協(xié)議來完成一部分握手。
首先我們來看個典型的 WebSocket 握手
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
熟悉 HTTP 的童鞋可能發(fā)現(xiàn)了,這段類似 HTTP 協(xié)議的握手請求中,多了這么幾個東西。
Upgrade: websocket
Connection: Upgrade
這個就是 WebSocket 的核心了,告訴 Apache 、 Nginx 等服務(wù)器:注意啦,我發(fā)起的請求要用 WebSocket 協(xié)議,快點(diǎn)幫我找到對應(yīng)的助理處理~而不是那個老土的 HTTP。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
首先, Sec-WebSocket-Key 是一個 Base64 encode 的值,這個是瀏覽器隨機(jī)生成的,告訴服務(wù)器:泥煤,不要忽悠我,我要驗(yàn)證你是不是真的是 WebSocket 助理。
然后, Sec_WebSocket-Protocol 是一個用戶定義的字符串,用來區(qū)分同 URL 下,不同的服務(wù)所需要的協(xié)議。簡單理解:今晚我要服務(wù)A,別搞錯啦~
最后, Sec-WebSocket-Version 是告訴服務(wù)器所使用的 WebSocket Draft (協(xié)議版本),在最初的時候,WebSocket 協(xié)議還在 Draft 階段,各種奇奇怪怪的協(xié)議都有,而且還有很多期奇奇怪怪不同的東西,什么 Firefox 和 Chrome 用的不是一個版本之類的,當(dāng)初 WebSocket 協(xié)議太多可是一個大難題。。不過現(xiàn)在還好,已經(jīng)定下來啦~大家都使用同一個版本:服務(wù)員,我要的是13歲的噢→_→
然后服務(wù)器會返回下列東西,表示已經(jīng)接受到請求, 成功建立 WebSocket 啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
這里開始就是 HTTP 最后負(fù)責(zé)的區(qū)域了,告訴客戶,我已經(jīng)成功切換協(xié)議啦~
Upgrade: websocket
Connection: Upgrade
依然是固定的,告訴客戶端即將升級的是 WebSocket 協(xié)議,而不是 mozillasocket,lurnarsocket 或者 shitsocket。
然后, Sec-WebSocket-Accept 這個則是經(jīng)過服務(wù)器確認(rèn),并且加密過后的 Sec-WebSocket-Key 。服務(wù)器:好啦好啦,知道啦,給你看我的 ID CARD 來證明行了吧。
后面的, Sec-WebSocket-Protocol 則是表示最終使用的協(xié)議。
至此,HTTP 已經(jīng)完成它所有工作了,接下來就是完全按照 WebSocket 協(xié)議進(jìn)行了。
總結(jié),WebSocket連接的過程是:
首先,客戶端發(fā)起http請求,經(jīng)過3次握手后,建立起TCP連接;http請求里存放WebSocket支持的版本號等信息,如:Upgrade、Connection、WebSocket-Version等;
然后,服務(wù)器收到客戶端的握手請求后,同樣采用HTTP協(xié)議回饋數(shù)據(jù);
最后,客戶端收到連接成功的消息后,開始借助于TCP傳輸信道進(jìn)行全雙工通信。
四、Websocket的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
WebSocket協(xié)議一旦建議后,互相溝通所消耗的請求頭是很小的
服務(wù)器可以向客戶端推送消息了
缺點(diǎn):
少部分瀏覽器不支持,瀏覽器支持的程度與方式有區(qū)別(IE10)
五、WebSocket應(yīng)用場景
即時聊天通信
多玩家游戲
在線協(xié)同編輯/編輯
實(shí)時數(shù)據(jù)流的拉取與推送
體育/游戲?qū)崨r
實(shí)時地圖位置
即時
Web應(yīng)用程序:即時Web應(yīng)用程序使用一個Web套接字在客戶端顯示數(shù)據(jù),這些數(shù)據(jù)由后端服務(wù)器連續(xù)發(fā)送。在WebSocket中,數(shù)據(jù)被連續(xù)推送/傳輸?shù)揭呀?jīng)打開的同一連接中,這就是為什么WebSocket更快并提高了應(yīng)用程序性能的原因。例如在交易網(wǎng)站或比特幣交易中,這是最不穩(wěn)定的事情,它用于顯示價格波動,數(shù)據(jù)被后端服務(wù)器使用Web套接字通道連續(xù)推送到客戶端。游戲應(yīng)用程序:在游戲應(yīng)用程序中,你可能會注意到,服務(wù)器會持續(xù)接收數(shù)據(jù),而不會刷新用戶界面。屏幕上的用戶界面會自動刷新,而且不需要建立新的連接,因此在
WebSocket游戲應(yīng)用程序中非常有幫助。聊天應(yīng)用程序:聊天應(yīng)用程序僅使用
WebSocket建立一次連接,便能在訂閱戶之間交換,發(fā)布和廣播消息。它重復(fù)使用相同的WebSocket連接,用于發(fā)送和接收消息以及一對一的消息傳輸。
不能使用WebSocket的場景
如果我們需要通過網(wǎng)絡(luò)傳輸?shù)娜魏螌?shí)時更新或連續(xù)數(shù)據(jù)流,則可以使用WebSocket。如果我們要獲取舊數(shù)據(jù),或者只想獲取一次數(shù)據(jù)供應(yīng)用程序使用,則應(yīng)該使用HTTP協(xié)議,不需要很頻繁或僅獲取一次的數(shù)據(jù)可以通過簡單的HTTP請求查詢,因此在這種情況下最好不要使用WebSocket。
注意:如果僅加載一次數(shù)據(jù),則RESTful Web服務(wù)足以從服務(wù)器獲取數(shù)據(jù)。
六、websocket 斷線重連
心跳就是客戶端定時的給服務(wù)端發(fā)送消息,證明客戶端是在線的, 如果超過一定的時間沒有發(fā)送則就是離線了。
如何判斷在線離線?
當(dāng)客戶端第一次發(fā)送請求至服務(wù)端時會攜帶唯一標(biāo)識、以及時間戳,服務(wù)端到db或者緩存去查詢改請求的唯一標(biāo)識,如果不存在就存入db或者緩存中,
第二次客戶端定時再次發(fā)送請求依舊攜帶唯一標(biāo)識、以及時間戳,服務(wù)端到db或者緩存去查詢改請求的唯一標(biāo)識,如果存在就把上次的時間戳拿取出來,使用當(dāng)前時間戳減去上次的時間,
得出的毫秒秒數(shù)判斷是否大于指定的時間,若小于的話就是在線,否則就是離線;
如何解決斷線問題
通過查閱資料了解到 nginx 代理的 websocket 轉(zhuǎn)發(fā),無消息連接會出現(xiàn)超時斷開問題。網(wǎng)上資料提到解決方案兩種,一種是修改nginx配置信息,第二種是websocket發(fā)送心跳包。
下面就來總結(jié)一下本次項(xiàng)目實(shí)踐中解決的websocket的斷線 和 重連 這兩個問題的解決方案。
主動觸發(fā)包括主動斷開連接,客戶端主動發(fā)送消息給后端
主動斷開連接
ws.close();
主動斷開連接,根據(jù)需要使用,基本很少用到。
主動發(fā)送消息
ws.send('hello world');
針對websocket斷線我們來分析一下,
斷線的可能原因1:websocket超時沒有消息自動斷開連接,應(yīng)對措施:
這時候我們就需要知道服務(wù)端設(shè)置的超時時長是多少,在小于超時時間內(nèi)發(fā)送心跳包,有2中方案:一種是客戶端主動發(fā)送上行心跳包,另一種方案是服務(wù)端主動發(fā)送下行心跳包。
下面主要講一下客戶端也就是前端如何實(shí)現(xiàn)心跳包:
首先了解一下心跳包機(jī)制
跳包之所以叫心跳包是因?yàn)椋核裥奶粯用扛艄潭〞r間發(fā)一次,以此來告訴服務(wù)器,這個客戶端還活著。事實(shí)上這是為了保持長連接,至于這個包的內(nèi)容,是沒有什么特別規(guī)定的,不過一般都是很小的包,或者只包含包頭的一個空包。
在TCP的機(jī)制里面,本身是存在有心跳包的機(jī)制的,也就是TCP的選項(xiàng):SO_KEEPALIVE。系統(tǒng)默認(rèn)是設(shè)置的2小時的心跳頻率。但是它檢查不到機(jī)器斷電、網(wǎng)線拔出、防火墻這些斷線。而且邏輯層處理斷線可能也不是那么好處理。一般,如果只是用于保活還是可以的。
心跳包一般來說都是在邏輯層發(fā)送空的echo包來實(shí)現(xiàn)的。
下一個定時器,在一定時間間隔下發(fā)送一個空包給客戶端,然后客戶端反饋一個同樣的空包回來,服務(wù)器如果在一定時間內(nèi)收不到客戶端發(fā)送過來的反饋包,那就只有認(rèn)定說掉線了。在長連接下,有可能很長一段時間都沒有數(shù)據(jù)往來。理論上說,這個連接是一直保持連接的,但是實(shí)際情況中,如果中間節(jié)點(diǎn)出現(xiàn)什么故障是難以知道的。更要命的是,有的節(jié)點(diǎn)(防火墻)會自動把一定時間之內(nèi)沒有數(shù)據(jù)交互的連接給斷掉。在這個時候,就需要我們的心跳包了,用于維持長連接,保活。
心跳檢測步驟:
// 前端解決方案:心跳檢測
var heartCheck = {
timeout: 30000, //30秒發(fā)一次心跳
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
//這里發(fā)送一個心跳,后端收到后,返回一個心跳消息,
//onmessage拿到返回的心跳就說明連接正常
ws.send('ping');
console.log('ping!')
self.serverTimeoutObj = setTimeout(function(){//如果超過一定時間還沒重置,說明后端主動斷開了
ws.close(); //如果onclose會執(zhí)行reconnect,我們執(zhí)行ws.close()就行了.如果直接執(zhí)行reconnect 會觸發(fā)onclose導(dǎo)致重連兩次
}, self.timeout);
}, this.timeout);
}
}客戶端每隔一個時間間隔發(fā)生一個探測包給服務(wù)器
客戶端發(fā)包時啟動一個超時定時器
服務(wù)器端接收到檢測包,應(yīng)該回應(yīng)一個包
如果客戶機(jī)收到服務(wù)器的應(yīng)答包,則說明服務(wù)器正常,刪除超時定時器
如果客戶端的超時定時器超時,依然沒有收到應(yīng)答包,則說明服務(wù)器掛了
斷線的可能原因2:websocket異常包括服務(wù)端出現(xiàn)中斷,交互切屏等等客戶端異常中斷等等
當(dāng)若服務(wù)端宕機(jī)了,客戶端怎么做、服務(wù)端再次上線時怎么做?
客戶端則需要斷開連接,通過onclose 關(guān)閉連接,服務(wù)端再次上線時則需要清除之間存的數(shù)據(jù),若不清除 則會造成只要請求到服務(wù)端的都會被視為離線。
針對這種異常的中斷解決方案就是處理重連,下面我們給出的重連方案是使用js庫處理:引入reconnecting-websocket.min.js,ws建立鏈接方法使用js庫api方法:
var ws = new ReconnectingWebSocket(url);
// 斷線重連:
reconnectSocket(){
if ('ws' in window) {
ws = new ReconnectingWebSocket(url);
} else if ('MozWebSocket' in window) {
ws = new MozWebSocket(url);
} else {
ws = new SockJS(url);
}
}斷網(wǎng)監(jiān)測支持使用js庫:offline.min.js
onLineCheck(){
Offline.check();
console.log(Offline.state,'---Offline.state');
console.log(this.socketStatus,'---this.socketStatus');
if(!this.socketStatus){
console.log('網(wǎng)絡(luò)連接已斷開!');
if(Offline.state === 'up' && websocket.reconnectAttempts > websocket.maxReconnectInterval){
window.location.reload();
}
reconnectSocket();
}else{
console.log('網(wǎng)絡(luò)連接成功!');
websocket.send('heartBeat');
}
}
// 使用:在websocket斷開鏈接時調(diào)用網(wǎng)絡(luò)中斷監(jiān)測
websocket.onclose => () {
onLineCheck();
};以上方案,只是拋磚引玉,如果大家有更好的解決方案歡迎評論區(qū)分享交流。
七、總結(jié)
WebSocket 是為了在 web 應(yīng)用上進(jìn)行雙通道通信而產(chǎn)生的協(xié)議,相比于輪詢HTTP請求的方式,WebSocket 有節(jié)省服務(wù)器資源,效率高等優(yōu)點(diǎn)。
WebSocket 中的掩碼是為了防止早期版本中存在中間緩存污染攻擊等問題而設(shè)置的,客戶端向服務(wù)端發(fā)送數(shù)據(jù)需要掩碼,服務(wù)端向客戶端發(fā)送數(shù)據(jù)不需要掩碼。
WebSocket 中 Sec-WebSocket-Key 的生成算法是拼接服務(wù)端和客戶端生成的字符串,進(jìn)行SHA1哈希算法,再用base64編碼。
WebSocket 協(xié)議握手是依靠 HTTP 協(xié)議的,依靠于 HTTP 響應(yīng)101進(jìn)行協(xié)議升級轉(zhuǎn)換。
原文地址:https://juejin.cn/post/7020964728386093093
最后
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點(diǎn)贊、在看” 支持一波??
