一、WebSocket 協議背景
早期,在網站上推送訊息給使用者,只能通過輪詢的方式或 Comet 技術。輪詢就是瀏覽器每隔幾秒鐘向服務端傳送 HTTP 請求,然後服務端返回訊息給客戶端。
輪詢技術一般在瀏覽器上就是使用 setInerval 或 setTimeout
這種方式的缺點:
需要不斷的向服務端傳送 HTTP 請求,這種就比較浪費頻寬資源。而且傳送 HTTP 請求只能由客戶端發起,這也是早期 HTTP1.0/1.1 協議的一個缺點。它做不到由服務端向客戶端發起請求。
為了能實現客戶端和服務端的雙向通訊,經過多年發展於是 WebSocket 協議在 2008 年就誕生了。
它最初是在 HTML5 中引入的。經過多年發展後,該協議慢慢被多個瀏覽器支援,RFC 在 2011 年就把該協議作為一個國際標準,叫 rfc6455。
二、協議簡介
WebSocket 是一種支援雙向通訊的網路協議。
- 雙向通訊:客戶端(比如瀏覽器)可以向服務端傳送訊息,服務端也可以主動向客戶端傳送訊息。
這樣就實現了客戶端和服務端的雙向通訊,那麼上面所說的訊息推送就比較容易實現了。
原先的 HTTP1.0/1.1 只能是客戶端向服務端傳送訊息。
協議特點:
- 建立在 TCP 協議之上。
- WebSocket 協議是從 HTTP 協議升級而來。
- 與 HTTP 協議良好相容新。預設埠是 80 和 443,握手階段採用 HTTP 協議。
- 資料格式比較輕量,通訊效率高,效能開銷小。
- 可以傳送文字,也可以傳送二進位制資料。
- 沒有同源限制,客戶端可以與任意服務端通訊。
- 協議識別符號是 ws(如果加密,則為 wss),伺服器網址就是 URL。
- 可以支援擴充套件,定了擴充套件協議。
- 保持連線狀態,websocket 是一種有狀態的協議,通訊就可以省略部分狀態資訊。
- 實時性更強,因為是雙向通訊協議,所以服務端可以隨時向客戶端傳送資料。
三、HTTP 升級到 WebSocket 過程
WebSocket 協議建立複用了 HTTP 的握手請求過程。
客戶端通過 HTTP 請求與 WebSocket 服務端協商升級協議。協議完成後,後續的資料互動則遵循 WebSocket 的協議。
- 客戶端發起協議升級請求
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
說明:上面請求資訊忽略了 HTTP 的一些非必要頭部請求資訊,剔除多餘的干擾。
- Origin: http://127.0.0.1:3000 : 原始的協議和URL
- Connection: Upgrade:表示要升級協議了
- Upgrade: websocket:表示要升級到 WebSocket 協議;
- Sec-WebSocket-Version: 13:表示 WebSocket 的版本。如果服務端不支援該版本,需要返回一個
Sec-WebSocket-Versionheader
,裡面包含服務端支援的版本號 - Sec-WebSocket-Key:與後面服務端響應首部的 Sec-WebSocket-Accept 是配套的,提供基本的防護,比如惡意的連線,或者無意的連線
- 服務端響應協議升級
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
-
HTTP/1.1 101 Switching Protocols: 狀態碼 101 表示協議切換
-
Sec-WebSocket-Accept:根據客戶端請求首部的 Sec-WebSocket-Key 計算出來
將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
通過 SHA1 計算出摘要,並轉成 base64 字串。計算公式如下:
Base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
-
Connection:Upgrade:表示協議升級
-
Upgrade: websocket:升級到 websocket 協議
四、WebSocket 資料交換
資料幀格式
在 WebSocket 協議中,客戶端與服務端資料交換的最小資訊單位叫做幀(frame),由 1 個或多個幀按照次序組成一條完整的訊息(message)。
資料傳輸的格式是由 ABNF 來描述的。
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 ... |
+---------------------------------------------------------------+
(https://www.rfc-editor.org/rfc/rfc6455.html#section-5.2 Base Framing Protocol)
上面圖中名詞解釋:
名詞 | 說明 | 大小 |
---|---|---|
FIN | 如果是 1,表示這是訊息(message)的最後一個分片(fragment);如果是 0,表示不是是訊息(message)的最後一個分片(fragment) | 1 個位元 |
RSV1, RSV2, RSV3 | 一般情況下全為 0。當客戶端、服務端協商採用 WebSocket 擴充套件時,這三個標誌位可以非 0,且值的含義由擴充套件進行定義。如果出現非零的值,且並沒有採用 WebSocket 擴充套件,連線出錯 | 各佔 1 個位元 |
opcode | 操作程式碼,Opcode 的值決定了應該如何解析後續的資料載荷(data payload)。如果操作程式碼是不認識的,那麼接收端應該斷開連線(fail the connection) | 4 個位元 |
mask | 表示是否要對資料載荷進行掩碼操作。從客戶端向服務端傳送資料時,需要對資料進行掩碼操作;從服務端向客戶端傳送資料時,不需要對資料進行掩碼操作。 如果服務端接收到的資料沒有進行過掩碼操作,服務端需要斷開連線。 如果 Mask 是 1,那麼在 Masking-key 中會定義一個掩碼鍵(masking key),並用這個掩碼鍵來對資料載荷進行反掩碼。所有客戶端傳送到服務端的資料幀,Mask 都是 1。 |
1 個位元 |
Payload length | 資料載荷的長度,單位是位元組。假設數 Payload length === x,如果: x 為 0~126:資料的長度為 x 位元組。 x 為 126:後續 2 個位元組代表一個 16 位的無符號整數,該無符號整數的值為資料的長度。 x 為 127:後續 8 個位元組代表一個 64 位的無符號整數(最高位為 0),該無符號整數的值為資料的長度。 此外,如果 payload length 佔用了多個位元組的話,payload length 的二進位制表達採用網路序(big endian,重要的位在前)。 |
為 7 位,或 7+16 位,或 1+64 位。 |
Masking-key | 所有從客戶端傳送到服務端的資料幀,資料載荷都進行了掩碼操作,Mask 為 1,且攜帶了 4 位元組的 Masking-key。如果 Mask 為 0,則沒有 Masking-key。 備註:載荷資料的長度,不包括 mask key 的長度。 |
0 或 4 位元組(32 位 |
Payload data | 載荷資料:包括了擴充套件資料、應用資料。其中,擴充套件資料 x 位元組,應用資料 y 位元組。The "Payload data" is defined as "Extension data" concatenated with "Application data". 擴充套件資料:如果沒有協商使用擴充套件的話,擴充套件資料資料為 0 位元組。所有的擴充套件都必須宣告擴充套件資料的長度,或者可以如何計算出擴充套件資料的長度。此外,擴充套件如何使用必須在握手階段就協商好。如果擴充套件資料存在,那麼載荷資料長度必須將擴充套件資料的長度包含在內。 應用資料:任意的應用資料,在擴充套件資料之後(如果存在擴充套件資料),佔據了資料幀剩餘的位置。載荷資料長度 減去 擴充套件資料長度,就得到應用資料的長度。 |
(x+y) 位元組 |
表中 opcode 操作碼:
- %x0:表示一個延續幀(continuation frame)。當 Opcode 為 0 時,表示本次資料傳輸採用了資料分片,當前收到的資料幀為其中一個資料分片。
- %x1:表示這是一個文字幀(frame),text frame
- %x2:表示這是一個二進位制幀(frame),binary frame
- %x3-7:保留的操作程式碼,用於後續定義的非控制幀。
- %x8:表示連線斷開。connection close
- %x9:表示這是一個 ping 操作。a ping
- %xA:表示這是一個 pong 操作。a pong
- %xB-F:保留的操作程式碼,用於後續定義的控制幀。
資料幀另外一種表達方式
ws-frame = frame-fin ; 1 bit in length
frame-rsv1 ; 1 bit in length
frame-rsv2 ; 1 bit in length
frame-rsv3 ; 1 bit in length
frame-opcode ; 4 bits in length
frame-masked ; 1 bit in length
frame-payload-length ; either 7, 7+16,
; or 7+64 bits in
; length
[ frame-masking-key ] ; 32 bits in length
frame-payload-data ; n*8 bits in
; length, where
; n >= 0
frame-fin = %x0 ; more frames of this message follow
/ %x1 ; final frame of this message
; 1 bit in length
frame-rsv1 = %x0 / %x1
; 1 bit in length, MUST be 0 unless
; negotiated otherwise
frame-rsv2 = %x0 / %x1
; 1 bit in length, MUST be 0 unless
; negotiated otherwise
frame-rsv3 = %x0 / %x1
; 1 bit in length, MUST be 0 unless
; negotiated otherwise
frame-opcode = frame-opcode-non-control /
frame-opcode-control /
frame-opcode-cont
frame-opcode-cont = %x0 ; frame continuation
frame-opcode-non-control= %x1 ; text frame
/ %x2 ; binary frame
/ %x3-7
; 4 bits in length,
; reserved for further non-control frames
frame-opcode-control = %x8 ; connection close
/ %x9 ; ping
/ %xA ; pong
/ %xB-F ; reserved for further control
; frames
; 4 bits in length
frame-masked = %x0
; frame is not masked, no frame-masking-key
/ %x1
; frame is masked, frame-masking-key present
; 1 bit in length
frame-payload-length = ( %x00-7D )
/ ( %x7E frame-payload-length-16 )
/ ( %x7F frame-payload-length-63 )
; 7, 7+16, or 7+64 bits in length,
; respectively
frame-payload-length-16 = %x0000-FFFF ; 16 bits in length
frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
; 64 bits in length
frame-masking-key = 4( %x00-FF )
; present only if frame-masked is 1
; 32 bits in length
frame-payload-data = (frame-masked-extension-data
frame-masked-application-data)
; when frame-masked is 1
/ (frame-unmasked-extension-data
frame-unmasked-application-data)
; when frame-masked is 0
frame-masked-extension-data = *( %x00-FF )
; reserved for future extensibility
; n*8 bits in length, where n >= 0
frame-masked-application-data = *( %x00-FF )
; n*8 bits in length, where n >= 0
frame-unmasked-extension-data = *( %x00-FF )
; reserved for future extensibility
; n*8 bits in length, where n >= 0
frame-unmasked-application-data = *( %x00-FF )
; n*8 bits in length, where n >= 0
客戶端到服務端的掩碼演算法
https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3 Client-to-Server Masking
掩碼鍵(Masking-key)是由客戶端挑選出來的 32 位的隨機數。掩碼操作不會影響資料載荷的長度。掩碼、反掩碼操作都採用如下演算法:
舉例說明:
Octet i of the transformed data ("transformed-octet-i") is the XOR of octet i of the original data ("original-octet-i") with octet at index i modulo 4 of the masking key ("masking-key-octet-j"): j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
- original-octet-i:為原始資料的第 i 位元組。
- transformed-octet-i:為轉換後的資料的第 i 位元組。
- j:為i mod 4的結果。
- masking-key-octet-j:為 mask key 第 j 位元組。
演算法描述為: original-octet-i 與 masking-key-octet-j 異或後,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
資料分片
分片的目的:
- 有了訊息分片,傳送一個訊息的時候,就可以傳送未知大小的資訊。如果訊息不能被分片,那麼就不得不緩衝整個訊息,以便計算長度。而有了分片就可以選擇合適大小緩衝區來緩衝分片。
- 第二個目的是可以使用多路複用。
WebSocket 的每條訊息(message)可能被切分為多個資料幀。
當 WebSocket 的接收方接收到一個資料幀時,會根據 FIN 值來判斷是否收到訊息的最後一個資料幀。
從上圖可以看出,FIN = 1 時,表示為訊息的最後一個資料幀;FIN = 0 時,則不是訊息的最後一個資料幀,接收方還要繼續監聽接收剩餘資料幀。
opcode 表示資料傳輸的型別,0x01 表示文字型別的資料;0x02 表示二進位制型別的資料;0x00 比較特殊,表示延續幀(continuation frame),意思就是完整資料對應的資料幀還沒有接收完。
更多分片內容請看這裡:https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4
訊息分片example:
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
(具體例子見:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers)
五:怎麼保持連線
在第二小結中我們介紹了 websocket 的特點,其中有一個是保持連線狀態。
websocket 是建立在 tcp 之上,那也就是客戶端與服務端的 tcp 通道要保持連線不斷開。
怎麼保持呢?可以用心跳來實現。
其實 websocket 協議早就想到了,它的幀資料格式中有一個欄位 opcode,定義了 2 種型別操作, ping 和 pong,opcode 分別是 0x9、0xA
。
說明:對於長時間沒有資料往來的連線,如果依舊長時間保持連線的狀態,那麼就會浪費連線資源。
[完]