WebSocket 協議 5~10 節

hsy0發表於2019-01-17

第 1~4 節在 WebSocket 協議 1~4 節;

5. 使用幀去組織資料

5.1 概覽

在 WebSocket 協議中,資料的傳輸使用一連串的幀。為了使得中介軟體不至於混淆(比如代理伺服器)以及為了第 10.3 節將討論安全原因,客戶端必須將要傳送到服務端的幀進行掩碼,掩碼將在第 5.3 節詳細討論。(注意,不管 WebSocket 有沒有執行在 TLS 之上,都必須有掩碼操作)服務端一旦接收到沒有進行掩碼的幀的話,必須關閉連線。這種情況下,服務端可以傳送一個關閉幀,包含一個狀態碼 1002(協議錯誤 protocol error),相關定義在 Section 7.4.1。服務端不必對傳送到客戶端的任何幀進行掩碼。如果客戶端接收到了服務端的掩碼後的幀,客戶端必須關閉連線。在這個情況下,客戶端可以向伺服器傳送關閉幀,包含狀態碼 1002(協議錯誤 protocol error),相關定義在 Section 7.4.1。(這些規則可能在將來技術說明中沒有嚴格要求)

基礎幀協議通過操作碼(opcode)定義了一個幀型別,一個有效負荷長度,以及特定的位置存放 “擴充套件資料 Extension data” 和 “應用資料 Application data”,擴充套件資料和應用資料合起來定義了 “有效負荷資料 Payload data”。某些數位和操作碼是保留的,為了將來的使用。

在客戶端和服務端完成了握手之後,以及任意一端傳送的關閉幀(在第 5.5.1 節介紹)之前,客戶端可以和服務端都可以在任何時間傳送資料幀。

基礎幀協議

這一節中將使用 ABNF 詳細定義資料傳輸的格式。(注意,和這文件中的其他 ABNF 不同,這一節中 ABNF 操作的是一組數位。每一組數位的長度將以註釋的形式存在。當資料在網路中傳輸時,最高有效位是在 ABNF 的最左邊(大端序))。下面的文字影象可以給出關於幀的一個高層概覽。如果下面的文字插圖和後的 ABNF 描述傳送衝突時,以插圖為準。

  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: 1 個數位(bit)

    標記這個幀是不是訊息中的最後一幀。第一個幀也可以是最後一幀。

  • RSV1,RSV2,RSV3: 各 1 個數位

    必須是 0,除非有擴充套件賦予了這些數位非 0 值的意義。如果接收到了一個非 0 的值並且沒有擴充套件賦予這些非 0 值的意義,那麼接收端需要標記連線為失敗。

  • 操作碼:4 個數位 定義瞭如何解釋 “有效負荷資料 Payload data”。如果接收到一個未知的操作碼,接收端必須標記 WebSocket 為失敗。定義瞭如下的操作碼:

    • %x0 表示這是一個繼續幀(continuation frame)
    • %x1 表示這是一個文字幀 (text frame)
    • %x2 表示這是一個二進位制幀 (binary frame)
    • %x3-7 為將來的非控制幀(non-control frame)而保留的
    • %x8 表示這是一個連線關閉幀 (connection close)
    • %x9 表示這是一個 ping 幀
    • %xA 表示這是一個 pong 幀
    • xB-F 為將來的控制幀(control frame)而保留的
  • 掩碼標識 Mask:1 個數位

    定義了 “有效負荷資料” 是否是被掩碼的。如果被設定為 1,那麼在 masking-key 部分將有一個掩碼鑰匙(masking key),並且使用這個掩碼鑰匙去將 “有效負荷資料” 進行反掩碼操作(第 5.3 節描述)。所有的由客戶端發往服務端的幀此數位都被設定成 1。

  • 有效負荷長度(Payload length): 7、7+16 或者 7+64 數位

    表示了 “有效負荷資料 Payload data” 的長度,以位元組為單位:如果是 0-125,那麼就直接表示了負荷長度。如果是 126,那麼接下來的兩個位元組表示的 16 位無符號整型數則是負荷長度。如果是 127,則接下來的 8 個位元組表示的 64 位無符號整型數則是負荷長度。表示長度的數值的位元組是按網路位元組序(network byte order 即大端序)表示的。注意在所有情況下,必須使用最小的負荷長度,比如,對於一個 124 位元組長度的字串,長度不可以編碼成 126,0,124。負荷長度是 “擴充套件資料 Extension data” 長度 + “應用資料Application data” 長度 。“擴充套件資料” 的長度可以是 0,那麼此時 “應用資料” 的長度就是負荷長度。

  • 掩碼鑰匙 Masking key:0 或者 4 個數位

    所有由客戶端發往服務端的幀中的內容都必須使用一個 32 位的值進行掩碼。這個欄位有值的時候(佔 4 個數位)僅當掩碼標識位設定成了 1,如果掩碼標識位設定為 0,則此欄位沒有值(佔 0 個數位)。對於進一步掩碼操作,見第 5.3 節。

  • 有效負荷資料 Payload data:(x+y) 位元組 byte

    “有效負荷資料” 的定義是 “擴充套件資料” 聯合 “應用資料”。

  • 擴充套件資料 Extension data: x 位元組

    “擴充套件資料是” 0 個位元組的,除非協商了一個擴充套件。任何的擴充套件都必須提供 “擴充套件資料” 的長度或者該長度應該如何計算,以及在握手階段如何使用 “擴充套件資料” 進行擴充套件協商。如果 “擴充套件資料” 存在,那麼它的長度被包含在了負荷長度中。

  • 應用資料 Application data: y位元組

    可以是任意的 “應用資料”,它在一個幀的範圍內緊接著 “擴充套件資料”。“應用資料” 的長度等於負荷長度減去 “擴充套件資料” 的長度

