一文吃透 WebSocket 原理
作者:Gaby 原文鏈接 ?https://juejin.cn/post/7020964728386093093
一.前言
踩著年末的尾巴,提前布局來年,為來年的工作做個好的鋪墊,所以就開始了面試歷程,因為項目中使用到了 WebSocket ,面試官在深挖項目經(jīng)驗的時候,也難免提到 WebSocket 相關的知識點,因為之前并沒有考慮這么深,所以,回答的還是有所欠缺,因此,趕緊趁熱再熟悉熟悉,也借此機會,整理出來供大家咀嚼,每個項目都有其值得挖掘的閃光點,要用有愛的眼睛??去發(fā)現(xiàn)。
二.什么是 WebSocket
WebSocket 是一種在單個TCP連接上進行全雙工通信的協(xié)議。WebSocket 使得客戶端和服務器之間的數(shù)據(jù)交換變得更加簡單,允許服務端主動向客戶端推送數(shù)據(jù)。
在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接, 并進行雙向數(shù)據(jù)傳輸。(維基百科)
WebSocket 本質上一種計算機網(wǎng)絡應用層的協(xié)議,用來彌補 http 協(xié)議在持久通信能力上的不足。
WebSocket 協(xié)議在2008年誕生,2011年成為國際標準。現(xiàn)在最新版本瀏覽器都已經(jīng)支持了。
它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發(fā)送信息,是真正的雙向平等對話,屬于服務器推送技術的一種。
WebSocket 的其他特點包括:
(1)建立在 TCP 協(xié)議之上,服務器端的實現(xiàn)比較容易。 (2)與 HTTP 協(xié)議有著良好的兼容性。默認端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。 (3)數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。 (4)可以發(fā)送文本,也可以發(fā)送二進制數(shù)據(jù)。 (5)沒有同源限制,客戶端可以與任意服務器通信。 (6)協(xié)議標識符是ws(如果加密,則為wss),服務器網(wǎng)址就是 URL。
ws://example.com:80/some/path
為什么需要 WebSocket?
我們已經(jīng)有了 HTTP 協(xié)議,為什么還需要另一個協(xié)議?它能帶來什么好處?
因為 HTTP 協(xié)議有一個缺陷:通信只能由客戶端發(fā)起,不具備服務器推送能力。
舉例來說,我們想了解查詢今天的實時數(shù)據(jù),只能是客戶端向服務器發(fā)出請求,服務器返回查詢結果。HTTP 協(xié)議做不到服務器主動向客戶端推送信息。

這種單向請求的特點,注定了如果服務器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。我們只能使用"輪詢":每隔一段時候,就發(fā)出一個詢問,了解服務器有沒有新的信息。最典型的場景就是聊天室。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。
在 WebSocket 協(xié)議出現(xiàn)以前,創(chuàng)建一個和服務端進雙通道通信的 web 應用,需要依賴HTTP協(xié)議,進行不停的輪詢,這會導致一些問題:
服務端被迫維持來自每個客戶端的大量不同的連接 大量的輪詢請求會造成高開銷,比如會帶上多余的header,造成了無用的數(shù)據(jù)傳輸。
http 協(xié)議本身是沒有持久通信能力的,但是我們在實際的應用中,是很需要這種能力的,所以,為了解決這些問題,WebSocket 協(xié)議由此而生,于2011年被IETF定為標準RFC6455,并被RFC7936所補充規(guī)范。并且在 HTML5 標準中增加了有關 WebSocket 協(xié)議的相關 api ,所以只要實現(xiàn)了 HTML5 標準的客戶端,就可以與支持 WebSocket 協(xié)議的服務器進行全雙工的持久通信了。
WebSocket 與 HTTP 的區(qū)別
WebSocket 與 HTTP 的關系圖:

相同點: 都是一樣基于TCP的,都是可靠性傳輸協(xié)議。都是應用層協(xié)議。
聯(lián)系: WebSocket在建立握手時,數(shù)據(jù)是通過HTTP傳輸?shù)摹5墙⒅螅谡嬲齻鬏敃r候是不需要HTTP協(xié)議的。
下面一張圖說明了 HTTP 與 WebSocket 的主要區(qū)別:

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