【W(wǎng)eb技術(shù)】一文吃透 WebSocket 原理

一.前言
踩著年末的尾巴,提前布局來年,為來年的工作做個(gè)好的鋪墊,所以就開始了面試歷程,因?yàn)轫?xiàng)目中使用到了 WebSocket ,面試官在深挖項(xiàng)目經(jīng)驗(yàn)的時(shí)候,也難免提到 WebSocket 相關(guān)的知識(shí)點(diǎn),因?yàn)橹安]有考慮這么深,所以,回答的還是有所欠缺,因此,趕緊趁熱再熟悉熟悉,也借此機(jī)會(huì),整理出來供大家咀嚼,每個(gè)項(xiàng)目都有其值得挖掘的閃光點(diǎn),要用有愛的眼睛??去發(fā)現(xiàn)。
二.什么是 WebSocket
WebSocket 是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,允許服務(wù)端主動(dòng)向客戶端推送數(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年成為國(guó)際標(biāo)準(zhǔn)?,F(xiàn)在最新版本瀏覽器都已經(jīng)支持了。
它的最大特點(diǎn)就是,服務(wù)器可以主動(dòng)向客戶端推送信息,客戶端也可以主動(dòng)向服務(wù)器發(fā)送信息,是真正的雙向平等對(duì)話,屬于服務(wù)器推送技術(shù)的一種。
WebSocket 的其他特點(diǎn)包括:
(1)建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。 (2)與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過各種 HTTP 代理服務(wù)器。 (3)數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。 (4)可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。 (5)沒有同源限制,客戶端可以與任意服務(wù)器通信。 (6)協(xié)議標(biāo)識(shí)符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
ws://example.com:80/some/path
為什么需要 WebSocket?
我們已經(jīng)有了 HTTP 協(xié)議,為什么還需要另一個(gè)協(xié)議?它能帶來什么好處?
因?yàn)?HTTP 協(xié)議有一個(gè)缺陷:通信只能由客戶端發(fā)起,不具備服務(wù)器推送能力。
舉例來說,我們想了解查詢今天的實(shí)時(shí)數(shù)據(jù),只能是客戶端向服務(wù)器發(fā)出請(qǐng)求,服務(wù)器返回查詢結(jié)果。HTTP 協(xié)議做不到服務(wù)器主動(dòng)向客戶端推送信息。

這種單向請(qǐng)求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。我們只能使用"輪詢":每隔一段時(shí)候,就發(fā)出一個(gè)詢問,了解服務(wù)器有沒有新的信息。最典型的場(chǎng)景就是聊天室。輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開)。
在 WebSocket 協(xié)議出現(xiàn)以前,創(chuàng)建一個(gè)和服務(wù)端進(jìn)雙通道通信的 web 應(yīng)用,需要依賴HTTP協(xié)議,進(jìn)行不停的輪詢,這會(huì)導(dǎo)致一些問題:
服務(wù)端被迫維持來自每個(gè)客戶端的大量不同的連接 大量的輪詢請(qǐng)求會(huì)造成高開銷,比如會(huì)帶上多余的header,造成了無用的數(shù)據(jù)傳輸。
http 協(xié)議本身是沒有持久通信能力的,但是我們?cè)趯?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í),數(shù)據(jù)是通過HTTP傳輸?shù)?。但是建立之后,在真正傳輸時(shí)候是不需要HTTP協(xié)議的。
下面一張圖說明了 HTTP 與 WebSocket 的主要區(qū)別:

不同點(diǎn):
1、 WebSocket是雙向通信協(xié)議,模擬Socket協(xié)議,可以雙向發(fā)送或接受信息,而HTTP是單向的;2、 WebSocket是需要瀏覽器和服務(wù)器握手進(jìn)行建立連接的,而http是瀏覽器發(fā)起向服務(wù)器的連接。3、 雖然 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 是一個(gè)持久化的協(xié)議,相對(duì)于 HTTP 這種非持久的協(xié)議來說。簡(jiǎn)單的舉個(gè)例子吧,用目前應(yīng)用比較廣泛的 PHP 生命周期來解釋。
HTTP 的生命周期通過 Request 來界定,也就是一個(gè) Request 一個(gè) Response ,那么在 HTTP1.0 中,這次 HTTP 請(qǐng)求就結(jié)束了。
在 HTTP1.1 中進(jìn)行了改進(jìn),使得有一個(gè) keep-alive,也就是說,在一個(gè) HTTP 連接中,可以發(fā)送多個(gè) Request,接收多個(gè) Response。但是請(qǐng)記住 Request = Response, 在 HTTP 中永遠(yuǎn)是這樣,也就是說一個(gè) Request 只能有一個(gè) Response。而且這個(gè) Response 也是被動(dòng)的,不能主動(dòng)發(fā)起。首先 WebSocket 是基于 HTTP 協(xié)議的,或者說借用了 HTTP 協(xié)議來完成一部分握手。
首先我們來看個(gè)典型的 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é)議的握手請(qǐng)求中,多了這么幾個(gè)東西。
Upgrade:?websocket
Connection:?Upgrade
這個(gè)就是 WebSocket 的核心了,告訴 Apache 、 Nginx 等服務(wù)器:注意啦,我發(fā)起的請(qǐng)求要用 WebSocket 協(xié)議,快點(diǎn)幫我找到對(duì)應(yīng)的助理處理~而不是那個(gè)老土的 HTTP 。
Sec-WebSocket-Key:?x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol:?chat,?superchat
Sec-WebSocket-Version:?13
首先, Sec-WebSocket-Key 是一個(gè) Base64 encode 的值,這個(gè)是瀏覽器隨機(jī)生成的,告訴服務(wù)器:泥煤,不要忽悠我,我要驗(yàn)證你是不是真的是 WebSocket 助理。 然后, Sec_WebSocket-Protocol 是一個(gè)用戶定義的字符串,用來區(qū)分同 URL 下,不同的服務(wù)所需要的協(xié)議。簡(jiǎn)單理解:今晚我要服務(wù)A,別搞錯(cuò)啦~ 最后, Sec-WebSocket-Version 是告訴服務(wù)器所使用的 WebSocket Draft (協(xié)議版本),在最初的時(shí)候,WebSocket 協(xié)議還在 Draft 階段,各種奇奇怪怪的協(xié)議都有,而且還有很多奇奇怪怪不同的東西,什么 Firefox 和 Chrome 用的不是一個(gè)版本之類的,當(dāng)初 WebSocket 協(xié)議太多可是一個(gè)大難題。。不過現(xiàn)在還好,已經(jīng)定下來啦~大家都使用同一個(gè)版本:服務(wù)員,我要的是13歲的噢→_→ 然后服務(wù)器會(huì)返回下列東西,表示已經(jīng)接受到請(qǐ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
依然是固定的,告訴客戶端即將升級(jí)的是 WebSocket 協(xié)議,而不是 mozillasocket ,lurnarsocket 或者 shitsocket。
然后, Sec-WebSocket-Accept 這個(gè)則是經(jīng)過服務(wù)器確認(rèn),并且加密過后的 Sec-WebSocket-Key。服務(wù)器:好啦好啦,知道啦,給你看我的 ID CARD 來證明行了吧。后面的, Sec-WebSocket-Protocol 則是表示最終使用的協(xié)議。至此,HTTP 已經(jīng)完成它所有工作了,接下來就是完全按照 WebSocket 協(xié)議進(jìn)行了??偨Y(jié), WebSocket 連接的過程是:
首先,客戶端發(fā)起http請(qǐng)求,經(jīng)過3次握手后,建立起TCP連接; http請(qǐng)求里存放WebSocket支持的版本號(hào)等信息,如:Upgrade、Connection、WebSocket-Version等;然后,服務(wù)器收到客戶端的握手請(qǐng)求后,同樣采用HTTP協(xié)議回饋數(shù)據(jù); 最后,客戶端收到連接成功的消息后,開始借助于TCP傳輸信道進(jìn)行全雙工通信。
四、Websocket的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
WebSocket協(xié)議一旦建議后,互相溝通所消耗的請(qǐng)求頭是很小的 服務(wù)器可以向客戶端推送消息了
缺點(diǎn):
少部分瀏覽器不支持,瀏覽器支持的程度與方式有區(qū)別(IE10)
五、WebSocket應(yīng)用場(chǎng)景
即時(shí)聊天通信 多玩家游戲 在線協(xié)同編輯/編輯 實(shí)時(shí)數(shù)據(jù)流的拉取與推送 體育/游戲?qū)崨r 實(shí)時(shí)地圖位置 即時(shí)Web應(yīng)用程序:即時(shí)Web應(yīng)用程序使用一個(gè)Web套接字在客戶端顯示數(shù)據(jù),這些數(shù)據(jù)由后端服務(wù)器連續(xù)發(fā)送。在WebSocket中,數(shù)據(jù)被連續(xù)推送/傳輸?shù)揭呀?jīng)打開的同一連接中,這就是為什么WebSocket更快并提高了應(yīng)用程序性能的原因。例如在交易網(wǎng)站或比特幣交易中,這是最不穩(wěn)定的事情,它用于顯示價(jià)格波動(dòng),數(shù)據(jù)被后端服務(wù)器使用Web套接字通道連續(xù)推送到客戶端。 游戲應(yīng)用程序:在游戲應(yīng)用程序中,你可能會(huì)注意到,服務(wù)器會(huì)持續(xù)接收數(shù)據(jù),而不會(huì)刷新用戶界面。屏幕上的用戶界面會(huì)自動(dòng)刷新,而且不需要建立新的連接,因此在WebSocket游戲應(yīng)用程序中非常有幫助。 聊天應(yīng)用程序:聊天應(yīng)用程序僅使用WebSocket建立一次連接,便能在訂閱戶之間交換,發(fā)布和廣播消息。它重復(fù)使用相同的WebSocket連接,用于發(fā)送和接收消息以及一對(duì)一的消息傳輸。
六、websocket 斷線重連
心跳就是客戶端定時(shí)的給服務(wù)端發(fā)送消息,證明客戶端是在線的, 如果超過一定的時(shí)間沒有發(fā)送則就是離線了。
如何判斷在線離線?
當(dāng)客戶端第一次發(fā)送請(qǐng)求至服務(wù)端時(shí)會(huì)攜帶唯一標(biāo)識(shí)、以及時(shí)間戳,服務(wù)端到db或者緩存去查詢改請(qǐng)求的唯一標(biāo)識(shí),如果不存在就存入db或者緩存中, 第二次客戶端定時(shí)再次發(fā)送請(qǐng)求依舊攜帶唯一標(biāo)識(shí)、以及時(shí)間戳,服務(wù)端到db或者緩存去查詢改請(qǐng)求的唯一標(biāo)識(shí),如果存在就把上次的時(shí)間戳拿取出來,使用當(dāng)前時(shí)間戳減去上次的時(shí)間, 得出的毫秒秒數(shù)判斷是否大于指定的時(shí)間,若小于的話就是在線,否則就是離線;
如何解決斷線問題
通過查閱資料了解到 nginx 代理的 websocket 轉(zhuǎn)發(fā),無消息連接會(huì)出現(xiàn)超時(shí)斷開問題。網(wǎng)上資料提到解決方案兩種,一種是修改nginx配置信息,第二種是 websocket 發(fā)送心跳包。下面就來總結(jié)一下本次項(xiàng)目實(shí)踐中解決的 websocket 的斷線 和 重連 這兩個(gè)問題的解決方案。主動(dòng)觸發(fā)包括主動(dòng)斷開連接,客戶端主動(dòng)發(fā)送消息給后端
1 主動(dòng)斷開連接
ws.close();
主動(dòng)斷開連接,根據(jù)需要使用,基本很少用到。
2 主動(dòng)發(fā)送消息
ws.send("hello?world");
斷線的可能原因1:websocket超時(shí)沒有消息自動(dòng)斷開連接,應(yīng)對(duì)措施:這時(shí)候我們就需要知道服務(wù)端設(shè)置的超時(shí)時(shí)長(zhǎng)是多少,在小于超時(shí)時(shí)間內(nèi)發(fā)送心跳包,有2種方案:一種是客戶端主動(dòng)發(fā)送上行心跳包,另一種方案是服務(wù)端主動(dòng)發(fā)送下行心跳包。
下面主要講一下客戶端也就是前端如何實(shí)現(xiàn)心跳包:
首先了解一下心跳包機(jī)制
跳包之所以叫心跳包是因?yàn)椋核裥奶粯用扛艄潭〞r(shí)間發(fā)一次,以此來告訴服務(wù)器,這個(gè)客戶端還活著。事實(shí)上這是為了保持長(zhǎng)連接,至于這個(gè)包的內(nèi)容,是沒有什么特別規(guī)定的,不過一般都是很小的包,或者只包含包頭的一個(gè)空包。
在 TCP 的機(jī)制里面,本身是存在有心跳包的機(jī)制的,也就是 TCP 的選項(xiàng):SO_KEEPALIVE 。系統(tǒng)默認(rèn)是設(shè)置的2小時(shí)的心跳頻率。但是它檢查不到機(jī)器斷電、網(wǎng)線拔出、防火墻這些斷線。而且邏輯層處理斷線可能也不是那么好處理。一般,如果只是用于?;钸€是可以的。
心跳包一般來說都是在邏輯層發(fā)送空的 echo 包來實(shí)現(xiàn)的。下一個(gè)定時(shí)器,在一定時(shí)間間隔下發(fā)送一個(gè)空包給客戶端,然后客戶端反饋一個(gè)同樣的空包回來,服務(wù)器如果在一定時(shí)間內(nèi)收不到客戶端發(fā)送過來的反饋包,那就只有認(rèn)定說掉線了。
在長(zhǎng)連接下,有可能很長(zhǎng)一段時(shí)間都沒有數(shù)據(jù)往來。理論上說,這個(gè)連接是一直保持連接的,但是實(shí)際情況中,如果中間節(jié)點(diǎn)出現(xiàn)什么故障是難以知道的。更要命的是,有的節(jié)點(diǎn)(防火墻)會(huì)自動(dòng)把一定時(shí)間之內(nèi)沒有數(shù)據(jù)交互的連接給斷掉。在這個(gè)時(shí)候,就需要我們的心跳包了,用于維持長(zhǎng)連接,?;睢?/p>
心跳檢測(cè)步驟:
客戶端每隔一個(gè)時(shí)間間隔發(fā)生一個(gè)探測(cè)包給服務(wù)器 客戶端發(fā)包時(shí)啟動(dòng)一個(gè)超時(shí)定時(shí)器 服務(wù)器端接收到檢測(cè)包,應(yīng)該回應(yīng)一個(gè)包 如果客戶機(jī)收到服務(wù)器的應(yīng)答包,則說明服務(wù)器正常,刪除超時(shí)定時(shí)器 如果客戶端的超時(shí)定時(shí)器超時(shí),依然沒有收到應(yīng)答包,則說明服務(wù)器掛了
//?前端解決方案:心跳檢測(cè)
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ā)送一個(gè)心跳,后端收到后,返回一個(gè)心跳消息,
????????????//onmessage拿到返回的心跳就說明連接正常
????????????ws.send("ping");
????????????console.log("ping!")
????????????self.serverTimeoutObj?=?setTimeout(function(){//如果超過一定時(shí)間還沒重置,說明后端主動(dòng)斷開了
????????????????ws.close();?//如果onclose會(huì)執(zhí)行reconnect,我們執(zhí)行ws.close()就行了.如果直接執(zhí)行reconnect?會(huì)觸發(fā)onclose導(dǎo)致重連兩次
????????????},?self.timeout);
????????},?this.timeout);
????}
}
斷線的可能原因2: websocket異常包括服務(wù)端出現(xiàn)中斷,交互切屏等等客戶端異常中斷等等 當(dāng)若服務(wù)端宕機(jī)了,客戶端怎么做、服務(wù)端再次上線時(shí)怎么做?客戶端則需要斷開連接,通過onclose關(guān)閉連接,服務(wù)端再次上線時(shí)則需要清除之間存的數(shù)據(jù),若不清除 則會(huì)造成只要請(qǐng)求到服務(wù)端的都會(huì)被視為離線。
針對(duì)這種異常的中斷解決方案就是處理重連,下面我們給出的重連方案是使用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)測(cè)支持使用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斷開鏈接時(shí)調(diào)用網(wǎng)絡(luò)中斷監(jiān)測(cè)
websocket.onclose?=>?()?{
????onLineCheck();
};
以上方案,只是拋磚引玉,如果大家有更好的解決方案歡迎評(píng)論區(qū)分享交流。
七、總結(jié)
WebSocket 是為了在 web 應(yīng)用上進(jìn)行雙通道通信而產(chǎn)生的協(xié)議,相比于輪詢HTTP請(qǐng)求的方式,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é)議升級(jí)轉(zhuǎn)換。
作者:Gaby 原文鏈接 ?https://juejin.cn/post/7020964728386093093
參考
阮一峰:WebSocket 教程 看完讓你徹底理解 WebSocket 原理

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