基礎幀協議通過接下來的 ABNF RFC5234 來定義其形式。一個重要的注意點就是下面的 ABNF 表示的是二進位制資料,而不是其表面上的字串。比如, %x0 和 %x1 各表示一個數位,數位上的值為 0 和 1,而不是表示的字元 “0” 和 “1” 的 ASCII 編碼。RFC5234 沒有定義 ABNF 的字元編碼。在這裡,ABNF 被特定了使用的是二進位制編碼,這裡二進位制編碼的意思就是每一個值都被編碼成具有特定數量的數位,具體的數量因不同的欄位而異。

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
複製程式碼

5.3 客戶端到服務端掩碼

一個被掩碼的幀需要將掩碼標識位(第 5.2 節定義)設定為 1。

掩碼鑰匙 masking key 整個都在幀中,就像第 5.2 節定義的。它用於對 “有效負荷資料” 進行掩碼操作,包括 “擴充套件資料” 和 “應用資料”。

掩碼鑰匙由客戶端隨機選取一個 32 位的值。在每次準備對幀進行掩碼操作時,客戶端必須選擇在可選的 32 位數值集合中選取一個新的掩碼鑰匙。掩碼鑰匙的值需要是不可被預測的;因此,掩碼鑰匙必須來源於一個具有很強保密性質的生成器,並且 伺服器/代理 不能夠輕易的預測到一連串的幀中使用的掩碼鑰匙。不可預測的掩碼鑰匙可以防止惡意程式在幀的傳輸過程中探測到掩碼鑰匙的內容。RFC4086 具體討論了為什麼對於一個安全性比較敏感的應用程式需要使用一個很強保密性質的生成器。

掩碼不會影響 “有效負載資料” 的長度。為了將掩碼後的資料進行反掩碼,或者倒過來,可以使用下面的演算法。同樣的演算法適用於不同方向發來的幀,比如,對於掩碼和反掩碼使用相同的步驟。

傳輸資料中的每 8 個數位的位元組 i (transformed-octet-i),生成方式是通過原資料中的每 8 個數位的位元組 i (original-octet-i)與以 i 與 4 取模後的數位為索引的掩碼鑰匙中的 8 為位元組 j(masking-key-octet-j) 進行異或(XOR)操作:

j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
複製程式碼

負載的長度不包括掩碼鑰匙的長度,它是 “有效負載資料 Payload data” 的長度,比如,位於掩碼鑰匙後的位元組的長度。

5.4 訊息碎片化

訊息碎片化的目的就是允許傳送那些在傳送時不知道其緩衝的長度的訊息。如果訊息不能被碎片化,那麼一端就必須將訊息整個地載入記憶體緩衝,這樣在傳送訊息前才可以計算出訊息的位元組長度。有了碎片化的機制,服務端或者中介軟體就可以選取其適用的記憶體緩衝長度,然後當緩衝滿了之後就傳送一個訊息碎片。

碎片機制帶來的另一個好處就是可以方便實現多路複用。沒有多路複用的話,就需要將一整個大的訊息放在一個邏輯通道中傳送,這樣會佔用整個輸出通道。多路複用需要可以將訊息分割成小的碎片,使這些小的碎片可以共享輸出通道。(注意多路複用的擴充套件在這片文件中並沒有進行描述)

除非運用了特定的擴充套件,否則幀是沒有特定的語義的。在客戶端和服務端協商了某個擴充套件,或者客戶端和服務端沒有協商擴充套件的情況下,中介軟體都有可能將幀進行 合併/分隔。也就是說,在客戶端和服務端沒有協商某個擴充套件時,雙方都不應該猜測幀與幀之間的邊界。注:這裡的某個擴充套件的意思就是賦予了幀特定的語義的擴充套件,比如多路複用擴充套件。

下面的規則解釋瞭如何進行碎片化:

  • 一個沒有被碎片化的訊息只包含一個幀,並且幀的 FIN 數位被設定為 1,且操作碼 opcode 不為 0。

  • 一個碎片化的訊息包含了一個 FIN 未被置為 0 的幀,且這個幀的 opcode 不為 0,在這個幀之後,將有 0 個或者多個 FIN 為 0 且 opcode 為 0 的幀,最後以一個 FIN 為 1 和 opcode 為 0 的幀結束。對於一個碎片化後的訊息,它的有效負荷就等於將碎片化後的幀的有效負荷按順序連線起來;不過當存在擴充套件時,這一點就不一定正確了,因為擴充套件可能會設定幀的 “擴充套件資料”。在沒有 “擴充套件資料” 的情況下,下面的例子演示了碎片化是如何工作的。

    例子:對於一個以三個幀傳送的文字訊息,其第一個幀的 opcode 是 0x1 並且 FIN 位是 0,第二個幀的 opcode 是 0x0 且 FIN 位是 0,第三個幀的 opcode 是 0x0 且 FIN 位是 1。

  • 控制幀(見第 5.5 節),可能會夾雜在訊息幀之間。控制幀是不能被碎片化的。

  • 訊息幀必須以其被髮送時的順序傳遞到接收端。

  • 不同訊息的訊息幀之間不可以相互夾雜,除非協商了一個定義瞭如何解釋這種夾雜行為的擴充套件。

  • 傳送端可以建立任意大小的非控制幀。

  • 客戶端和服務端必須支援傳送和接受碎片化或者非碎片化的訊息。

  • 一個控制幀是不可以被碎片化的,中介軟體必須不可以試圖將控制幀進行碎片化。

  • 如果幀中使用了 RSV 數位,但是中介軟體不理解其中的任意的 RSV 數位 的值時,它必須不可以改變訊息的原有的碎片化幀。

  • 在中介軟體不能確定客戶端和服務端進行了哪些擴充套件協商的情況下,中介軟體必須不可以修改原有的碎片化幀。

  • 最後,組成訊息的所有幀都是相同的資料型別,在第一個幀中的 opcode 中指明。因為控制幀不能被碎片化,組成訊息的碎片型別必須是文字、二進位制、或者其他的保留型別。

