最近在一個項目中,需要使用到websocket,於是就花了一點時間來熟悉websocket並總結寫篇blog。
為何使用websocket
在瀏覽器與服務器通信間,傳統的 HTTP 請求在某些場景下並不理想,比如實時聊天、實時性的小游戲等等,
其面臨主要兩個缺點:
- 無法做到消息的「實時性」;
- 服務端無法主動推送信息;
其基於 HTTP 的主要解決方案有:
- 基於 ajax 的輪詢:客戶端定時或者動態相隔短時間內不斷向服務端請求接口,詢問服務端是否有新信息;其缺點也很明顯:多余的空請求(浪費資源)、數據獲取有延時;
- Long Poll:其采用的是阻塞性的方案,客戶端向服務端發起 ajax 請求,服務端掛起該請求不返回數據直到有新的數據,客戶端接收到數據之后再次執行 Long Poll;該方案中每個請求都掛起了服務器資源,在大量連接的場景下是不可接受的;
可以看到,基於 HTTP 協議的方案都包含一個本質缺陷 —— 「被動性」,服務端無法下推消息,僅能由客戶端發起請求不斷詢問是否有新的消息,同時對於客戶端與服務端都存在性能消耗。
WebSocket 是 HTML5 開始提供的一種瀏覽器與服務器間進行全雙工通訊的網絡技術。 WebSocket 通信協議於2011年被IETF定為標准RFC 6455,WebSocketAPI 被 W3C 定為標准。 在 WebSocket API 中,瀏覽器和服務器只需要要做一個握手的動作,然后,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。
WebSocket 是 HTML5 中提出的新的網絡協議標准,其包含幾個特點:
- 建立於 TCP 協議之上的應用層;
- 一旦建立連接(直到斷開或者出錯),服務端與客戶端握手后則一直保持連接狀態,是持久化連接;
- 服務端可通過實時通道主動下發消息;
- 數據接收的「實時性(相對)」與「時序性」;
- 較少的控制開銷。連接創建后,ws客戶端、服務端進行數據交換時,協議控制的數據包頭部較小。在不包含頭部的情況下,服務端到客戶端的包頭只有2~10字節(取決於數據包長度),客戶端到服務端的的話,需要加上額外的4字節的掩碼。而HTTP協議每次通信都需要攜帶完整的頭部。
- 支持擴展。ws協議定義了擴展,用戶可以擴展協議,或者實現自定義的子協議。(比如支持自定義壓縮算法等)
實踐
在瀏覽器中使用 Websocket 非常簡單,在支持 Websocket 的瀏覽器中會提供了原生的 WebSocekt 對象,其中對於消息的接收與數據幀處理在瀏覽器中已經封裝好了。
以下將用一個簡單的例子解釋如何使用 WebSocekt;
瀏覽器中提供了原生類 WebSocket ,使用 new 關鍵字實例化它:
WebSocket WebSocket(String url,optional String | [] protocols); //let websocket = new WebSocket("ws://echo.websocket.org/");
接收兩個參數:
-
url 表示需要連接的地址,比如:ws://localhost:8080;
-
protocols 可選參數,可以是一個字符串或者一個數組,用來表示子協議,這樣做可以讓一個服務器實現多種 WebSocket 子協議;
實例化對象提供兩個方法: -
send 接收一個 String|ArrayBuffer|Blob 數據,作為數據發送到服務端;
-
close 接收一個(可選)的 code(關閉狀態號,默認為 1000) 與一個(可選)的字符串(表示斷開原因),客戶端主動斷開連接;
連接狀態:
WebSocket 類提供了一些常量表示連接狀態:
- WebSocket.CONNECTING 0 連接還沒開啟;
- WebSocket.OPEN 1 連接已開啟並准備好進行通信;
- WebSocket.CLOSING 3 連接正在關閉的過程中;
- WebSocket.CLOSED 4 連接已經關閉,或者連接無法建立;
- WebSocket 的實例對象中提供了 readyState 屬性來判斷當前狀態;
實例化對象中可以監聽到以下事件:
- open 連接打開的回調事件,這時 readyState 變為 OPEN;
- message 收到消息的回調事件,同時回調函數接收到一個 MessageEvent 數據;
- close 連接關閉的回調事件,這時 readyState 變為 CLOSED;
- error 建立與連接過程發生錯誤的回調事件;
代碼實現
<h1>Echo Test</h1>
<input id="sendTxt" type="text">
<button id="sendBtn">發送</button>
<div id="recv"></div>
<script type="text/javascript">
var websocket = new WebSocket("ws://echo.websocket.org/"); // 引入websocket
websocket.onopen = function(){ console.log('websocket open'); document.getElementById("recv").innerHTML = "Connected"; } // 結束websocket
websocket.onclose = function(){ console.log('websocket close'); } // 接受到信息
websocket.onmessage = function(e){ console.log(e.data); document.getElementById("recv").innerHTML = e.data; } // 點擊發送webscoket
document.getElementById("sendBtn").onclick = function(){ var txt = document.getElementById("sendTxt").value; websocket.send(txt); } </script>
首先觸發 open 事件,之后每次發送數據服務端都會回復數據,因此觸發了 message 事件,如果觸發 close 事件;這里最后一次發送之后未收到服務端回復也是因為客戶端立即斷開了連接;websocket.send()是發送信息方法
事件與數據
對 WebSocket 實例監聽事件有兩種方式,這里以 message 事件為例:
- 對 onmessage 屬性直接賦值,正如以上:ws.onmessage = function () {};
- 使用 addEventListener 監聽事件,如:ws.addEventListener('message', function () {});
在 message 回調函數中得到 MessageEvent 類型參數 e ,我們需要的數據可以通過 e.data 獲取;
需要注意的一點是:不論服務端與客戶端,其接受到的數據都是序列化后的字符串(當然也有 ArrayBuffer|Blob 類型數據),很多時候我們需要解析處理數據,比如 JSON.parse(e.data);
連接穩定性
由於網絡環境復雜,某些情況會出現斷開連接或者連接出錯,需要我們在 close 或者 error 事件中監聽非正常斷開並重連;
由於一些原因在 error 時瀏覽器並不會響應回調事件,因此穩妥的做法還需要在 open 之后開啟一個定時任務去判斷當前的連接狀態 readyState ,在出現異常情況下嘗試重連;
心跳
websocket規范定義了心跳機制,一方可以通過發送ping(opcode 0x9)消息給另一方,另一方收到ping后應該盡可能快的返回pong(0xA)。
心跳機制是用於檢測連接的對方在線狀態,因此如果沒有心跳,那么無法判斷一方還在連接狀態中,一些網絡層比如 nginx 或者瀏覽器層會主動斷開連接,
在 JavaScript 中,WebSocket 並沒有開放 ping/pong 的 API ,雖然瀏覽器自帶了心跳處理,然而不同廠商的實現也不盡相同,因此需要在我們開發時候與服務端約定好一個自實現的心跳機制;
比如瀏覽器中,檢測到 open 事件后,啟動一個定時任務,每次發送數據 0x9 給服務端,而服務端返回 0xA 作為響應;
實踐下來,心跳的定時任務一般是相隔 15-20 秒發送一次。
舉例,WebSocket服務端向客戶端發送ping,只需要如下代碼(采用ws模塊)
網絡協議
前文說到,Websocket 是建立與 TCP 之上,那么其與 HTTP 協議有和關系呢?
Websocket 連接分為建連階段與連接階段,在建立連接階段借助於 HTTP ,而在連接階段則與 HTTP 無關。
建連階段
從瀏覽器的 Network 中,找到 ws 連接,可以看到:
General Request URL:ws://localhost:8080/ Request Method:GET Status Code:101 Switching Protocols Response Headers HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: py9bt3HbjicUUmFWJfI0nhGombo= Request Headers GET ws://localhost:8080/ HTTP/1.1 Host: localhost:8080 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://localhost:8080 Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36 DNT: 1 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,la;q=0.6,ja;q=0.5 Sec-WebSocket-Key: 2idFk3+96Hs5hh+c9GOQCg== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
這是一個標准的 HTTP 請求,相比於我們常見的 HTTP 請求協議,請求頭中多了幾個字段:
Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: 2idFk3+96Hs5hh+c9GOQCg==
重點請求首部意義如下:
- Connection: Upgrade:表示要升級協議
- Upgrade: websocket:表示要升級到websocket協議。
- Sec-WebSocket-Version: 13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,里面包含服務端支持的版本號。
- Sec-WebSocket-Key :是一個 Base64 encode 的值,由瀏覽器隨機生成的,用於驗證服務器連接的正確性;與后面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。
- Connection 為 Upgrade ,Upgrade 為 websocket ,表示告知 Nginx 與 Apache 等服務器該次連接並非為 HTTP 連接,實質上是一個 websocket ,因此服務器會轉發到相應的 websocket 任務處理;
- Sec-WebSocket-Versio 表示為使用的 websocket 服務版本;
響應頭中:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: py9bt3HbjicUUmFWJfI0nhGombo=
可以看到其返回狀態碼為 101 ,表示切換協議;
Upgrade 與 Connection 用於回復客戶端表示已經切換協議成功;
Sec-WebSocket-Accept 字段與 Sec-WebSocket-Key 相對應,用於驗證服務的正確性;
連接階段
當通過 HTTP 建立連接握手后,接下來則是真正的 Websocket 連接了,其基於 TCP 收發數據,Websocket 封裝並開放接口。
WSS
在 HTTP 協議中,很多時候為了加密與安全需要使用 HTTPS 請求(HTTP + TCL);
相應的,在 Websocket 協議中,也是可以使用加密傳輸的 —— wss ,比如 wss://localhost:8080。
使用的也是與 HTTPS 一樣的證書,在這里一般是交由 Nginx 等服務層去做證書處理。
Sec-WebSocket-Key/Accept的作用
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在於提供基礎的防護,減少惡意連接、意外連接。
作用大致歸納如下:
- 避免服務端收到非法的websocket連接(比如http客戶端不小心請求連接websocket服務,此時服務端可以直接拒絕連接)
- 確保服務端理解websocket連接。因為ws握手階段采用的是http協議,因此可能ws連接是被一個http服務器處理並返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協議。(並非百分百保險,比如總是存在那么些無聊的http服務器,光處理Sec-WebSocket-Key,但並沒有實現ws協議。。。)
- 用瀏覽器里發起ajax請求,設置header時,Sec-WebSocket-Key以及其他相關的header是被禁止的。這樣可以避免客戶端發送ajax請求時,意外請求協議升級(websocket upgrade)
- 可以防止反向代理(不理解ws協議)返回錯誤的數據。比如反向代理前后收到兩次ws連接的升級請求,反向代理把第一次請求的返回給cache住,然后第二次請求到來時直接把cache住的請求給返回(無意義的返回)。
- Sec-WebSocket-Key主要目的並不是確保數據的安全性,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。
強調:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但連接是否安全、數據是否安全、客戶端/服務端是否合法的 ws客戶端、ws服務端,其實並沒有實際性的保證。
數據掩碼的作用
WebSocket協議中,數據掩碼的作用是增強協議的安全性。但數據掩碼並不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。除了加密通道本身,似乎沒有太多有效的保護通信安全的辦法。
那么為什么還要引入掩碼計算呢,除了增加計算機器的運算量外似乎並沒有太多的收益。
答案還是兩個字:安全。但並不是為了防止數據泄密,而是為了防止早期版本的協議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。
本文參考文章: https://qiutc.me/post/websocket-guide.html 、https://segmentfault.com/a/1190000012709475
