WebSocket 是一種網絡通訊協議。在 2009 年誕生,於 2011 年被 IETF 定爲標準 RFC 6455 通訊標準。並由 RFC7936 補充規範。WebSocket API 也被 W3C 定爲標準。html
WebSocket 是 HTML5 開始提供的一種在單個 TCP 鏈接上進行全雙工(full-duplex)通信的協議。沒有了 Request 和 Response 的概念,二者地位徹底平等,鏈接一旦創建,就創建了真•持久性鏈接,雙方能夠隨時向對方發送數據。nginx
(HTML5 是 HTML 最新版本,包含一些新的標籤和全新的 API。HTTP 是一種協議,目前最新版本是 HTTP/2 ,因此 WebSocket 和 HTTP 有一些交集,二者相異的地方仍是不少。二者交集的地方在 HTTP 握手階段,握手成功後,數據就直接從 TCP 通道傳輸。)git
在沒有 WebSocket 以前,Web 爲了實現即時通訊,有如下幾種方案,最初的 polling ,到以後的 Long polling,最後的基於 streaming 方式,再到最後的 SSE,也是經歷了幾個不種的演進方式。github
這種方式下,是不適合獲取實時信息的,客戶端和服務器之間會一直進行鏈接,每隔一段時間就詢問一次。客戶端會輪詢,有沒有新消息。這種方式鏈接數會不少,一個接受,一個發送。並且每次發送請求都會有 HTTP 的 Header,會很耗流量,也會消耗 CPU 的利用率。web
這個階段能夠看到,一個 Request 對應一個 Response,一來一回一來一回。算法
在 Web 端,短輪詢用 AJAX JSONP Polling 輪詢實現。跨域
因爲 HTTP 沒法無限時長的保持鏈接,因此不能在服務器和 Web 瀏覽器之間頻繁的長時間進行數據推送,因此 Web 應用經過經過頻繁的異步 JavaScript 和 XML (AJAX) 請求來實現輪循。數組
長輪詢是對輪詢的改進版,客戶端發送 HTTP 給服務器以後,有沒有新消息,若是沒有新消息,就一直等待。直到有消息或者超時了,纔會返回給客戶端。消息返回後,客戶端再次創建鏈接,如此反覆。這種作法在某種程度上減少了網絡帶寬和 CPU 利用率等問題。瀏覽器
這種方式也有必定的弊端,實時性不高。若是是高實時的系統,確定不會採用這種辦法。由於一個 GET 請求來回須要 2個 RTT,極可能在這段時間內,數據變化很大,客戶端拿到的數據已經延後不少了。緩存
另外,網絡帶寬低利用率的問題也沒有從根源上解決。每一個 Request 都會帶相同的 Header。
對應的,Web 也有 AJAX 長輪詢,也叫 XHR 長輪詢。
客戶端打開一個到服務器端的 AJAX 請求,而後等待響應,服務器端須要一些特定的功能來容許請求被掛起,只要一有事件發生,服務器端就會在掛起的請求中送回響應並關閉該請求。客戶端在處理完服務器返回的信息後,再次發出請求,從新創建鏈接,如此循環。
iframe 流方式是在頁面中插入一個隱藏的 iframe,利用其 src 屬性在服務器和客戶端之間建立一條長連接,服務器向 iframe 傳輸數據(一般是 HTML,內有負責插入信息的 JavaScript),來實時更新頁面。iframe 流方式的優勢是瀏覽器兼容好。
使用 iframe 請求一個長鏈接有一個很明顯的不足之處:IE、Morzilla Firefox 下端的進度欄都會顯示加載沒有完成,並且 IE 上方的圖標會不停的轉動,表示加載正在進行。
Google 的天才們使用一個稱爲 「htmlfile」 的 ActiveX 解決了在 IE 中的加載顯示問題,並將這種方法用到了 gmail+gtalk 產品中。Alex Russell 在 「What else is burried down in the depth's of Google's amazing JavaScript?」文章中介紹了這種方法。Zeitoun 網站提供的 comet-iframe.tar.gz,封裝了一個基於 iframe 和 htmlfile 的 JavaScript comet 對象,支持 IE、Mozilla Firefox 瀏覽器,能夠做爲參考。
實現思路:瀏覽器必須支持 multi-part 標誌,客戶端經過 AJAX 發出請求 Request,服務器保持住這個鏈接,而後能夠經過 HTTP1.1 的 chunked encoding 機制(分塊傳輸編碼)不斷 push 數據給客戶端,直到 timeout 或者手動斷開鏈接。
實現思路:在頁面中內嵌入一個使用了 Socket 類的 Flash 程序,JavaScript 經過調用此 Flash 程序提供的 Socket 接口與服務器端的 Socket 接口進行通訊,JavaScript 經過 Flash Socket 接收到服務器端傳送的數據。
服務器發送事件(SSE)也是 HTML5 公佈的一種服務器向瀏覽器客戶端發起數據傳輸的技術。一旦建立了初始鏈接,事件流將保持打開狀態,直到客戶端關閉。該技術經過傳統的 HTTP 發送,並具備 WebSockets 缺少的各類功能,例如自動從新鏈接、事件 ID 以及發送任意事件的能力。
SSE 就是利用服務器向客戶端聲明,接下來要發送的是流信息(streaming),會接二連三地發送過來。這時,客戶端不會關閉鏈接,會一直等着服務器發過來的新的數據流,能夠類比視頻流。SSE 就是利用這種機制,使用流信息向瀏覽器推送信息。它基於 HTTP 協議,目前除了 IE/Edge,其餘瀏覽器都支持。
SSE 是單向通道,只能服務器向瀏覽器發送,由於流信息本質上就是下載。
服務器向瀏覽器發送的 SSE 數據,必須是 UTF-8 編碼的文本,具備以下的 HTTP 頭信息。
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
複製代碼
上面三行之中,第一行的Content-Type必須指定 MIME 類型爲event-steam
以上是常見的四種基於流的作法,Iframe Streaming、XHR Streaming、Flash Streaming、Server-Sent Events。
從瀏覽器兼容難度看 —— 短輪詢/AJAX > 長輪詢/Comet > 長鏈接/SSE
從上面這幾種演進的方式來看,也是不斷改進的過程。
短輪詢效率低,很是浪費資源(網絡帶寬和計算資源)。有必定延遲、服務器壓力較大,而且大部分是無效請求。
長輪詢雖然省去了大量無效請求,減小了服務器壓力和必定的網絡帶寬的佔用,可是仍是須要保持大量的鏈接。
最後到了基於流的方式,在服務器往客戶端推送,這個方向的流實時性比較好。可是依舊是單向的,客戶端請求服務器依然還須要一次 HTTP 請求。
那麼人們就在考慮了,有沒有這樣一個完美的方案,即能雙向通訊,又能夠節約請求的 header 網絡開銷,而且有更強的擴展性,最好還能夠支持二進制幀,壓縮等特性呢?
因而人們就發明了這樣一個目前看似「完美」的解決方案 —— WebSocket。
在 HTML5 中公佈了 WebSocket 標準之後,直接取代了 Comet 成爲服務器推送的新方法。
Comet 是一種用於 web 的推送技術,能使服務器實時地將更新的信息傳送到客戶端,而無須客戶端發出請求,目前有兩種實現方式,長輪詢和 iframe 流。
優勢:
較少的控制開銷,在鏈接建立後,服務器和客戶端之間交換數據時,用於協議控制的數據包頭部相對較小。在不包含擴展的狀況下,對於服務器到客戶端的內容,此頭部大小隻有2至10字節(和數據包長度有關);對於客戶端到服務器的內容,此頭部還須要加上額外的4字節的掩碼。相對於 HTTP 請求每次都要攜帶完整的頭部,此項開銷顯著減小了。
更強的實時性,因爲協議是全雙工的,因此服務器能夠隨時主動給客戶端下發數據。相對於HTTP請求須要等待客戶端發起請求服務端才能響應,延遲明顯更少;即便是和Comet等相似的長輪詢比較,其也能在短期內更屢次地傳遞數據。
長鏈接,保持鏈接狀態。與HTTP不一樣的是,Websocket須要先建立鏈接,這就使得其成爲一種有狀態的協議,以後通訊時能夠省略部分狀態信息。而HTTP請求可能須要在每一個請求都攜帶狀態信息(如身份認證等)。
雙向通訊、更好的二進制支持。與 HTTP 協議有着良好的兼容性。默認端口也是 80 和 443,而且握手階段採用 HTTP 協議,所以握手時不容易被屏蔽,能經過各類 HTTP 代理服務器。
缺點:部分瀏覽器不支持(支持的瀏覽器會愈來愈多)。 應用場景:較新瀏覽器支持、不受框架限制、較高擴展性。
一句話總結一下 WebSocket:
WebSocket 是 HTML5 開始提供的一種獨立在單個 TCP 鏈接上進行全雙工通信的有狀態的協議(它不一樣於無狀態的 HTTP),而且還能支持二進制幀、擴展協議、部分自定義的子協議、壓縮等特性。
目前看來,WebSocket 是能夠完美替代 AJAX 輪詢和 Comet 。可是某些場景仍是不能替代 SSE,WebSocket 和 SSE 各有所長!
WebSocket 的 RFC6455 標準中制定了 2 個高級組件,一個是開放性 HTTP 握手用於協商鏈接參數,另外一個是二進制消息分幀機制用於支持低開銷的基於消息的文本和二進制數據傳輸。接下來就好好談談這兩個高級組件,這一章節詳細的談談握手的細節,下一個章節再談談二進制消息分幀機制。
首先,在 RFC6455 中寫了這樣一段話:
WebSocket 協議嘗試在既有 HTTP 基礎設施中實現雙向 HTTP 通訊,所以 也使用 HTTP 的 80 和 443 端口......不過,這個設計不限於經過 HTTP 實現 WebSocket 通訊,將來的實現能夠在某個專用端口上使用更簡單的握手,而 沒必要從新定義麼一個協議。
——WebSocket Protocol RFC 6455
從這段話中咱們可看出制定 WebSocket 協議的人的「野心」或者說對將來的規劃有多遠,WebSocket 制定之初就已經支持了能夠在任意端口上進行握手,而不只僅是要依靠 HTTP 握手。
不過目前用的對多的仍是依靠 HTTP 進行握手。由於 HTTP 的基礎設施已經至關完善了。
接下來看一個具體的 WebSocket 握手的例子。以筆者本身的網站 threes.halfrost.com/ 爲例。
打開這個網站,網頁一渲染就會開啓一個 wss 的握手請求。握手請求以下:
GET wss://threes.halfrost.com/sockjs/689/8x5nnke6/websocket HTTP/1.1
// 請求的方法必須是GET,HTTP版本必須至少是1.1
Host: threes.halfrost.com
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
// 請求升級到 WebSocket 協議
Origin: https://threes.halfrost.com
Sec-WebSocket-Version: 13
// 客戶端使用的 WebSocket 協議版本
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: _ga=GA1.2.00000006.14111111496; _gid=GA1.2.23232376.14343448247; Hm_lvt_d60c126319=1524898423,1525574369,1526206975,1526784803; Hm_lpvt_d606319=1526784803; _gat_53806_2=1
Sec-WebSocket-Key: wZgx0uTOgNUsHGpdWc0T+w==
// 自動生成的鍵,以驗證服務器對協議的支持,其值必須是 nonce 組成的隨機選擇的 16 字節的被 base64 編碼後的值
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
// 可選的客戶端支持的協議擴展列表,指示了客戶端但願使用的協議級別的擴展
複製代碼
這裏和普通的 HTTP 協議相比,不一樣的地方有如下幾處:
請求的 URL 是 ws:// 或者 wss:// 開頭的,而不是 HTTP:// 或者 HTTPS://。因爲 websocket 可能會被用在瀏覽器之外的場景,因此這裏就使用了自定義的 URI。類比 HTTP,ws協議:普通請求,佔用與 HTTP 相同的 80 端口;wss協議:基於 SSL 的安全傳輸,佔用與 TLS 相同的 443 端口。
Connection: Upgrade
Upgrade: websocket
複製代碼
這兩處是普通的 HTTP 報文通常沒有的,這裏利用 Upgrade 進行了協議升級,指明升級到 websocket 協議。
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: wZgx0uTOgNUsHGpdWc0T+w==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
複製代碼
Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 協議太多,不一樣廠商都有本身的協議版本,不過如今已經定下來了。若是服務端不支持該版本,須要返回一個 Sec-WebSocket-Version,裏面包含服務端支持的版本號。(詳情見下面的多版本的 websocket 握手一節)
最新版本就是 13,固然有可能存在很是早期的版本 7 ,8(目前基本不會不存在 7,8 的版本了)
注意:儘管本文檔的草案版本(0九、十、十一、和 12)發佈了(它們多不是編輯上的修改和澄清而不是改變電報協議 [wire protocol]),值 九、十、十一、和 12 不被用做有效的 Sec-WebSocket-Version。這些值被保留在 IANA 註冊中心,但並將不會被使用。
+--------+-----------------------------------------+----------+
|Version | Reference | Status |
| Number | | |
+--------+-----------------------------------------+----------+
| 0 + draft-ietf-hybi-thewebsocketprotocol-00 | Interim |
+--------+-----------------------------------------+----------+
| 1 + draft-ietf-hybi-thewebsocketprotocol-01 | Interim |
+--------+-----------------------------------------+----------+
| 2 + draft-ietf-hybi-thewebsocketprotocol-02 | Interim |
+--------+-----------------------------------------+----------+
| 3 + draft-ietf-hybi-thewebsocketprotocol-03 | Interim |
+--------+-----------------------------------------+----------+
| 4 + draft-ietf-hybi-thewebsocketprotocol-04 | Interim |
+--------+-----------------------------------------+----------+
| 5 + draft-ietf-hybi-thewebsocketprotocol-05 | Interim |
+--------+-----------------------------------------+----------+
| 6 + draft-ietf-hybi-thewebsocketprotocol-06 | Interim |
+--------+-----------------------------------------+----------+
| 7 + draft-ietf-hybi-thewebsocketprotocol-07 | Interim |
+--------+-----------------------------------------+----------+
| 8 + draft-ietf-hybi-thewebsocketprotocol-08 | Interim |
+--------+-----------------------------------------+----------+
| 9 + Reserved | |
+--------+-----------------------------------------+----------+
| 10 + Reserved | |
+--------+-----------------------------------------+----------+
| 11 + Reserved | |
+--------+-----------------------------------------+----------+
| 12 + Reserved | |
+--------+-----------------------------------------+----------+
| 13 + RFC 6455 | Standard |
+--------+-----------------------------------------+----------+
複製代碼
[RFC 6455]
The |Sec-WebSocket-Key| header field is used in the WebSocket opening handshake. It is sent from the client to the server to provide part of the information used by the server to prove that it received a valid WebSocket opening handshake. This helps ensure that the server does not accept connections from non-WebSocket clients (e.g., HTTP clients) that are being abused to send data to unsuspecting WebSocket servers.
Sec-WebSocket-Key 字段用於握手階段。它從客戶端發送到服務器以提供部份內容,服務器用來證實它收到的信息,而且能有效的完成 WebSocket 握手。這有助於確保服務器不會接受來自非 WebSocket 客戶端的鏈接(例如 HTTP 客戶端)被濫用發送數據到毫無防備的 WebSocket 服務器。
Sec-WebSocket-Key 是由瀏覽器隨機生成的,提供基本的防禦,防止惡意或者無心的鏈接。
Sec-WebSocket-Extensions 是屬於升級協商的部分,這裏放在下一章節進行詳細講解。
接着來看看 Response:
HTTP/1.1 101 Switching Protocols
// 101 HTTP 響應碼確認升級到 WebSocket 協議
Server: nginx/1.12.1
Date: Sun, 20 May 2018 09:06:28 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok=
// 簽名的鍵值驗證協議支持
Sec-WebSocket-Extensions: permessage-deflate
// 服務器選擇的WebSocket 擴展
複製代碼
在 Response 中,用 HTTP 101 響應碼迴應,確認升級到 WebSocket 協議。
一樣也有兩個 WebSocket 的 header:
Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok=
// 簽名的鍵值驗證協議支持
Sec-WebSocket-Extensions: permessage-deflate
// 服務器選擇的 WebSocket 擴展
複製代碼
Sec-WebSocket-Accept 是通過服務器確認後,而且加密以後的 Sec-WebSocket-Key。
Sec-WebSocket-Accept 的計算方法以下:
僞代碼:
> toBase64(sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ))
複製代碼
一樣,Sec-WebSocket-Key/Sec-WebSocket-Accept 只是在握手的時候保證握手成功,可是對數據安全並不保證,用 wss:// 會稍微安全一點。
WebSocket 握手有可能會涉及到子協議的問題。
先來看看 WebSocket 的對象初始化函數:
WebSocket WebSocket(
in DOMString url,
// 表示要鏈接的URL。這個URL應該爲響應WebSocket的地址。
in optional DOMString protocols
// 能夠是一個單個的協議名字字符串或者包含多個協議名字字符串的數組。默認設爲一個空字符串。
);
複製代碼
這裏有一個 optional ,是一個能夠協商協議的數組。
var ws = new WebSocket('wss://example.com/socket', ['appProtocol', 'appProtocol-v2']);
ws.onopen = function () {
if (ws.protocol == 'appProtocol-v2') {
...
} else {
...
}
}
複製代碼
在建立 WebSocket 對象的時候,能夠傳遞一個可選的子協議數組,告訴服務器,客戶端能夠理解哪些協議或者但願服務器接收哪些協議。服務器能夠從數據裏面選擇幾個支持的協議進行返回,若是一個都不支持,那麼會直接致使握手失敗。觸發 onerror 回調,並斷開鏈接。
這裏的子協議能夠是自定義的協議。
使用 WebSocket 版本通知能力( Sec-WebSocket-Version 頭字段),客戶端能夠初始請求它選擇的 WebSocket 協議的版本(這並不必定必須是客戶端支持的最新的)。若是服務器支持請求的版本且握手消息是原本有效的,服務器將接受該版本。若是服務器不支持請求的版本,它必須以一個包含全部它將使用的版本的 Sec-WebSocket-Version 頭字段(或多個 Sec-WebSocket-Version 頭字段)來響應。 此時,若是客戶端支持一個通知的版本,它可使用新的版本值重作 WebSocket 握手。
舉個例子:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 25
複製代碼
服務器不支持 25 的版本,則會返回:
HTTP/1.1 400 Bad Request
...
Sec-WebSocket-Version: 13, 8, 7
複製代碼
客戶端支持 13 版本的,則須要從新握手:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 13
複製代碼
在 WebSocket 握手階段,會 5 個帶 WebSocket 的 header。這 5 個 header 都是和升級協商相關的。
Sec-WebSocket-Version
客戶端代表本身想要使用的版本號(通常都是 13 號版本),若是服務器不支持這個版本,則須要返回本身支持的版本。客戶端拿到 Response 之後,須要對本身支持的版本號從新握手。這個 header 客戶端必需要發送。
Sec-WebSocket-Key
客戶端請求自動生成的一個 key。這個 header 客戶端必需要發送。
Sec-WebSocket-Accept
服務器針對客戶端的 Sec-WebSocket-Key 計算的響應值。這個 header 服務端必需要發送。
Sec-WebSocket-Protocol
用於協商應用子協議:客戶端發送支持的協議列表,服務器必須只回應一個協議名。若是服務器一個協議都不能支持,直接握手失敗。客戶端能夠不發送子協議,可是一旦發送,服務器沒法支持其中任意一個都會致使握手失敗。這個 header 客戶端可選發送。
Sec-WebSocket-Extensions
用於協商本次鏈接要使用的 WebSocket 擴展:客戶端發送支持的擴展,服務器經過返回相同的首部確認本身支持一或多個擴展。這個 header 客戶端可選發送。服務端若是都不支持,不會致使握手失敗,可是這次鏈接不能使用任何擴展。
協商是在握手階段,握手完成之後,HTTP 通訊結束,接下來的全雙工所有都交給 WebSocket 協議管理(TCP 通訊)。
負責制定 WebSocket 規範的 HyBi Working Group 就進行了兩項擴展 Sec-WebSocket-Extensions:
多路複用擴展(A Multiplexing Extension for WebSockets)
這個擴展能夠將 WebSocket 的邏輯鏈接獨立出來,實現共享底層的 TCP 鏈接。
壓縮擴展(Compression Extensions for WebSocket)
給 WebSocket 協議增長了壓縮功能。(例如 x-webkit-deflate-frame 擴展)
若是不進行多路複用擴展,每一個 WebSocket 鏈接都只能獨享專門的一個 TCP 鏈接,並且當遇到一個巨大的消息分紅多個幀的時候,容易產生隊首阻塞的狀況。隊首阻塞會致使延遲,因此分紅多個幀的時候能儘可能的小是關鍵。不過在進行了多路複用擴展之後,多個鏈接複用一個 TCP 鏈接,每一個信道依舊會存在隊首阻塞的問題。除了多路複用,還要進行多路並行發送消息。
若是經過 HTTP2 進行 WebSocket 傳輸,性能會更好一點,畢竟 HTTP2 原生就支持了流的多路複用。利用 HTTP2 的分幀機制進行 WebSocket 的分幀,多個 WebSocket 能夠在同一個會話中傳輸。
WebSocket 另外一個高級組件是:二進制消息分幀機制。WebSocket 會把應用的消息分割成一個或多個幀,接收方接到到多個幀會進行組裝,等到接收到完整消息以後再通知接收端。
WebSocket 數據幀格式以下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
複製代碼
FIN:0表示不是最後一個分片,1表示是最後一個分片。
RSV1, RSV2, RSV3:
通常狀況下全爲 0。當客戶端、服務端協商採用 WebSocket 擴展時,這三個標誌位能夠非 0,且值的含義由擴展進行定義。若是出現非零的值,且並無採用 WebSocket 擴展,鏈接出錯。
%x0:表示一個延續幀。當 Opcode 爲 0 時,表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片;
%x1:表示這是一個文本幀(text frame);
%x2:表示這是一個二進制幀(binary frame);
%x3-7:保留的操做代碼,用於後續定義的非控制幀;
%x8:表示鏈接斷開;
%x9:表示這是一個心跳請求(ping);
%xA:表示這是一個心跳響應(pong);
%xB-F:保留的操做代碼,用於後續定義的控制幀。
表示是否要對數據載荷進行掩碼異或操做。1表示須要,0表示不須要。(只適用於客戶端發給服務器的消息)
表示數據載荷的長度,這裏有 3 種狀況:
若是數據長度在 0 - 125 之間,那麼 Payload len 用 7 位表示足以,表示的數也就是淨荷長度;
若是數據長度等於 126,那麼 Payload len 須要用 7 + 16 位表示,接下來 2 字節表示的 16 位無符號整數纔是這一幀的長度;
若是數據長度等於 127,那麼 Payload len 須要用 7 + 64 位表示,接下來 8 字節表示的 64 位無符號整數纔是這一幀的長度。
若是 Mask = 0,則沒有 Masking-key,若是 Mask = 1,則 Masking-key 長度爲 4 字節,32位。
掩碼是由客戶端隨機選擇的 32 位值。 當準備一個掩碼的幀時,客戶端必須從容許的 32 位值集合中選擇一個新的掩碼鍵。 掩碼鍵須要是不可預測的;所以,掩碼鍵必須來自一個強大的熵源, 且用於給定幀的掩碼鍵必須不容易被服務器/代理預測用於後續幀的掩碼鍵。 掩碼鍵的不可預測性對防止惡意應用的做者選擇出如今報文上的字節是必要的。 RFC 4086 [RFC4086]討論了什麼須要一個用於安全敏感應用的合適的熵源。
掩碼不影響「負載數據」的長度。 變換掩碼數據到解掩碼數據,或反之亦然,如下算法被應用。 相同的算法應用,無論轉化的方向,例如,相同的步驟即應用到掩碼數據也應用到解掩碼數據。
變換數據的八位位組 i ("transformed-octet-i")是原始數據的八位位組 i("original-octet-i")異或(XOR)i 取模 4 位置的掩碼鍵的八位位組("masking-key-octet-j"):
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
複製代碼
算法簡單描述:按位作循環異或運算,先對該位的索引取模來得到 Masking-key 中對應的值 x,而後對該位與 x 作異或,從而獲得真實的 byte 數據。
注意:掩碼的做用並非爲了防止數據泄密,而是爲了防止客戶端中運行的惡意腳本對不支持 WebSocket 的中間設備進行代理緩存投毒攻擊(proxy cache poisoning attack)
要了解這種攻擊的細節,請參考 W2SP 2011 的論文Talking to Yourself for Fun and Profit。
攻擊主要分2步,
第一步,先進行一次 WebSocket 鏈接。黑客經過代理服務器向本身的服務器進行 WebSocket 握手,因爲 WebSocket 握手是 HTTP 消息,因此代理服務器把黑客本身服務器的 Response 轉發回給黑客的時候,會認爲本次 HTTP 請求結束。
第二步,在代理服務器上面製造「投毒」攻擊。因爲 WebSocket 握手成功,因此黑客能夠向本身的服務器上發送數據了,發送一條精心設置過的 HTTP 格式的文本信息。這條數據的 host 須要僞形成普通用戶即將要訪問的服務器,請求的資源是普通用戶即將要請求的資源。代理服務器會認爲這是一條新的請求,因而向黑客本身的服務器請求,這時候也須要黑客本身服務器配合,收到這條「投毒」之後的消息之後,當即返回「毒藥」,返回一些惡意的腳本資源等等。至此,「投毒」成功。
當用戶經過代理服務器請求要請求的安全資源的時候,因爲 host 和 url 以前已經被黑客利用 HTTP 格式的文本信息緩存進了代理服務器,「投毒」的資源也被緩存了,這個時候用戶請求相同的 host 和 url 的資源的時候,代理緩存服務器發現已經緩存了,就當即會把「投毒」之後的惡意腳本或資源返回給用戶。這時候用戶就收到了攻擊。
載荷數據分爲擴展數據和應用數據兩種。
擴展數據:若是沒有協商使用擴展的話,擴展數據數據爲0字節。擴展數據的長度若是存在,必須在握手階段就固定下來。載荷數據的長度也要算上擴展數據。
應用數據:若是存在擴展數據,則排在擴展數據以後。
控制幀由操做碼肯定,操做碼最高位爲 1。 當前定義的用於控制幀的操做碼包括 0x8 (Close)、0x9(Ping)、和0xA(Pong)。 操做碼 0xB-0xF 保留用於將來還沒有定義的控制幀。
控制幀用於傳達有關 WebSocket 的狀態。 控制幀能夠插入到分幀消息的中間。
全部的控制幀必須有一個小於等於125字節的有效載荷長度,控制幀必須不能被分幀。
在 RFC6455 中給出了關閉時候建議的狀態碼,沒有規範的定義,只是給了一個預約義的狀態碼。
狀態碼 | 說明 | 保留✔︎或者不能使用✖︎ |
---|---|---|
0-999 | 該範圍內的狀態碼不被使用。 | ✖︎ |
1000 | 表示正常關閉,意思是建議的鏈接已經完成了。 | |
1001 | 表示端點「離開」(going away),例如服務器關閉或瀏覽器導航到其餘頁面。 | |
1002 | 表示端點由於協議錯誤而終止鏈接。 | |
1003 | 表示端點因爲它收到了不能接收的數據類型(例如,端點僅理解文本數據,但接收到了二進制消息)而終止鏈接。 | |
1004 | 保留。可能在未來定義其具體的含義。 | ✔︎ |
1005 | 是一個保留值,且不能由端點在關閉控制幀中設置此狀態碼。 它被指定用在期待一個用於表示沒有狀態碼是實際存在的狀態碼的應用中。 | ✔︎ |
1006 | 是一個保留值,且不能由端點在關閉控制幀中設置此狀態碼。 它被指定用在期待一個用於表示鏈接異常關閉的狀態碼的應用中。 | ✔︎ |
1007 | 表示端點由於消息中接收到的數據是不符合消息類型而終止鏈接(好比,文本消息中存在非 UTF-8[RFC3629] 數據)。 | |
1008 | 表示端點由於接收到的消息違反其策略而終止鏈接。 這是一個當沒有其餘合適狀態碼(例如 1003 或 1009)或若是須要隱藏策略的具體細節時能被返回的通用狀態碼。 | |
1009 | 表示端點因接收到的消息對它的處理來講太大而終止鏈接。 | |
1010 | 表示端點(客戶端)由於它指望服務器協商一個或多個擴展,但服務器沒有在 WebSocket 握手響應消息中返回它們而終止鏈接。 所須要的擴展列表應該出如今關閉幀的 reason 部分。 | |
1011 | 表示服務器端由於遇到了一個不指望的狀況使它沒法知足請求而終止鏈接。 | |
1012 | ||
1013 | ||
1014 | ||
1015 | 是一個保留值,且不能由端點在關閉幀中被設置爲狀態碼。 它被指定用在期待一個用於表示鏈接因爲執行 TLS 握手失敗而關閉的狀態碼的應用中(好比,服務器證書不能驗證)。 | ✔︎ |
1000-2999 | 該範圍內的狀態碼保留給本協議、其將來的修訂和一個永久的和現成的公共規範中指定的擴展的定義。 | ✔︎ |
3000-3999 | 該範圍內的狀態碼保留給庫、框架和應用使用。 這些狀態碼直接向 IANA 註冊。本規範未定義這些狀態碼的解釋。 | ✔︎ |
4000-4999 | 該範圍內的狀態碼保留用於私有使用且所以不能被註冊。 這些狀態碼能夠被在 WebSocket 應用之間的先前的協議使用。 本規範未定義這些狀態碼的解釋。 | ✔︎ |
當接收到 0x9 Ping 操做碼的控制幀之後,應當當即發送一個包含 pong 操做碼的幀響應,除非接收到了一個關閉幀。兩端都會在鏈接創建後、關閉前的任意時間內發送 Ping 幀。Ping 幀能夠包含「應用數據」。ping 幀就能夠做爲 keepalive 心跳包。
當接收到 0xA pong 操做碼的控制幀之後,知道對方還可響應。Pong 幀必須包含與被響應 Ping 幀的應用程序數據徹底相同的數據。若是終端接收到一個 Ping 幀,且尚未對以前的 Ping 幀發送 Pong 響應,終端可能選擇發送一個 Pong 幀給最近處理的 Ping 幀。一個 Pong 幀可能被主動發送,這做爲單向心跳。儘可能不要主動發送 pong 幀。
分幀規則由 RFC6455 進行定義,應用對如何分幀是無感知的。分幀這一步由客戶端和服務器完成。
分幀也能夠更好的利用多路複用的協議擴展,多路複用須要能夠分割消息爲更小的分段來更好的共享輸出通道。
RFC 6455 規定的分幀規則以下:
例子:對於一個做爲三個片斷髮送的文本消息,第一個片斷將有一個 0x1 操做碼和一個 FIN 位清零,第二個片斷將有一個 0x0 操做碼和一個 FIN 位清零,且第三個片斷將有 0x0 操做碼和一個 FIN 位設置。(0x0 操做碼在上面講解過,表示一個延續幀。當 O操做碼 爲 0x0 時,表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片;)
實現注意:在沒有任何擴展時,一個接收者沒必要按順序緩衝整個幀來處理它。例如,若是使用了一個流式 API,一個幀的一部分能被交付到應用。可是,請注意這個假設可能不適用全部將來的 WebSocket 擴展。
WebSocket API 及其簡潔,能夠調用的函數只有下面這麼幾個:
var ws = new WebSocket('wss://example.com/socket');
ws.onerror = function (error) { ... }
ws.onclose = function () { ... }
ws.onopen = function () {
ws.send("Connection established. Hello server!");
}
ws.onmessage = function(msg) {
if(msg.data instanceof Blob) {
processBlob(msg.data);
} else {
processText(msg.data);
}
}
複製代碼
除去新建 WebSocket 對象和 send() 方法之外,剩下的就是4個回調方法了。
上述的這些方法中,send() 方法須要額外注意一點的是,這個方法是異步的,並非同步方法。意味着當咱們把要發送的內容丟到這個函數中的時候,函數就異步返回了,此時不要誤認爲已經發送出去了。WebSocket 自身有一個排隊的機制,數據會先丟到數據緩存區中,而後按照排隊的順序進行發送。
若是是一個巨大的文件排隊中,後面又來了一些優先級比這個消息高的消息,好比系統出錯,須要當即斷開鏈接。因爲排隊排在大文件以後,必須等待大文件發送完畢才能發送這個優先級更高的消息。這就形成了隊首阻塞的問題了,致使優先級更高的消息延遲。
WebSocket API 制定者考慮到了這個問題,因而給了咱們另外 2 個爲數很少的能夠改變 WebSocket 對象行爲的屬性,一個是 bufferedAmount,另一個是 binaryType。
if (ws.bufferedAmount == 0)
ws.send(evt.data);
複製代碼
在上述這種狀況下就可使用 bufferedAmount 監聽緩存區的數量,從而避免隊首阻塞的問題,更進一步也能夠和 Priority Queue 結合到一塊兒,實現按照優先級高低來發送消息。
WebSocket 對傳輸的格式沒有任何限制,能夠是文本也能夠是二進制,均可以。協議中經過 Opcode 類型字段來區分是 UTF-8 仍是二進制。WebSocket API 能夠接收 UTF-8 編碼的 DOMString 對象,也能夠接收 ArrayBuffer、 ArrayBufferView 或 Blob 等二進制數據。
瀏覽器對接收到的數據,若是不手動設置任何其餘選項的話,默認處理是,文本是默認轉成 DOMString 對象,二進制數據或者 Blob 對象會直接轉給給應用,中間不作任何處理。
var ws = new WebSocket('wss://example.com/socket');
ws.binaryType = "arraybuffer";
複製代碼
惟一能干涉的地方就是把接收到的二進制數據所有都強制轉換成 arraybuffer 類型而不是 Blob 類型。至於爲什麼要轉換成 arraybuffer 類型, W3C 的候選人給出的建議以下:
用戶代理能夠將這個選項看做一個暗示,以決定如何處理接收到的二進制數據:若是這裏設置爲 「blob」,那就能夠放心地將其轉存到磁盤上;而若是設置爲 「arraybuffer」,那極可能在內存裏處理它更有效。天然地,咱們鼓勵用戶代理使用更細微的線索,以決定是否將到來的數據放到內存裏。
——The WebSocket API W3C Candidate Recommendation
簡單的說:若是轉換成了 Blob 對象,就表明了一個不可變的文件對象或者原始數據。若是不須要修改或者不須要切分它,保留成 Blob 對象是一個好的選擇。若是要處理這段原始數據,放進內存裏面處理明顯會更加合適,那麼就請轉換成 arraybuffer 類型。
有一張來自 WebSocket.org 網站的測試,用 XHR 輪詢和 WebSocket 進行對比:
上圖中,咱們先看藍色的柱狀圖,是 Polling 輪詢消耗的流量,此次測試,HTTP 請求和響應頭信息開銷總共包括 871 字節。固然每次測試不一樣的請求,頭的開銷不一樣。此次測試都以 871 字節的請求來測試。
Use case A: 1,000 clients polling every second: Network throughput is (871 x 1,000) = 871,000 bytes = 6,968,000 bits per second (6.6 Mbps)
Use case B: 10,000 clients polling every second: Network throughput is (871 x 10,000) = 8,710,000 bytes = 69,680,000 bits per second (66 Mbps)
Use case C: 100,000 clients polling every 1 second: Network throughput is (871 x 100,000) = 87,100,000 bytes = 696,800,000 bits per second (665 Mbps)
而 Websocket 的 Frame 是 just two bytes of overhead instead of 871,僅僅用 2 個字節就代替了輪詢的 871 字節!
Use case A: 1,000 clients receive 1 message per second: Network throughput is (2 x 1,000) = 2,000 bytes = 16,000 bits per second (0.015 Mbps)
Use case B: 10,000 clients receive 1 message per second: Network throughput is (2 x 10,000) = 20,000 bytes = 160,000 bits per second (0.153 Mbps)
Use case C: 100,000 clients receive 1 message per second: Network throughput is (2 x 100,000) = 200,000 bytes = 1,600,000 bits per second (1.526 Mbps)
相同的每秒客戶端輪詢的次數,當次數高達 10W/s 的高頻率次數的時候,Polling 輪詢須要消耗 665Mbps,而 Websocket 僅僅只花費了 1.526Mbps,將近 435 倍!!
從結果上看, WebSocket 確實比輪詢效率和網速消耗都要好不少。
從使用場景來講,XHR、SSE、WebSocket 各有優缺點。
XHR 相對其餘兩種方式更加簡單,依靠 HTTP 完善的基礎設施,很容易實現。不過它不支持請求流,對相應流也不是完美支持(須要支持 Streams API 才能支持響應流)。傳輸數據格式方面,文本和二進制都支持,也支持壓縮。HTTP 對它的報文負責分幀。
SSE 也一樣不支持請求流,在進行一次握手之後,服務端就能夠以事件源協議把數據做爲響應流發給客戶端。SSE 只支持文本數據,不能支持二進制。由於 SSE 不是爲傳輸二進制而設計的,若是有必要,能夠把二進制對象編碼爲 base64 形式,而後再使用 SSE 進行傳輸。SSE 也支持壓縮,事件流負責對它進行分幀。
WebSocket 是目前惟一一個經過同一個 TCP 鏈接實現的全雙工的協議,請求流和響應流都完美支持。支持文本和二進制數據,自己自帶二進制分幀。在壓縮方面差一些,由於有些不支持,例如 x-webkit-deflate-frame 擴展,在筆者上文中距離的那個 ws 請求中服務器就沒有支持壓縮。
若是全部的網絡環境均可以支持 WebSocket 或者 SSE 固然是最好不過的了。可是這是不現實的,網絡環境變幻無窮,有些網絡可能就屏蔽了 WebSocket 通訊,或者用戶設備就不支持 WebSocket 協議,因而 XHR 也就有了用武之地。
若是客戶端不須要給服務端發消息,只須要不斷的實時更新,那麼考慮用 SSE 也是不錯的選擇。不過 SSE 目前在 IE 和 Edge 上支持的較差。WebSocket 在這方面比 SSE 強。
因此應該根據不一樣場景選擇不一樣的協議,各取所長。
Reference:
RFC6455
Server-Sent Events 教程
Comet:基於 HTTP 長鏈接的「服務器推」技術
WEB性能權威指南
What is Sec-WebSocket-Key for?
10.3. Attacks On Infrastructure (Masking)
Why are WebSockets masked?
How does websocket frame masking protect against cache poisoning?
What is the mask in a WebSocket frame?
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: github.com/halfrost/Ha…