注意:如果控制幀不能夾雜在訊息幀的話,那麼將導致 ping 的結果產生延遲,比如在處理了一個非常長的訊息後才響應 ping 控制幀時。因此,要求在處理訊息幀的期間可以響應控制幀。

重點注意:在沒有擴充套件的情況下,接收端為了處理訊息不是非得緩衝所有的幀。比如如果使用了 流API (streaming API),資料幀可以直接傳遞給應用層。不過這樣假設並不一定在所有的擴充套件中都適用。

5.5 控制幀

控制幀是通過它的 opcode 的最高有效位是 1 去確定的。當前已經定義了的控制幀包括 0x8 (close)0x9 (Ping)0xA (Pong)。操作碼 0xB-0xF 是為將來的控制幀保留的,目前尚未定義。

控制幀是為了在 WebSocket 中通訊連線狀態。控制幀可以夾雜在訊息幀之間傳送。

所有的控制幀的負載長度都必須是 125 位元組,並且不能被碎片化。

關閉幀

關閉幀的操作碼 opcode 是 0x8

關閉幀可以包含訊息體(通過幀的 “應用資料” 部分)去表示關閉的原因,比如一端正在關閉服務,一端接收到的幀過大,或者一端接收到了不遵循格式的幀。如果有訊息體的話,訊息體的前兩個位元組必須是無符號的整型數(採用網路位元組序),以此整型數去表示狀態碼 /code/ 定義在第 7.4 節。在兩個位元組的無符號整型數之後,可以跟上以 UTF-8 編碼的資料表示 /reason/,/reason/ 資料的具體解釋方式此文件並沒有定義。並且 /reason/ 的內容不一定是人類可讀的資料,只要是有利於發起連線的指令碼進行除錯就可以。因為 /reason/ 並不一定就是人類可讀的,所以客戶端必須不將此內容展示給終端使用者。

客戶端傳送的每一個幀都必須按照第 5.3 節中的內容進行掩碼。

應用程式在傳送了關閉幀之後就不可以再傳送其他資料幀了。

如果接收到關閉幀的一端之前沒有傳送過關閉幀的話,那麼它必須傳送一個關閉幀作為響應。(當傳送一個關閉幀作為響應的時候,傳送端通常在作為響應的關閉幀中採用和其接收到的關閉幀相同的狀態碼)。並且響應必須儘快的傳送。一端可以延遲關閉幀的傳送,比如一個重要的訊息已經傳送了一半,那麼可以在訊息的剩餘部分傳送完之後再傳送關閉幀。但是作為首先傳送了關閉幀,並在等待另一端進行關閉響應的那一端來說,並不一定保證其會繼續處理資料內容。

在傳送和接收到了關閉幀之後,一端就可以認為 WebSocket 連線已經關閉,並且必須關閉底層相關的 TCP 連線。如果是服務端首先傳送了關閉幀,那麼在接收到客戶端返回的關閉幀之後,服務端必須立即關閉底層相關的 TCP 連線;但是如果是客戶端首先傳送了關閉幀,並接收到了服務端返回的關閉幀之後,可以選擇其認為合適的時間關閉連線,比如,在一段時間內沒有接收到服務端的 TCP 關閉握手。

如果客戶端和服務端同時傳送了關閉訊息,那麼它們兩端都將會接收到來自對方的關閉訊息,那麼它們就可以認為 WebSocket 連線已經關閉,並且關閉底層相關的 TCP 連線。

5.5.2 Ping

Ping 幀的操作碼是 0x9

Ping 幀也可以有 “應用資料”

一旦接收到了 Ping 幀,接收到的一端必須傳送一個 Pong 幀作為響應,除非它已經接收到了關閉幀。響應的一端必須儘快的做出響應。Pong 幀定義在第 5.5.3 節。

一端可以在連線建立之後,到連線關閉之前的任意時間點傳送 Ping 幀。

注意:Ping 幀的目的可以是保持連線(keepalive)或者是驗證服務端是否還是有響應的。

5.5.3 Pong

Pong 幀的操作碼是 0xA

第 5.5.2 節的要求同時適用於 Ping 幀和 Pong 幀。

Pong 幀的 “應用資料” 中的內容必須和其響應的 Ping 幀中的 “應用資料” 的內容相同。

如果一端接收到了 Ping 幀並且在沒有來得及響應的時候又接收到了新的 Ping 幀,那麼響應端可以選擇最近的 Ping 幀作為響應的物件。

Pong 幀可以在未被主動請求的情況下傳送給對方。這被認為是單向的心跳包。單向心跳包是得不到響應的。

5.6 資料幀

資料幀(比如,非控制幀)是通過操作碼的最高有效位是 0 來確定的。當前已經定義的資料幀包括 0x1 (文字)0x2 (二進位制)。操作碼 0x3-0x7 是為了將來的非控制幀的使用而保留的。

資料幀承載了 “應用層 application-layer” 或者 “擴充套件層 extension-layer” 的資料。操作碼決定了資料的表現形式。

  • 文字 Text

    “有效負載資料 Payload data” 是以 UTF-8 編碼的文字。注意,作為整個文字訊息的一部分的部分文字幀可能包含了部分的 UTF-8 序列;但是整個的訊息的內容必須是一個有效的 UTF-8 序列。對於無效的 UTF-8 訊息的處理在第 8.1 節中描述。

  • 二進位制 Binary

    “有效負荷資料 Payload data” 是僅由應用層來決定的任意二進位制內容。

5.7 例子

  • 一個單個幀的沒有進行掩碼的文字訊息

    • 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f(訊息內容為 “Hello”)
  • 一個單個幀的掩碼後的訊息

    • 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58(訊息內容為 “ Hello”)
  • 一個碎片化的沒有掩碼的文字訊息

    • 0x01 0x03 0x48 0x65 0x6c(訊息內容為 “Hel”)
    • 0x80 0x02 0x6c 0x6f(訊息內容為 “lo”)
  • 沒有掩碼的 Ping 請求和其掩碼後的響應

    • 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f(訊息體部分為 “Hello”,只不過是例子,可以為任意內容)
    • x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58(訊息體部分也是 “Hello”,和其響應的 Ping 相同)
  • 256 個位元組的二進位制訊息,使用單個未掩碼的幀

    • 0x82 0x7E 0x0100 [256 個位元組的二進位制資料]
  • 64 Kb 的二進位制訊息,使用單個未掩碼的幀

    • 0x82 0x7F 0x0000000000010000 [65536 個位元組的二進位制資料]

5.8 擴充套件性

協議被設計為允許擴充套件,擴充套件可以在基礎協議的功能上新增更多的功能。通訊雙方必須在握手期間完成擴充套件的協商。在這份技術說明中,為擴充套件提供使用的部分為:操作碼 0x3 到 0x7、以及 0xB 到 0xF,“擴充套件資料 Extension data” 欄位,frame-rsv1、frame-rsv2、frame-rsv3 這三個位於幀頭部的數位。關於擴充套件協商的詳細在第 9.1 節中討論。下面的列表是關於擴充套件的預期使用形式,不過它既不完整也不規範:

  • “擴充套件資料” 可以放在 “應用資料” 之間,它們共同組成 “有效負荷資料”
  • 保留的數位可以為每一幀按需分配
  • 保留的操作碼可以被定義
  • 如果需要更多的操作碼的話,可以佔用保留數位以為操作碼提供更多的數位空間
  • 佔用保留數位,或者在 “有效負荷資料” 之外定義 “擴充套件” 的操作碼,以此獲得更大的操作碼錶示空間,或者更多的區別每一幀的數位

6 傳送和接收資料

6.1 傳送資料

為了在 WebSocket 連線上傳送由 /data/ 組成的 WebSocket 訊息,傳送端必須按下的步驟去執行:

  1. 傳送端必須確定當前的 WebSocket 連線的狀態是 OPEN(見 第 4.1 和 4.2 節)。在任何時間點,如果連線的狀態改變了,那麼傳送端必須終止下面的步驟。

  2. 傳送端必須使用 WebSocket 幀將 /data/ 按第 5.2 節中描述的形式包裹起來。如果資料太大,或者在傳送時不能整個地獲取需傳送資料,那麼傳送端可以按照第 5.4 節中描述的,將資料分割成一連串的幀進行傳送。

  3. 包含資料的第一個幀的操作碼必須設定為適當的資料型別,以便接收端可以確定用文字還是二進位制來解釋其接收到的資料,資料型別定義在第 5.2 節。

  4. 在訊息的最後一個包含資料的幀中必須將其 FIN 設定為 1,相關定義在第 5.2 節。

  5. 如果資料是由客戶端傳送的,那麼資料在傳送前必須按照第 5.2 中定義的方式進行掩碼。

  6. 如果連線中進行了擴充套件協商,那麼額外的擴充套件相關的處理將會應用到幀上。

  7. 幀必須經由 WebSocket 底層相關的網路連線傳送。

6.2 接收資料

為了接收 WebSocket 資料,接收端必須監聽底層相關的網路連線。接收到的資料必須按照第 5.2 節中定義的格式進行解析。如果接收到的是一個控制幀,那麼必須按照第 5.5 節中的定義去處理。一旦接收到第 5.6 節中定義的資料幀,接收端必須注意資料幀的型別 /type/,這點根據幀的 opcode,定義在第 5.2 節。“應用資料 Application data” 被定義為訊息的資料 /data/。如果幀是一個沒有被碎片化的幀,定義在第 5.4 節,那麼就說明一個訊息已經被完全接收了,即知道了其型別 /type/ 和資料 /data/。如果幀是碎片化訊息的一部分,那麼其隨後的幀的 “應用資料” 連線在一起組成訊息的資料 /data/。當最後一個碎片化的幀被接收時,也就是幀的 FIN 位為 1 時,表明一個 WebSocket 訊息已經被完全接收了,其資料 /data/ 就是所有相關碎片化的幀的 “應用資料” 連線到一起的值,而 /type/ 就是第一個或者其他組成訊息的碎片化幀的操作碼。之後的幀必須被解釋為屬於一個新的訊息。

