WebSocket 協議詳解


一、WebSocket 協議背景

早期,在網站上推送消息給用戶,只能通過輪詢的方式或 Comet 技術。輪詢就是瀏覽器每隔幾秒鍾向服務端發送 HTTP 請求,然后服務端返回消息給客戶端。

輪詢技術一般在瀏覽器上就是使用 setInerval 或 setTimeout

這種方式的缺點:

需要不斷的向服務端發送 HTTP 請求,這種就比較浪費帶寬資源。而且發送 HTTP 請求只能由客戶端發起,這也是早期 HTTP1.0/1.1 協議的一個缺點。它做不到由服務端向客戶端發起請求。

為了能實現客戶端和服務端的雙向通信,經過多年發展於是 WebSocket 協議在 2008 年就誕生了。

它最初是在 HTML5 中引入的。經過多年發展后,該協議慢慢被多個瀏覽器支持,RFC 在 2011 年就把該協議作為一個國際標准,叫 rfc6455

HTTP1.0 協議有個性質:它是一個無狀態的協議。客戶端每次發起一個請求,然后服務端回復一個請求,這次連接就會關閉。客戶端斷開與服務端的連接。而 WebSocket 協議不會關閉。

HTTP1.1 協議加了一個 Keep-Alive 的屬性,這個不能看作是一個長連接,它是對 TCP 連接的設置使用,是對這一次的連接存活最大時間設置。在這次存活時間內可以繼續發送 HTTP 消息。對於這個屬性每個服務器有一個默認超時時間,最終還是會關閉。而 WebSocket 協議是一個長連接,雖然它也是建立在 TCP 之上,但是它通過 HTTP 協議握手成功后,成為 WebSocket 協議,之后交互數據不需要發送 HTTP Header 和 HTTP Request 數據,WebSocket 有自己的數據編碼協議。WebSocket 協議本身也對探測服務端是否存活作了規定,詳細情況請看下面的內容。

簡單圖解對比下 2 個協議交互情況:

image

二、協議簡介

WebSocket 是一種支持雙向通信的網絡協議。

  • 雙向通信:客戶端(比如瀏覽器)可以向服務端發送消息,服務端也可以主動向客戶端發送消息。

這樣就實現了客戶端和服務端的雙向通信,那么上面所說的消息推送就比較容易實現了。

原先的 HTTP1.0/1.1 只能是客戶端向服務端發送消息。

協議特點:

  • 建立在 TCP 協議之上。
  • WebSocket 協議是從 HTTP 協議升級而來。
  • 與 HTTP 協議良好兼容新。默認端口是 80 和 443,握手階段采用 HTTP 協議。
  • 數據格式比較輕量,通信效率高,性能開銷小。
  • 可以發送文本,也可以發送二進制數據。
  • 沒有同源限制,客戶端可以與任意服務端通信。
  • 協議標識符是 ws(如果加密,則為 wss),服務器網址就是 URL。
  • 可以支持擴展,定了擴展協議。
  • 保持連接狀態,websocket 是一種有狀態的協議,通信就可以省略部分狀態信息。
  • 實時性更強,因為是雙向通信協議,所以服務端可以隨時向客戶端發送數據。

三、HTTP 升級到 WebSocket 過程

WebSocket 協議建立復用了 HTTP 的握手請求過程。

客戶端通過 HTTP 請求與 WebSocket 服務端協商升級協議。協議完成后,后續的數據交互則遵循 WebSocket 的協議。

  1. 客戶端發起協議升級請求
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 是配套的,提供基本的防護,比如惡意的連接,或者無意的連接
  1. 服務端響應協議升級
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

說明:對於長時間沒有數據往來的連接,如果依舊長時間保持連接的狀態,那么就會浪費連接資源。

[完]

六、參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM