【譯】WebSocket協議第五章——資料幀(Data Framing)

黃Java發表於2019-01-07

概述

本文為WebSocket協議的第五章,本文翻譯的主要內容為WebSocket傳輸的資料相關內容。

有興趣瞭解該文件之前幾章內容的同學可以見:

資料幀(協議正文)

5.1 概覽

在WebSocket協議中,資料是通過一系列資料幀來進行傳輸的。為了避免由於網路中介(例如一些攔截代理)或者一些在第10.3節討論的安全原因,客戶端必須在它傳送到伺服器的所有幀中新增掩碼(Mask)(具體細節見5.3節)。(注意:無論WebSocket協議是否使用了TLS,幀都需要新增掩碼)。服務端收到沒有新增掩碼的資料幀以後,必須立即關閉連線。在這種情況下,服務端可以傳送一個在7.4.1節定義的狀態碼為1002(協議錯誤)的關閉幀。服務端禁止在傳送資料幀給客戶端時新增掩碼。客戶端如果收到了一個新增了掩碼的幀,必須立即關閉連線。在這種情況下,它可以使用第7.4.1節定義的1002(協議錯誤)狀態碼。(這些規則可能會在將來的規範中放開)。

基礎的資料幀協議使用操作碼、有效負載長度和在“有效負載資料”中定義的放置“擴充套件資料”與“引用資料”的指定位置來定義幀型別。特定的bit位和操作碼為將來的協議擴充套件做了保留。

一個資料幀可以在開始握手完成之後和終端傳送了一個關閉幀之前的任意一個時間通過客戶端或者服務端進行傳輸(第5.5.1節)。

5.2 基礎幀協議

在這節中的這種資料傳輸部分的有線格式是通過ABNFRFC5234來進行詳細說明的。(注意:不像這篇文件中的其他章節內容,在這節中的ABNF是對bit組進行操作。每一個bit組的長度是在評論中展示的。線上上編碼時,最高位的bit是在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 bit

​ 必須設定為0,除非擴充套件了非0值含義的擴充套件。如果收到了一個非0值但是沒有擴充套件任何非0值的含義,接收終端必須斷開WebSocket連線。

Opcode: 4 bit

​ 定義“有效負載資料”的解釋。如果收到一個未知的操作碼,接收終端必須斷開WebSocket連線。下面的值是被定義過的。

​ %x0 表示一個持續幀

​ %x1 表示一個文字幀

​ %x2 表示一個二進位制幀

​ %x3-7 預留給以後的非控制幀

​ %x8 表示一個連線關閉包

​ %x9 表示一個ping包

​ %xA 表示一個pong包

​ %xB-F 預留給以後的控制幀

Mask: 1 bit

​ mask標誌位,定義“有效負載資料”是否新增掩碼。如果設定為1,那麼掩碼的鍵值存在於Masking-Key中,根據5.3節描述,這個一般用於解碼“有效負載資料”。所有的從客戶端傳送到服務端的幀都需要設定這個bit位為1。

Payload length: 7 bits, 7+16 bits, or 7+64 bits

​ 以位元組為單位的“有效負載資料”長度,如果值為0-125,那麼就表示負載資料的長度。如果是126,那麼接下來的2個bytes解釋為16bit的無符號整形作為負載資料的長度。如果是127,那麼接下來的8個bytes解釋為一個64bit的無符號整形(最高位的bit必須為0)作為負載資料的長度。多位元組長度量以網路位元組順序表示(譯註:應該是指大端序和小端序)。在所有的示例中,長度值必須使用最小位元組數來進行編碼,例如:長度為124位元組的字串不可用使用序列126,0,124進行編碼。有效負載長度是指“擴充套件資料”+“應用資料”的長度。“擴充套件資料”的長度可能為0,那麼有效負載長度就是“應用資料”的長度。

Masking-Key: 0 or 4 bytes

​ 所有從客戶端發往服務端的資料幀都已經與一個包含在這一幀中的32 bit的掩碼進行過了運算。如果mask標誌位(1 bit)為1,那麼這個欄位存在,如果標誌位為0,那麼這個欄位不存在。在5.3節中會介紹更多關於客戶端到服務端增加掩碼的資訊。

Payload data: (x+y) bytes

​ “有效負載資料”是指“擴充套件資料”和“應用資料”。

Extension data: x bytes

​ 除非協商過擴充套件,否則“擴充套件資料”長度為0 bytes。在握手協議中,任何擴充套件都必須指定“擴充套件資料”的長度,這個長度如何進行計算,以及這個擴充套件如何使用。如果存在擴充套件,那麼這個“擴充套件資料”包含在總的有效負載長度中。

Application data: y bytes

​ 任意的“應用資料”,佔用“擴充套件資料”後面的剩餘所有欄位。“應用資料”的長度等於有效負載長度減去“擴充套件應用”長度。

基礎資料幀協議通過ABNF進行了正式的定義。需要重點知道的是,這些資料都是二進位制的,而不是ASCII字元。例如,長度為1 bit的欄位的值為%x0 / %x1代表的是一個值為0/1的單獨的bit,而不是一整個位元組(8 bit)來代表ASCII編碼的字元“0”和“1”。一個長度為4 bit的範圍是%x0-F的欄位值代表的是4個bit,而不是位元組(8 bit)對應的ASCII碼的值。不要指定字元編碼:“規則解析為一組最終的值,有時候是字元。在ABNF中,字元僅僅是一個非負的數字。在特定的上下文中,會根據特定的值的對映(編碼)編碼集(例如ASCII)”。在這裡,指定的編碼型別是將每個欄位編碼為特定的bits陣列的二進位制編碼的最終資料。

ws-frame =

  • frame-fin; 長度為1 bit
  • frame-rsv1; 長度為1 bit
  • frame-rsv2; 長度為1 bit
  • frame-rsv3; 長度為1 bit
  • frame-opcode; 長度為4 bit
  • frame-masked; 長度為1 bit
  • frame-payload-length; 長度為7或者7+16或者7+64 bit
  • [frame-masking-key]; 長度為32 bit
  • frame-payload-data; 長度為大於0的n*8 bit(其中n>0)

frame-fin =

  • %x0,除了以下為1的情況
  • %x1,最後一個訊息幀
  • 長度為1 bit

frame-rsv1 =

  • %x0 / %x1,長度為1 bit,如果沒有協商則必須為0

frame-rsv2 =

  • %x0 / %x1,長度為1 bit,如果沒有協商則必須為0

frame-rsv3 =

  • %x0 / %x1,長度為1 bit,如果沒有協商則必須為0

frame-opcode =

  • frame-opcode-non-control
  • frame-opcode-control
  • frame-opcode-cont

frame-opcode-non-control

  • %x1,文字幀
  • %x2,二進位制幀
  • %x3-7,保留給將來的非控制幀
  • 長度為4 bit

frame-opcode-control

  • %x8,連線關閉
  • %x9,ping幀
  • %xA,pong幀
  • %xB-F,保留給將來的控制幀
  • 長度為4 bit

frame-masked

  • %x0,不新增掩碼,沒有frame-masking-key
  • %x1,新增掩碼,存在frame-masking-key
  • 長度為1 bit

frame-payload-length

  • %x00-7D,長度為7 bit
  • %x7E frame-payload-length-16,長度為7+16 bit
  • %x7F frame-payload-length-63,長度為7+64 bit

frame-payload-length-16

  • %x0000-FFFF,長度為16 bit

frame-payload-length-63

  • %x0000000000000000-7FFFFFFFFFFFFFFF,長度為64 bit

frame-masking-key

  • 4(%x00-FF),當frame-mask為1時存在,長度為32 bit

frame-payload-data

  • frame-masked-extension-data frame-masked-application-data,當frame-masked為1時
  • frame-unmasked-extension-data frame-unmasked-application-data,當frame-masked為0時

frame-masked-extension-data

  • *(%x00-FF),保留給將來的擴充套件,長度為n*8,其中n>0

frame-masked-application-data

  • *(%x00-FF),長度為n*8,其中n>0

frame-unmasked-extension-data

  • *(%x00-FF),保留給將來的擴充套件,長度為n*8,其中n>0

frame-unmasked-application-data

  • *(%x00-FF),長度為n*8,其中n>0

5.3 客戶端到服務端新增掩碼

新增掩碼的資料幀必須像5.2節定義的一樣,設定frame-masked欄位為1。

掩碼值像第5.2節說到的完全包含在幀中的frame-masking-key上。它是用於對定義在同一節中定義的幀負載資料Payload data欄位中的包含Extension dataApplication data的資料進行新增掩碼。

掩碼欄位是一個由客戶端隨機選擇的32bit的值。當準備掩碼幀時,客戶端必須從允許的32bit值中須知你咋一個新的掩碼值。掩碼值必須是不可被預測的;因此,掩碼必須來自強大的熵源(entropy),並且給定的掩碼不能讓伺服器或者代理能夠很容易的預測到後續幀。掩碼的不可預測性對於預防惡意應用作者在網上暴露相關的位元組資料至關重要。RFC 4086討論了安全敏感的應用需要一個什麼樣的合適的強大的熵源。

掩碼不影響Payload data的長度。進行掩碼的資料轉換為非掩碼資料,或者反過來,根據下面的演算法即可。這個同樣的演算法適用於任意操作方向的轉換,例如:對資料進行掩碼操作和對資料進行反掩碼操作所涉及的步驟是相同的。

表示轉換後資料的八位位元組的i(transformed-octet-i)是表示的原始資料的i(original-octet-i)與索引i模4得到的掩碼值(masking-key-octet-j)經過異或操作(XOR)得到的:

j = i MOD 4 transfromed-octed-i = original-octet-i XOR masking-key-octet-j

在規範中定義的位於frame-payload-length欄位的有效負載的長度,不包括掩碼值的長度。它只是Payload data的長度。如跟在掩碼值後面的位元組陣列的數。

5.4 訊息分片

訊息分片的主要目的是允許傳送一個未知長度且訊息開始傳送後不需要快取的訊息。如果訊息不能被分片,那麼一端必須在快取整個訊息,因此這個訊息的長度必須在第一個位元組傳送前就需要計算出來。如果有訊息分片,服務端或者代理可以選擇一個合理的快取長度,當快取區滿了以後,就想網路傳送一個片段。

第二個訊息分片使用的場景是不適合在一個邏輯通道內傳輸一個大的訊息佔滿整個輸出頻道的多路複用場景。多路複用需要能夠將訊息進行自由的切割成更小的片段來共享輸出頻道。(注意:多路複用的擴充套件不在這個文件中討論)。

除非在擴充套件中另有規定,否則幀沒有語義的含義。如果客戶端和服務的沒有協商擴充套件欄位,或者服務端和客戶端協商了一些擴充套件欄位,並且代理能夠完全識別所有的協商擴充套件欄位,在這些擴充套件欄位存在的情況下知道如何進行幀的合併和拆分,代理就可能會合並或者拆分幀。這個的一個含義是指在缺少擴充套件欄位的情況下,傳送者和接收者都不能依賴特定的幀邊界的存在。

訊息分片相關的規則如下:

  • 一個未分片的訊息包含一個設定了FIN欄位(標記為1)的單獨的幀和一個除0以外的操作碼。
  • 一個分片的訊息包含一個未設定的FIN欄位(標記為0)的單獨的幀和一個除0以外的操作碼,然後跟著0個或者多個未設定FIN欄位的幀和操作碼為0的幀,然後以一個設定了FIN欄位以及操作碼為0的幀結束。一個分片的訊息內容按幀順序組合後的payload欄位,是等價於一個單獨的更大的訊息payload欄位中包含的值;然而,如果擴充套件欄位存在,因為擴充套件欄位定義了Extension data的解析方式,因此前面的結論可能不成立。例如:Extension data可能只出現在第一個片段的開頭,並適用於接下來的片段,或者可能每一個片段都有Extension data,但是隻適用於特定的片段。在Extension data不存在時,下面的示例演示了訊息分片是如何運作的。 示例:一個文字需要分成三個片段進行傳送,第一個片段包含的操作碼為0x1並且未設定FIN欄位,第二個片段的操作碼為0x0並且未設定FIN欄位,第三個片段的操作碼為0x0並且設定了FIN欄位。
  • 控制幀(見5.5節)可能被插入到分片訊息的中間。控制幀不能被分片。
  • 訊息片段必須在傳送端按照順序傳送給接收端。
  • 除非在擴充套件中定義了這種巢狀的邏輯,否則一條訊息分的片不能與另一條訊息分的片巢狀傳輸。
  • 終端必須有能力來處理在分片的訊息中的控制幀。
  • 傳送端可能會建立任意大小的非控制訊息片段。
  • 客戶端和服務端必須同時支援分片和不分片訊息。
  • 控制幀不能被分片,並且代理不允許改變控制幀的片段。
  • 如果有保留欄位被使用並且代理不能理解這些欄位的值時,那麼代理不能改變訊息的片段。
  • 在擴充套件欄位已經被協商過,但是代理不知道協商擴充套件欄位的具體語義時,代理不能改變任意訊息的片段。同樣的,擴充套件不能看到WebSocket握手(並且得不到通知內容)導致WebSocket的連線禁止改變連線過程中任意的訊息片段。
  • 作為這些規則的結論,所有的訊息片段都是同型別的,並且設定了第一個片段的操作碼(opccode)欄位。控制幀不能被分片,所有的訊息分片型別必須是文字或者二進位制,或者是保留的任意一個操作碼。

注:如果控制幀沒有被打斷,心跳(ping)的等待時間可能會變很長,例如在一個很大的訊息之後。因此,在分片的訊息傳輸中插入控制幀是有必要的。

實踐說明:如果擴充套件欄位不存在,接收者不需要使用快取來儲存下整個訊息片段來進行處理。例如:如果使用一個流式API,再收到部分幀的時候就可以將資料交給上層應用。然而,這個假設對以後所有的WebSocket擴充套件可能不一定成立。

5.5 控制幀

控制幀是通過操作碼最高位的值為1來進行區分的。當前已經定義的控制幀操作碼包括0x8(關閉),0x9(心跳Ping)和0xA(心跳Pong)。操作碼0xB-0xF沒有被定義,當前被保留下來做為以後的控制幀。

控制幀是用於WebSocket的通訊狀態的。控制幀可以被插入到訊息片段中進行傳輸。

所有的控制幀必須有一個126位元組或者更小的負載長度,並且不能被分片。

5.5.1 關閉(Close)

控制幀的操作碼值是0x8。

關閉幀可能包含內容(body)(幀的“應用資料”部分)來表明連線關閉的原因,例如終端的斷開,或者是終端收到了一個太大的幀,或者是終端收到了一個不符合預期的格式的內容。如果這個內容存在,內容的前兩個位元組必須是一個無符號整型(按照網路位元組序)來代表在7.4節中定義的狀態碼。跟在這兩個整型位元組之後的可以是UTF-8編碼的的資料值(原因),資料值的定義不在此文件中。資料值不一定是要人可以讀懂的,但是必須對於除錯有幫助,或者能傳遞有關於當前開啟的這條連線有關聯的資訊。資料值不保證人一定可以讀懂,所以不能把這些展示給終端使用者。

從客戶端傳送給服務端的控制幀必須新增掩碼,具體見5.3節。

應用禁止在傳送了關閉的控制幀後再傳送任何的資料幀。

如果終端收到了一個關閉的控制幀並且沒有在以前傳送一個關閉幀,那麼終端必須傳送一個關閉幀作為回應。(當傳送一個關閉幀作為回應時,終端通常會輸出它收到的狀態碼)響應的關閉幀應該儘快傳送。終端可能會推遲傳送關閉幀直到當前的訊息都已經傳送完成(例如:如果大多數分片的訊息已經傳送了,終端可能會在傳送關閉幀之前將剩餘的訊息片段傳送出去)。然而,已經傳送關閉幀的終端不能保證會繼續處理收到的訊息。

在已經傳送和收到了關閉幀後,終端認為WebSocket連線以及關閉了,並且必須關閉底層的TCP連線。服務端必須馬上關閉底層的TCP連線,客戶端應該等待服務端關閉連線,但是也可以在收到關閉幀以後任意時間關閉連線。例如:如果在合理的時間段內沒有收到TCP關閉指令。

如果客戶端和服務端咋同一個時間傳送了關閉幀,兩個終端都會傳送和接收到一條關閉的訊息,並且應該認為WebSocket連線已經關閉,同時關閉底層的TCP連線。

5.5.2 心跳Ping

心跳Ping幀包含的操作碼是0x9。

關閉幀可能包含“應用資料”。

如果收到了一個心跳Ping幀,那麼終端必須傳送一個心跳Pong 幀作為回應,除非已經收到了一個關閉幀。終端應該儘快恢復Pong幀。Pong幀將會在5.5.3節討論。

終端可能會在建立連線後與連線關閉前中間的任意時間傳送Ping幀。

注意:Ping幀可能是用於保活或者用來驗證遠端是否仍然有應答。

5.5.3 心跳Pong

心跳Ping幀包含的操作碼是0xA。

5.5.2節詳細說明了Ping幀和Pong幀的要求。

作為回應傳送的Pong幀必須完整攜帶Ping幀中傳遞過來的“應用資料”欄位。

如果終端收到一個Ping幀但是沒有傳送Pong幀來回應之前的pong幀,那麼終端可能選擇用Pong幀來回復最近處理的那個Ping幀。

Pong幀可以被主動傳送。這會作為一個單項的心跳。預期外的Pong包的響應沒有規定。

5.6 資料幀

資料幀(例如非控制幀)的定義是操作碼的最高位值為0。當前定義的資料幀操作嗎包含0x1(文字)、0x2(二進位制)。操作碼0x3-0x7是被保留作為非控制幀的操作碼。

資料幀會攜帶應用層/擴充套件層資料。操作碼決定了攜帶的資料解析方式:

文字

“負載欄位”是用UTF-8編碼的文字資料。注意特殊的文字幀可能包含部分UTF-8序列;然而,整個訊息必須是有效的UTF-8編碼資料。重新組合訊息後無效的UTF-8編碼資料處理見8.1節。

二進位制

“負載欄位”是任意的二進位制資料,二進位制資料的解析僅僅依靠應用層。

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請求和新增掩碼的Ping響應(譯者注:即Pong) 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (包含內容為”Hello", 但是文字內容是任意的) 0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (包含內容為”Hello", 匹配ping的內容)
  • 256位元組的二進位制資料放入一個未新增掩碼資料幀 0x82 0x7E 0x0100 [256 bytes of binary data\]
  • 64KB二進位制資料在一個非掩碼幀中 0x82 0x7F 0x0000000000010000 [65536 bytes of binary data\]

5.8 擴充套件性

這個協議的設計初衷是允許擴充套件的,可以在基礎協議上增加能力。終端的連線必須在握手的過程中協商使用的所有擴充套件。在規範中提供了從0x3-0x7和0xB-0xF的操作碼,在資料幀Header中的“擴充套件資料”欄位、frame-rsv1、frame-rsv2、frame-rsv3欄位都可以用於擴充套件。擴充套件的協商討論將在以後的9.1節中詳細討論。下面是一些符合預期的擴充套件用法。下面的列表不完整,也不是規範中內容。

  • “擴充套件資料”可以放置在“負載資料“中的應用資料”之前的位置。
  • 保留的欄位可以在每一幀需要時被使用。
  • 保留的操作碼的值可以被定義。
  • 如果需要更多的操作碼,那麼保留的操作碼欄位可以被定義。
  • 保留的欄位或者“擴充套件”操作碼可以在“負載資料”之中的分配額外的位置來定義,這樣可以定義更大的操作碼或者更多的每一幀的欄位。

相關文章