擴充套件(第 9 節)可能會更改資料被讀取的方式,特別是如何界定訊息之間的邊界。擴充套件在有效負荷中的 “應用資料” 之前新增的 “擴充套件資料” 也可能會修改 “應用資料” 的內容(比如進行了壓縮)。

服務端必須將來自客戶端的幀進行反掩碼,操作定義在第 5.3 節。

7 關閉連線

7.1 定義

7.1.1 關閉 WebSocket 連線

為了關閉 WebSocket 連線,一端可以關閉底層的 TCP 連線。一端在關閉連線的時候必須乾淨的關閉,比如 TLS 會話,儘可能的丟棄所有已經接收但是尚未處理的位元組。一端可以在需要的時候以任意的理由去關閉連線,比如在收到攻擊時。

底層的 TCP 連線,在一般情況下應該由服務端先進行關閉,而客戶端則需要在一段時間內等待服務端的 TCP 關閉,如果超過了客戶端的等待時間,客戶端則可以關閉 TCP 連線。

一個以使用 Berkeley sockets 的 C 語言的例子演示如何幹淨的關閉連線:首先一端需要呼叫對 socket 呼叫 shutdown() 函式,並以 SHUT_WR 為函式的引數,然後呼叫 recv() 函式直到其返回值為 0,最後呼叫 close() 函式關閉 socket。

7.1.2 開始 WebSocket 關閉握手

為了關閉開始 WebSocket 關閉握手,需要關閉的一端必須選擇一個狀態碼(第 7.4 節)/code/ 和可選的關閉原因 (第 7.1.6 節)/reason/,然後按照第 5.5.1 節中的描述傳送一個關閉幀,幀的狀態碼以及原因就是之前選取的 /code/ 和 /reason/。一旦一端傳送並接收到了關閉幀,就可以按照第 7.1.1 節中定義的內容關閉 WebSocket 連線。

7.1.3 WebSocket 關閉握手已經開始

一旦任何一端傳送或者接收到關閉幀,就表明 WebSocket 關閉握手已經開始,並且 WebSocket 連線的狀態變為 CLOSING。

7.1.4 WebSocket 連線已經關閉

當底層的 TCP 連線已經關閉時,就表明 WebSocket 連線已經關閉,並且 WebSocket 連線的狀態變為 CLOSED。如果 TCP 連線在 WebSocket 關閉握手完成之後才進行關閉,就說明關閉是乾淨(cleanly)的。否則的話就說明 WebSocket 連線已經關閉,但不是乾淨地(cleanly)。

7.1.5 WebSocket 連線關閉程式碼

與第 5.5.1 節和第 7.4 節中定義的一樣,一個關閉幀可以包含一個關閉狀態碼,以此表明關閉的原因。WebSocket 的關閉可以由任意一端發起,或者同時發起。返回的關閉幀的狀態碼與接收到的關閉幀的狀態碼相同。如果關閉幀沒有包含狀態碼,那麼就認為其狀態碼是 1005。如果一端發現 WebSocket 連線已經關閉但是沒有收到關閉幀,那麼就認為此時的狀態碼是 1006。

注意:兩端的關閉幀的狀態碼不必相同。比如,如果遠端的一端傳送了一個關閉幀,但是本地的應用程式還沒有讀取位於接收快取中的關閉幀,並且應用程式也傳送了一個關閉幀,那麼兩端都將會達到 “傳送了” 和 “接收到” 關閉幀的狀態。每一端都會看到來自另一端的具有不同狀態碼的關閉幀。因此,兩端可以不必要求傳送和接收到的關閉幀的狀態碼是相同的,這樣兩端就可以大概同時進行 WebSocket 連線的關閉了。

7.1.6 WebSocket 連線關閉原因

與第 5.5.1 節和第 7.4 節中定義的相同,關閉幀可以包含一個狀態碼,並且在狀態碼之後可以跟隨以 UTF-8 編碼的資料,具體這些資料應該如何被解釋依賴於對端的實現,本協議並沒有明確的定義。每一端都可以發起 WebSocket 關閉,或者同時發起。WebSocket 連線關閉原因的定義就是跟隨在關閉狀態碼之後的以 UTF-8 編碼的資料,響應的關閉幀中的 /reason/ 內容來自請求的關閉幀中 /reason/,並與之相同。如果沒有定義這些 UTF-8 資料,那麼關閉的原因就是空字串。

注意:遵循第 7.1.5 節中描述的邏輯,兩端不必要求傳送和接收的關閉幀的 /reason/ 是相同的。

7.1.7 將 WebSocket 連線標記為失敗

因為某種演算法或者特定的需求使得一端需要將 WebSocket 連線表示位失敗。為了達到這個目的,客戶端必須關閉 WebSocket 連線,並且可以將問題以適當的方式反饋給使用者(對於開發者來說可能非常重要)。同樣的,服務端為了達到這個目的也必須關閉 WebSocket 連線,並且使用日誌記錄下發生的問題。

如果在希望將 WebSocket 連線標記為失敗之前,WebSocket 連線已經建立的話,那麼一端在關閉 WebSocket 連線之前應該傳送關閉幀,並帶上適當的狀態碼(第 7.4 節)。如果一端認為另一端不可能有能力去接受和處理關閉幀時,比如 WebSocket 連線尚未建立,那麼可以省略傳送關閉幀的過程。如果一端標記了 WebSocket 連線為失敗的,那麼它不可以再接受和處理來自遠端的資料(包括響應一個關閉幀)。

除了上面的情況或者應用層需要(比如,使用了 WebSocket API 的指令碼),客戶端不應該關閉連線。

7.2 異常關閉

7.2.1 客戶端發起的關閉

因為某種演算法或者在開始握手的實際運作過程中,需要標記 WebSocket 連線為失敗。為了達到這個目的,客戶端必須按照第 7.1.7 節中描述的內容將 WebSocket 連線標記為失敗。

如果在任意時間點,底層的傳輸層連線傳送了丟失,那麼客戶端必須將 WebSocket 連線標記為失敗。

除了上面的情況或者特定的應用層需要(比如,使用了 WebSocket API 的指令碼),客戶端不可以關閉連線。

7.2.2 服務端發起的關閉

因為某種演算法或者在握手期間終止 WebSocket 連線,服務端必須按照第 7.1.1 節的描述去關閉 WebSocket 連線。

7.2.3 從異常中恢復

異常關閉可能有很多的原因引起。比如一個短暫的錯誤導致的異常關閉,在這種情況下,通過重連可以使用一個沒有問題的連線,然後繼續正常的操作。然而異常也可能是一個由非短暫的問題引起的,如果所有釋出的客戶端在經歷了一個異常關閉之後,立刻不斷的試圖向伺服器發起重連,如果有大量的客戶端在試圖重連的話,那麼伺服器將有可能面對拒絕服務攻擊(denial-of-service attack)。這樣造成的結果就是服務將無法在短期內恢復。

為了防止這個問題出現,客戶端應該在發生了異常關閉之後進行重連時使用一些補償機制。

第一個重連應該延遲,在一個隨機時間後進行。產生用於延遲的隨機時間的引數由客戶端去決定,初始的重連延遲可以在 0 到 5 秒之間隨機選取。客戶端可以根據實際應用的情況去決定具體的隨機值。

如果第一次的重連失敗,那麼接下來的重連應該使用一個更長的延遲,可以使用一些已有的方法,比如 truncated binary exponential backoff

7.3 連線的一般關閉

服務端可以在其需求的時候對 WebSocket 連線進行關閉。客戶端不應該隨意的關閉 WebSocket 連線。當需要進行關閉的時候,需要遵循第 7.1.2 節中定義的過程。

狀態碼

當關閉已經建立的連線時(比如在握手完成後傳送關閉幀),請求關閉的一端必須表明關閉的原因。如何解釋原因,以及對於原因應該採取什麼動作,都是這份技術說明中沒有定義的。這份技術說明中定義了一組預定義的狀態碼,以及擴充套件、框架、最終應用程式使用的狀態碼範圍。狀態碼相關的原因 /reason/ 在關閉幀中是可選的。

7.4.1 已定義的狀態碼

當傳送關閉幀的時候,一端可以採用下面的預定義的狀態碼:

  • 1000

    • 1000 表明這是一個正常的關閉,表示連線已經圓滿完成了其工作。
  • 1001

    • 1001 表明一端是即將關閉的,比如服務端將關閉或者瀏覽器跳轉到了其他頁面。
  • 1002

    • 1002 表明一端正在因為協議錯誤而關閉連線。
  • 1003

    • 1003 表明一端因為接收到了無法受理的資料而關閉連線(比如只能處理文字的一端接收到了一個二進位制的訊息)
  • 1004

    • 保留的。特定的含義會在以後定義。
  • 1005

    • 1005 是一個保留值,並且必須不可以作為關閉幀的狀態碼。它的存在意義就是應用程式可以使用其表示幀中沒有包含狀態碼。
  • 1006

    • 1006 這是一個保留值,並且必須不可以作為關閉幀的狀態碼。它的存在意義就是如果連線非正常關閉而應用程式需要一個狀態碼時,可以使用這個值。
  • 1007

    • 1007 表明一端接收到的訊息內容與之標記的型別不符而需要關閉連線(比如文字訊息中出現了非 UTF-8 的內容)
  • 1008

    • 1008 表明了一端接收到的訊息內容違反了其接收訊息的策略而需要關閉連線。這是一個通用的狀態碼,可以在找不到其他合適的狀態碼時使用此狀態碼,或者希望隱藏具體與接收端的哪些策略不符時(比如 1003 和 1009)。
  • 1009

    • 1009 表明一端接收了非常大的資料而其無法處理時需要關閉連線。
  • 1010

    • 1010 表明了客戶端希望服務端協商一個或多個擴充套件,但是服務端在返回的握手資訊中包含協商資訊。擴充套件的列表必須出現在其傳送給服務端的關閉幀的 /reason/ 中。注意這個狀態碼並不被服務端使用。
  • 1011

    • 1011 表明了一端遇到了異常情況使得其無法完成請求而需要關閉連線。
  • 1015

    • 1015 是一個保留值,並且它必須不可以作為狀態碼在關閉幀中使用,在應用程式需要一個狀態碼去表明執行 TLS 握手失敗時,可以使用它(比如服務端的證照沒有通過驗證)。

7.4.2 保留的狀態碼區間

  • 0-999

    在 0-999 之間的狀態碼是不被使用的

  • 1000-2999

    在 1000-2999 之間的狀態碼是本協議保留的,並且擴充套件可以在其公開的技術說明中使用。

  • 3000-3999

    在 3000-3999 之間的狀態碼是為庫、框架、應用程式保留的。這些狀態碼可以直接通過 IANA 進行註冊。狀態碼的具體表示意義為在本協議中定義。

  • 4000-4999

    在 4000-4999 之間的狀態碼是為了私有使用而保留的,因此不可以被註冊。相應狀態碼的使用及其意義可以在 WebSocket 應用程式之間事先商議好。這些狀態碼的意義在本協議中未定義。

錯誤處理

8.1 處理編碼錯誤的 UTF-8 資料

當一端在以 UTF-8 編碼解釋接收到的資料,但是發現其實不是有效的 UTF-8 編碼時,一端必須標記 WebSocket 連線為失敗。這個規則適用於握手以及隨後的資料傳輸階段。

9. 擴充套件

在這份技術說明中,客戶端是可以請求使用擴充套件的,並且服務端可以受理客戶端請求的擴充套件中的一個或者所有擴充套件。服務端響應的擴充套件必須屬於客戶端請求的擴充套件列表。如果擴充套件協商中包含了相應的擴充套件引數,那麼引數的選擇和應用必須按照具體的擴充套件的技術說明中描述的方式。

9.1 擴充套件協商

客戶端通過包含 |Sec-WebSocket-Extensions| 去請求擴充套件,此欄位名遵循普通的 HTTP 頭欄位的規則 [RFC2616], Section 4.2](tools.ietf.org/html/rfc261…),其內容的形式經由下面的 ABNF RFC2616 表示式給出定義。注意,著一節中的 ABNF 語法規則遵循 RFC2616,包括了 “隱含的 *LWS 規則”。如果一端接收到的值不符合下面的 ABNF,那麼接收端必須立刻標記 WebSocket 連線為失敗。

Sec-WebSocket-Extensions = extension-list
extension-list = 1#extension
extension = extension-token *( ";" extension-param )
extension-token = registered-token
registered-token = token
extension-param = token [ "=" (token | quoted-string) ]
    ;When using the quoted-string syntax variant, the value
    after quoted-string unescaping MUST conform to the
    ;'token' ABNF.
複製程式碼

注意,和其他的 HTTP 頭欄位一樣,這些頭欄位也可以分隔成多行,或者由多行合併。因此下面的兩個是等價的:

Sec-WebSocket-Extensions: foo
Sec-WebSocket-Extensions: bar; baz=2
複製程式碼

等價於

Sec-WebSocket-Extensions: foo, bar; baz=2
複製程式碼

任何的 extension-token 比如使用已註冊的 token(見第 11.4 節)。為擴充套件提供的引數比如遵循相應擴充套件的定義。注意,客戶端只是提供它希望使用的擴充套件,除非服務端從中選擇了一個或多個表明其也希望使用,否則客戶端不可以私自的使用。

注意,擴充套件的在列表中順序是重要的。多個擴充套件之間的互動方式,可能在具體定義了擴充套件的文件中進行了描述。如果沒有定義描述了多個擴充套件之間應該如何互動,那麼排在靠前位置的擴充套件應該最先被考慮使用。在服務端響應中列出的擴充套件將是連線實際將會使用的擴充套件。擴充套件之間修改資料或者幀的操作順序,應該假設和擴充套件在服務端握手響應中的擴充套件列表中出現的順序相同。

比如,如果有兩個擴充套件 “foo” 和 “bar”,並且在服務端傳送的 |Sec-WebSocket-Extensions| 的值為 “foo, bar”,那麼對資料的操作整體來看就是 bar(foo(data)),對於資料或者幀的修改過程看起來像是 “棧 stack”。

一個關於受理擴充套件頭欄位的非規範化的例子:

Sec-WebSocket-Extensions: deflate-stream
Sec-WebSocket-Extensions: mux; max-channels=4; flow-control, deflate-stream
Sec-WebSocket-Extensions: private-extension
複製程式碼

服務端受理一個或者多個擴充套件,通過 |Sec-WebSocket-Extensions| 頭欄位包含一個或者多個來自客戶端請求中的擴充套件。擴充套件引數的解釋,以及服務端如何正確響應客戶端的引數,都在各自擴充套件的定義中描述。

9.2 已知的擴充套件

擴充套件提供了一個外掛的機制,以提供額外的協議功能。這份文件沒有定義任何的擴充套件,但是實現時可以使用獨立定義在其他文件的擴充套件。

10. 安全考慮

這一節描述了一些 WebSocket 協議在使用中需要注意的問題。問題被分成了不同的小節。

10.1 非瀏覽器客戶端

WebSocket 可以抵禦執行在被信任的應用程式(比如瀏覽器)中的惡意 Javascript 指令碼,比如,通過檢查 |Origin| 頭欄位。不過當面對具有更多功能的客戶端時就不能採用此方法了(檢查 |Origin| 頭欄位)。

這份協議可以適用於執行在 web 頁面中的指令碼,也可以直接被主機所使用。那些主機可以因為自身的目的傳送一個偽造的 |Origin| 頭欄位,以此迷惑伺服器。服務端因此伺服器不應該資訊任何的客戶端輸入。

例子:如果服務端使用了客戶端的 SQL 查詢語句,所有的輸入文字在提交到 SQL 伺服器之前必須進行跳脫操作(escape),減少服務端被 SQL 注入的風險。

10.2 Origin 的考慮

服務端不必接收來自網際網路的所有請求,可以僅僅受理包含特定源的請求。如果請求的源不符合服務端的接收範圍,那麼服務端應該在對客戶端的握手響應中包含狀態碼 “403 Forbidden”。

|Origin| 的作用是可以預防來自執行在可信任的客戶端中的 Javascript 的惡意攻擊。客戶端本身可以連線到伺服器,通過 |Origin| 的機制決定是否將通訊的許可權交給 Javascript 應用。這麼做的目的不是針對非瀏覽器的連線,而是杜絕執行在被信任的瀏覽器可能的潛在威脅 - Javascript 指令碼偽造 WebSocket 連線。

10.3 針對基礎設施的攻擊

除了一端的終節點會收到攻擊之外,基礎設施中的其他部分,比如代理,也可能會收到攻擊。

針對代理的攻擊實際上是針對那些在實現上有缺陷的代理伺服器,有缺陷的代理伺服器的工作方式類似:

  1. 首先你通過 Socket 的方式和 IP 為 2.2.2.2 的伺服器建立連線,連線是經由代理的。
  2. 在連線建立完成後,你傳送了類似下面的文字:
GET /script.js HTTP/1.1
Host: target.com
複製程式碼

(更多更深入的描述見 Talking

這段文字首先是傳到代理伺服器的,代理伺服器正確的工作方式是應該將此文字直接轉發給 IP 為 2.2.2.2 的伺服器。可是,有缺陷的代理會認為這是一個 HTTP 請求,需要採用 HTTP 代理的機制,進而訪問了 target.com 並獲取了 /script.js。

這種錯誤的工作方式並不是你所期望的。但是不可能一一檢查網路中所有可能存在此問題的代理,所以最好的方式就是將客戶端傳送的內容都進行掩碼操作,這樣就不會出現那種讓有缺陷的代理伺服器產生迷惑的內容了。

10.4 特定實現的限制

在協議實現中,可能會有一些客觀的限制,比如特定平臺的限制,這些限制與幀的大小或者所有幀合併後的訊息的大小相關(比如,惡意的終節點可以通過傳送單個很大的幀(2**60),或者傳送很多很小的幀但是這些幀組成的訊息非常大,以此來耗盡另一方的資源)。因此在實現中,一端應該強制使用一些限制,限制幀的大小,以及許多幀最後組成的訊息的大小。

10.5 WebSocket 客戶端認證

這份協議沒有規定任何方式可被用於服務端在握手期間對客戶端進行認證。WebSocket 服務端可以使用任何在普通 HTTP 服務端中使用的對客戶端的認證方式,比如 cookie,HTTP 認證,或者 TLS 認證。

10.6 連線的保密性和完整性

WebSocket 協議的保密性和完整性是通過將其執行在 TLS 上達到的。WebSocket 實現必須支援 TLS 並在需要的時候使用它。

對於使用 TLS 的連線,TLS 提供的大部分好處都是基於 TLS 握手階段協商的演算法的強度。比如,一些 TLS 加密演算法沒有保證資訊的保密性。為了使安全達到合適的程度,客戶端應該只使用高強度的 TLS 演算法。W3C.REC-wsc-ui-20100812 具體討論了什麼是高強度的 TLS 演算法,RFC5246 的附錄 A.5 和 附錄 D.3 提供了一些指導意見。

10.7 處理錯誤資料

客戶端和服務端接收的資料都必須經過驗證。如果在任意時間點上,一端接收到了無法理解的或者違反標準的資料,或者發現了不安全的資料,或者在握手期間接收到了非期望的值(比如錯誤的路徑或者源),則可以關閉 TCP 連線。如果接收到無效資料時 WebSocket 連線已經建立,那麼一端在關閉 WebSocket 連線之前,應該向另一端傳送一個帶有適當的狀態碼的關閉幀。通過使用具有適當狀態碼的關閉幀,可以幫助定位問題。如果在握手期間接收到了無效的資料,那麼服務端應該返回適當的 HTTP 狀態碼 RFC2616

一個典型的安全問題就是當傳送的資料採用了錯誤的編碼時。這份協議中規定了,文字資料包含的必須是 UTF-8 編碼的資料。應用程式需要通過一個長度去確定幀序列的傳輸何時結束,但是這個長度往往在事先不好確定(碎片化的訊息)。這就給檢查文字訊息是否採用了正確的編碼帶來了困難,因為必須等到訊息的所有碎片幀都接受完成了,才可以檢查它們組成的訊息的編碼是否正確。不過如果不檢查編碼的話,就不能確保接收的資料可以被正確的解釋,並會帶來潛在的安全問題。

10.8 在 WebSocket 握手中採用 SHA-1

這份文件中描述的 WebSocket 握手並不依賴於 SHA-1 演算法的安全屬性,比如抗碰撞性或者在 RFC4270 中描述的 second pre-image attack。

11~14

略,見 原文

相關文章