1. 程式人生 > 其它 >HTTP/2 中的幀定義

HTTP/2 中的幀定義

在 HTTP/2 的規範中定義了許多幀型別,每個幀型別由唯一的 8 位型別程式碼標識。每種幀型別在建立和管理整個連線或單個 stream 流中起到不同的作用。

特定的幀型別的傳輸可以改變連線的狀態。如果端點無法維持連線狀態的同步檢視,則無法在連線內繼續成功通訊。因此,重要的是端點必須共享的理解狀態,在使用了任何給定幀的情況下,這些狀態是如何受到它們影響的。

Connection 連線:1 個 TCP 連線,包含 1 個或者多個 stream。所有通訊都在一個 TCP 連線上完成,此連線可以承載任意數量的雙向資料流。

Stream 資料流:一個雙向通訊的資料流,包含 1 條或者多條 Message。每個資料流都有一個唯一的識別符號和可選的優先順序資訊,用於承載雙向訊息。

Message 訊息:對應 HTTP/1.1 中的請求 request 或者響應 response,包含 1 條或者多條 Frame。

Frame 資料幀:最小通訊單位,以二進位制壓縮格式存放內容。來自不同資料流的幀可以交錯傳送,然後再根據每個幀頭的資料流識別符號重新組裝。

在 HTTP/1.1 中的一個訊息是由 Start Line + header + body 組成的,而 HTTP/2 中一個訊息是由 HEADER frame + 若干個 DATA frame 組成的,如下圖:

HTTP/2 所有效能增強的核心在於新的二進位制分幀層,它定義瞭如何封裝 HTTP 訊息並在客戶端與伺服器之間傳輸。這裡所謂的“層”,指的是位於套接字介面與應用可見的高階 HTTP API 之間一個經過優化的新編碼機制:HTTP 的語義(包括各種動詞、方法、標頭)都不受影響,不同的是傳輸期間對它們的編碼方式變了。 HTTP/1.x 協議以換行符作為純文字的分隔符,而 HTTP/2 將所有傳輸的資訊分割為更小的訊息和幀,並採用二進位制格式對它們編碼。

這樣一來,客戶端和伺服器為了相互理解,都必須使用新的二進位制編碼機制:HTTP/1.x 客戶端無法理解只支援 HTTP/2 的伺服器,反之亦然。不過不要緊,現有的應用不必擔心這些變化,因為客戶端和伺服器會替我們完成必要的分幀工作。

一. DATA 幀

DATA 幀(型別 = 0x0)可以傳輸與流相關聯的任意可變長度的八位位元組序列。例如,使用一個或多個 DATA 幀來承載 HTTP 請求或響應有效載荷。DATA 幀也可以包含填充。可以將填充新增到 DATA 幀用來模糊訊息的大小。填充是一種安全的功能;

DATA 幀結構如下:

    +---------------+
    |Pad Length? (8)|
    +---------------+-----------------------------------------------+
    |                            Data (*)                         ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+

DATA 幀包含以下幾個欄位:

  • Pad Length:
    一個 8 位欄位,包含以八位位元組為單位的幀填充長度。該欄位是有條件的(如圖中的 "?" 所示),僅在設定了 PADDED 標誌時才存在。

  • Data:
    應用資料。在減去存在的其他欄位的長度之後,data 的大小是幀有效載荷的剩餘部分。

  • Padding:
    填充的八位位元組,它不包含應用程式語義值。傳送時,填充的八位字必須設定為零。接收方沒有義務驗證填充,但可以將非零填充視為 PROTOCOL_ERROR 型別的連線錯誤

DATA 幀定義了以下 flag 標識:

  • END_STREAM (0x1):
    設定這個欄位的時候,位 0 表示該幀是端點為將要傳送的標識流的最後一幀。設定此標誌會導致流進入"半關閉"狀態或者"關閉"狀態。

  • PADDED (0x8):
    設定這個欄位的時候,位 3 表示存在 Pad Length 欄位及其描述的任何填充。

DATA 幀必須與某一個流相互關聯。如果接收到其流識別符號欄位為 0x0 的 DATA 幀,則接收方必須以 PROTOCOL_ERROR 型別的連線錯誤進行響應。

DATA 幀會受到流量控制,只能在流處於“開啟”或“半關閉(遠端)”狀態時傳送。整個 DATA 幀有效載荷包含在流量控制中,包括 Pad Length 和 Padding 欄位(如果存在)。如果收到的資料幀的流不是“開啟”或“半關閉(本地)”狀態,則接收方必須以 STREAM_CLOSED 型別的流錯誤進行響應。

填充八位位元組的總數由填充長度欄位的值確定。如果填充的長度是幀有效負載的長度或更長,則接收方必須將其視為 PROTOCOL_ERROR 型別的連線錯誤。

注意:通過包含值為零的 Pad Length 欄位,可以將幀的大小增加一個八位位元組。

data 幀比較簡單,抓包看看它的內容:

上圖中可以看到兩個 flag 標記位,END_STREAM 和 PADDED。END_STREAM 是 false。由於 PADDED 是 false,無填充,所以 Pad Length 是 0。

二. HEADERS 幀

HEADERS 幀 (型別 = 0x1) 用於開啟一個流,另外還帶有 header block fragment 頭塊片段。HEADERS 幀可以在“空閒”,“保留(本地)”,“開啟”或“半關閉(遠端)”狀態的流上傳送。此幀專門用來傳遞 HTTP header(相當於 HTTP/1.1 中的 start line + header) 的。

C
    +---------------+
    |Pad Length? (8)|
    +-+-------------+-----------------------------------------------+
    |E|                 Stream Dependency? (31)                     |
    +-+-------------+-----------------------------------------------+
    |  Weight? (8)  |
    +-+-------------+-----------------------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+

HEADERS 幀包含以下幾個欄位:

  • Pad Length:
    一個 8 位欄位,包含以八位位元組為單位的幀填充長度。僅當設定了 PADDED 標誌時,才會出現此欄位。

  • E:
    一個單位標誌,用來標識流依賴是獨佔的。僅當設定了 PRIORITY 標誌時,才會出現此欄位。

  • Stream Dependency:
    此流所依賴的流的 31 位流識別符號。僅當設定了 PRIORITY 標誌時,才會出現此欄位。

  • Weight:
    一個無符號的 8 位整數,表示流的優先順序權重。這個值代表獲得 1 到 256 之間的權重。僅當設定了 PRIORITY 標誌時,才會出現此欄位。

  • Header Block Fragment:
    頭塊片段

  • Padding:
    填充的 8 位位元組。

HEADERS 幀定義了以下 flag 標識:

  • END_STREAM (0x1):
    設定這個欄位的時候,位 0 表示 header block是端點為將要傳送的標識流的最後一幀。

HEADERS 幀攜帶了 END_STREAM 標誌,該標誌表示流的結束。但是,設定了 END_STREAM 標誌的 HEADERS 幀後面可以跟著同一個流上的 CONTINUATION 幀。從邏輯上講,CONTINUATION 幀是 HEADERS 幀的一部分。

  • END_HEADERS (0x4):
    這個 flag 被設定的時候,位 2 表示該幀包含整個頭塊,並且後面沒有任何 CONTINUATION 幀。

沒有設定 END_HEADERS 標誌的 HEADERS 幀必須後跟相同流的 CONTINUATION 幀。接收方必須將接收任何其他型別的幀或不同流上的幀視為 PROTOCOL_ERROR 型別的連線錯誤

  • PADDED (0x8):
    這個 flag 被設定的時候,位 3 表示 Pad Length 欄位及其描述的任何填充都存在。

  • PRIORITY (0x20):
    這個 flag 被設定的時候,位 5 表示存在 Exclusive Flag(E),Stream Dependency 和 Weight 欄位。

HEADERS 幀的有效負載包含一個頭塊片段。一個 header block 頭塊如果大於一個 HEADERS 幀,將會在 CONTINUATION 幀中繼續傳輸。

HEADERS 幀必須與某一個流相關聯。如果接收到其流識別符號欄位為 0x0 的 HEADERS 幀,則接收方必須以 PROTOCOL_ERROR 型別的連線錯誤進行響應。HEADERS 幀會更改連線狀態。

HEADERS 幀可以包含填充段。填充欄位和標誌與為 DATA 幀定義的欄位和標誌相同。超過頭塊片段剩餘大小的填充必須被視為 PROTOCOL_ERROR。

HEADERS 幀中的優先順序資訊在邏輯上等同於單獨的 PRIORITY 幀,但是優先順序資訊包含在 HEADERS 中可避免在建立新流時流優先順序流失的可能性。HEADERS 幀中的優先順序欄位在 stream 流上的第一個 HEADERS 幀之後會重新確定流的優先順序。

抓取一個實際的包看看 HEADERS 幀中有哪些內容。

Flag 裡面包含了上面提到的 END_STREAM、END_HEADERS、PADDED、PRIORITY 標識。再看看其他欄位,Pad Length 由於 PADDED 設定是 false,所以這裡沒有填充。PRIORITY 設定了值,所以 E 這個單位標誌位存在。Stream Dependency 是 0,Stream Indentifer 是 1。Weight 是 255,沒有 Padding。 剩下的部分全部都是 Header Block Fragment。如下圖:

在圖中可以看到,HTTP/2 中對 HTTP 1.X 中的首部欄位的名字做了一些變更。例如 HTTP 1.X 中的 HOST,對應 HTTP/2 中的 :authority:。HTTP 1.X 中的請求行變成了 HTTP/2 中的 :method:、:scheme:、:path:。其他的欄位例如 user-agent 雖然名字沒有變化,但是儲存方式都發生了變化。具體變化在 HPACK 中細講。

上面這連續的 5 張圖展示了 HTTP/2 中的 Header Block Fragment 儲存方式。從頭部欄位中可以看出 HTTP/2 全新的儲存方式和更高的壓縮率。更加詳細的分析見 HPACK 詳解。

上面的抓包是一個 request 的 HEADERS 幀,再舉一個 response 的例子。

上圖中可以看到,HTTP 1.X 中的 response 中的狀態行轉變成了 HTTP/2 中的 :status:,其他 HTTP 1.X 中的首部欄位也相應的在 HEADERS 幀中。

HEADERS 幀會經常使用 Weight 權重欄位,例如,不同檔案感覺重要性不同,有不同的權重:

上面圖中這個例子,html 檔案和 woff 字型檔案的權重就比 js 檔案和 jpg 圖片檔案的權重高。

同一種類型的檔案,也會有權重的高低不同,例如都是 CSS 檔案:

上面圖中這個例子,同樣是 css 檔案,權重也有不同。

三. PRIORITY 幀

PRIORITY 幀(型別 = 0x2)指定了 stream 流的傳送方的建議優先順序。它可以在任何流的狀態下發送,包括空閒或關閉的流。

    +-+-------------------------------------------------------------+
    |E|                  Stream Dependency (31)                     |
    +-+-------------+-----------------------------------------------+
    |   Weight (8)  |
    +-+-------------+

PRIORITY 幀包含以下幾個欄位:

  • E:
    一個單位標誌,指示流依賴是獨佔的。

  • Stream Dependency:
    此流所依賴的流的 31 位流識別符號。

  • Weight:
    無符號的 8 位整數,表示流的優先順序權重。這個值代表獲得 1 到 256 之間的權重。預設權重 16。

PRIORITY 不包含任何 flag 標識。

PRIORITY 幀始終標識一個流。如果接收到流識別符號為 0x0 的 PRIORITY 幀,則接收方必須以 PROTOCOL_ERROR 型別的連線錯誤進行響應。

PRIORITY 幀可以在任何狀態下在 stream 流上傳送,但不能在包含單個頭塊的連續幀之間傳送。請注意,此幀可能在處理中或者在幀傳送完成後到達,這會導致它對標識的流沒有影響。對於處於 "半關閉(遠端)" 或 "關閉" 狀態的流,該幀只能影響標識的流的處理和它依賴流的處理;它不會影響該流上的幀傳輸。

可以針對 "空閒" 或 "關閉" 狀態的流傳送 PRIORITY 幀。這允許通過改變未使用或關閉的父流的優先順序來重新確定一組依賴流的優先順序。然而,關閉流上傳送的優先順序幀可能存在被對端忽略的風險,因為對端可能已經丟棄了這個流的優先順序狀態資訊。

長度不超過 5 個八位位元組的 PRIORITY 幀必須被視為 FRAME_SIZE_ERROR 型別的流錯誤

在上面抓包截圖中可以看到,PRIORITY 幀的 Exclusive = true,代表該流的依賴是獨佔的。Stream Dependency = 49,依賴第 49 號的流。Weight 權重是 219。

四. RST_STREAM 幀

在 HTTP 1.X 中,一個連線同一時間內只發送一個請求,如果需要中途中止,直接關閉連線即可。但是在 HTTP/2 中,多個 Stream 會共享同一個連線。如果關閉連線會影響其他的 Stream 流,RST_STREAM 幀也就出現了,它允許立刻中止一個未完成的流。

RST_STREAM幀(型別 = 0x3)允許立即終止一個 stream 流。傳送 RST_STREAM 以請求取消一個流或指示已發生錯誤的情況。

C
    +---------------------------------------------------------------+
    |                        Error Code (32)                        |
    +---------------------------------------------------------------+

RST_STREAM 幀包含一個無符號的 32 位整數,用於標識錯誤程式碼。錯誤程式碼表明瞭流被終止的原因。

RST_STREAM 幀沒有定義任何 flag 標誌。

RST_STREAM 幀完全終止引用的流並使其進入"關閉"狀態。在流上接收到 RST_STREAM 後,接收方不得為該流傳送額外的幀,但 PRIORITY 幀除外。但是,在傳送 RST_STREAM 之後,傳送端點必須準備好接收和處理在 RST_STREAM 幀到達之前,可能已經由對端在傳送的流上傳送的附加幀。

RST_STREAM 幀必須與流相關聯。如果接收到具有流識別符號 0x0 的 RST_STREAM 幀,則接收方必須將其視為 PROTOCOL_ERROR 型別的連線錯誤。

不得為“空閒”狀態的流傳送 RST_STREAM 幀。如果接收到標識空閒流的 RST_STREAM 幀,則接收方必須將其視為 PROTOCOL_ERROR 型別的連線錯誤。長度不超過 4 個八位位元組的 RST_STREAM 幀必須被視為 FRAME_SIZE_ERROR 型別的連線錯誤。

RST_STREAM 幀由於沒有 flag 標誌,是十種幀型別裡面比較簡單的型別。這裡的 Error Code 是 CANCEL (0x8)。

五. SETTINGS 幀

SETTINGS 幀(型別 = 0x4)傳遞影響端點通訊方式的配置引數,例如設定對端行為的首選項和約束。SETTINGS 幀還用於確認收到這些引數。單獨地,SETTINGS 引數也可以稱為"設定"。

SETTINGS 引數不是通過協商來確定的;它們描述了傳送對端的特徵,它們由接收對端使用。相同的引數對不同的對等端設定可能不同。例如,客戶端可能會設定較高的初始流量控制視窗,而伺服器可能會設定較低的值以節省資源。

SETTINGS 幀必須由兩個端點在連線開始時傳送,並且可以在任何其他時間由任一端點在連線的生命週期內傳送。實現方必須支援 HTTP/2 規範定義的所有引數。

SETTINGS 幀中的每個引數都會替換該引數的任何現有值。引數按它們出現的順序處理,並且 SETTINGS 幀的接收方不需要保持除其引數的當前值之外的任何狀態。因此,SETTINGS 引數的值是接收方看到的最後一個值。

SETTINGS 引數由接收對端確認。要啟用此功能,SETTINGS 幀將定義以下標誌:

  • ACK (0x1):
    設定這個欄位的時候,位 0 表示該幀已經被對等的 SETTINGS 幀的接收和應用。設定此位後,SETTINGS 幀的有效負載必須為空。收到設定了 ACK 標誌且長度欄位值不為 0 的 SETTINGS 幀必須被視為 FRAME_SIZE_ERROR 型別的連線錯誤。有關更多資訊。

SETTINGS 幀始終適用於連線,而不是作用於單個流。SETTINGS 幀的流識別符號必須為零(0x0)。如果端點收到其流識別符號欄位不是 0x0 的 SETTINGS 幀,則端點必須響應 PROTOCOL_ERROR 型別的連線錯誤。

SETTINGS 幀影響連線狀態。一個格式錯誤或不完整的 SETTINGS 幀必須被視為 PROTOCOL_ERROR 型別的連線錯誤。

長度不是 6 個八位位元組的 SETTINGS 幀必須被視為 FRAME_SIZE_ERROR 型別的連線錯誤。

1. SETTINGS Format

SETTINGS 幀的有效負載由零個或多個引數組成,每個引數由無符號 16 位設定識別符號和無符號 32 位值組成。

C
    +-------------------------------+
    |       Identifier (16)         |
    +-------------------------------+-------------------------------+
    |                        Value (32)                             |
    +---------------------------------------------------------------+

2. Defined SETTINGS Parameters

定義了以下引數:

  • SETTINGS_HEADER_TABLE_SIZE(0x1):
    允許傳送方以八位位元組通知遠端端點用於解碼頭塊的頭壓縮表的最大大小。編碼器可以通過使用特定於報頭塊內的報頭壓縮格式的訊號來選擇等於或小於該值的任何大小。初始值為 4,096 個八位位元組。

  • SETTINGS_ENABLE_PUSH(0x2):
    此設定可用於禁用伺服器推送。如果端點接收到此引數設定為 0,則端點絕不能傳送 PUSH_PROMISE 幀。既將該引數設定為 0 並且已將其確認的端點必須將 PUSH_PROMISE 幀的接收視為 PROTOCOL_ERROR型別的連線錯誤。初始值為 1,表示允許伺服器推送。除 0 或 1 以外的任何值必須視為 PROTOCOL_ERROR 型別的連線錯誤。

  • SETTINGS_MAX_CONCURRENT_STREAMS(0x3):
    表示傳送方允許的最大併發流數。此限制是有方向性的:它適用於傳送方允許接收方建立的流的數量。初始化的時候,此值沒有限制。建議此值不小於 100,以免不必要地限制並行性。當 SETTINGS_MAX_CONCURRENT_STREAMS 的值 0 不應被端點視為特殊值。零值確實會阻止建立新流;但是,另外它也適用於被啟用的流用盡的任何限制。伺服器應該只為短連線設定零值;如果伺服器不希望接受請求,則關閉連線更合適。

SETTINGS_MAX_CONCURRENT_STREAMS 僅統計 open 和 half-close 狀態的流,不包含用於推送狀態的 reserved 狀態的流。

  • SETTINGS_INITIAL_WINDOW_SIZE(0x4):
    表示傳送方的初始視窗大小(以八位位元組為單位),用於 stream 流級別流量控制。初始值為 2^16-1(65,535)個八位位元組。此設定會影響所有流的視窗大小。高於最大流量控制視窗大小 2^31-1 的值必須被視為 FLOW_CONTROL_ERROR 型別的連線錯誤。

  • SETTINGS_MAX_FRAME_SIZE(0x5):
    表示傳送方願意接收的最大幀有效負載的大小(以八位位元組為單位)。初始值為 2^14(16,384)個八位位元組。端點廣播的值必須在此初始值與允許的最大幀大小(2^24-1 或 16,777,215個八位位元組)之間(包括兩者)。超出此範圍的值必須視為 PROTOCOL_ERROR 型別的連線錯誤。

  • SETTINGS_MAX_HEADER_LIST_SIZE(0x6):
    此通知設定以八位位元組的形式通知對端,傳送方準備接受的頭列表的最大大小。該值基於頭欄位的未壓縮大小,包括八位位元組的名稱和值的長度加上每個頭欄位的32個八位位元組的開銷。對於任何給定的請求,可以強制執行低於建議值的下限。此設定的初始值無限制。

接收端如果接收到具有任何未知或不支援的識別符號的 SETTINGS 幀,必須忽略該設定。

3. Settings Synchronization

SETTINGS 中的大多數值受益於或要求瞭解對端何時接收並應用更改的引數值。為了提供這樣的同步時間點,其中未設定 ACK 標誌的 SETTINGS 幀的接收者必須在接收時儘快使更新的引數生效。

SETTINGS 幀中的值必須按照它們出現的順序進行處理,而值之間不需要進行其他幀處理。必須忽略不支援的引數。一旦處理完所有值,接收方必須立即發出設定了 ACK 標誌的 SETTINGS 幀。在接收到設定了 ACK 標誌的 SETTINGS 幀時,改變的引數的傳送者可以認為引數已經生效。

如果 SETTINGS 幀的傳送方在合理的時間內沒有收到確認,則可能會發出 SETTINGS_TIMEOUT 型別的連線錯誤。

4. For Example

舉個例子,SETTINGS 幀的有效負載有多個引數的情況:

在上圖中,上面一個 SETTINGS 幀的 ACK 的值是 false,表示該幀已經被對等的 SETTINGS 幀的接收和應用。這個 SETTINGS 幀帶了 3 個引數,SETTINGS_MAX_CONCURRENT_STREAMS(0x3) = 128、SETTINGS_INITIAL_WINDOW_SIZE(0x4) = 65536、SETTINGS_MAX_FRAME_SIZE(0x5) = 16777215 。下面的 SETTINGS 幀一個引數也沒有攜帶,ACK 標記位是 true。

六. PUSH_PROMISE 幀

PUSH_PROMISE幀(型別 = 0x5) 用於在傳送方打算髮起的流之前提前通知對端。PUSH_PROMISE 幀包括端點計劃建立的流的無符號 31 位識別符號以及為流提供附加上下文的一組頭。8.2 節詳細描述了 PUSH_PROMISE 幀的使用。此幀是服務端推送資源時描述請求的幀。

C
    +---------------+
    |Pad Length? (8)|
    +-+-------------+-----------------------------------------------+
    |R|                  Promised Stream ID (31)                    |
    +-+-----------------------------+-------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+

PUSH_PROMISE 幀包含以下幾個欄位:

  • Pad Length:
    一個 8 位欄位,包含以八位位元組為單位的幀填充長度。僅當設定了 PADDED 標誌時,才會出現此欄位。

  • R:
    一個保留位。

  • Promised Stream ID:
    無符號的 31 位整數,用於標識 PUSH_PROMISE 保留的流。promised 流識別符號必須是傳送方傳送的下一個流的有效選擇。

  • Header Block Fragment:
    包含請求標頭欄位的頭塊片段。

  • Padding:
    填充的八位位元組。

PUSH_PROMISE 幀定義了以下 flag 標識:

  • END_HEADERS (0x4):
    設定這個欄位的時候,位 2 表示該幀包含整個 header 塊,並且後面沒有任何 CONTINUATION 幀。沒有設定 END_HEADERS 標誌的 PUSH_PROMISE 幀必須後跟相同流的 CONTINUATION 幀。接收方必須將接收到任何其他型別的幀或不同流上的幀視為 PROTOCOL_ERROR 型別的連線錯誤。

  • PADDED (0x8):
    設定這個欄位的時候,位 3 表示存在 Pad Length 欄位及其描述的任何填充。

PUSH_PROMISE 幀必須僅在處於 "開啟" 或 "半關閉(遠端)" 狀態的對端初始化發起的流上傳送。 PUSH_PROMISE 幀的流識別符號表明了與其關聯的流。如果流識別符號欄位指定值 0x0,則接收方必須響應型別為 PROTOCOL_ERROR的連線錯誤。Promised 流不需要按承諾的順序使用。PUSH_PROMISE 僅保留流識別符號以供以後使用。

如果對端的 SETTINGS_ENABLE_PUSH 設定設定為 0,則不能傳送 PUSH_PROMISE。已設定此設定並已收到確認的端點必須將 PUSH_PROMISE 幀的接收視為 PROTOCOL_ERROR 型別的連線錯誤。

PUSH_PROMISE 幀的接收者可以通過引用 promised 流識別符號的 RST_STREAM 幀返回給 PUSH_PROMISE 的傳送者來選擇拒絕 promised 流。

PUSH_PROMISE 幀以兩種方式修改連線狀態。首先,包含頭塊可能會修改為維護頭部壓縮的狀態。其次,PUSH_PROMISE 還保留一個流供以後使用,使得 promised 流進入"保留"狀態。傳送方不得在流上傳送 PUSH_PROMISE,除非該流是 "開啟" 或 "半關閉(遠端)" 狀態;傳送方必須確保 promised 流是新流識別符號的有效選擇即,promised 流必須處於 "空閒" 狀態)。

由於 PUSH_PROMISE 保留一個流,忽略 PUSH_PROMISE 幀會導致流狀態變得不確定。接收方必須將既不 "開啟" 也不 "半關閉(本地)" 的流上的 PUSH_PROMISE 接收視為 PROTOCOL_ERROR 型別的連線錯誤。 但是,在關聯流上傳送 RST_STREAM 的端點必須處理可能在接收和處理 RST_STREAM 幀之前建立的 PUSH_PROMISE 幀。

接收方必須將接收到 promise 非法流識別符號的 PUSH_PROMISE 幀視為 PROTOCOL_ERROR 型別的連線錯誤。注意,非法流識別符號是當前未處於 "空閒" 狀態的流的識別符號。

PUSH_PROMISE 幀可以包括填充。填充欄位和 flag 標誌與為 DATA 幀定義的欄位和標誌相同。

客戶端會從 1 開始設定 stream ID,之後每開啟一個流,都會增加 2,並且之後一直用奇數。伺服器開啟在 PUSH_PROMISE 中標明的流時,設定的 stream ID 從 2 開始,並且之後一直用偶數。這樣設計避免了客戶端和伺服器之間的 stream ID 衝突,也可以輕鬆的判斷哪些物件是由服務端推送的。0 是保留數字,用於連線控制訊息,不能用於建立新的 stream 流。

舉個 PUSH_PROMISE 幀的例子:

上圖的例子中,Promised Stream ID = 2,偶數開始的。END_HEADERS 是 true,R 保留位是 0。padded 是 false,所以 pad length 也是 0,之後緊接著是 Header Block Fragment。

七. PING 幀

PING 幀(型別 = 0x6)是用於測量來自發送方的最小往返時間以及確定空閒連線是否仍然起作用的機制。 PING 幀可以從任何端點發送。可用作心跳檢測,兼具計算 RTT 往返時間的功能。

C
    +---------------------------------------------------------------+
    |                                                               |
    |                      Opaque Data (64)                         |
    |                                                               |
    +---------------------------------------------------------------+

除了幀頭之外,PING 幀必須在有效載荷中包含 8 個八位位元組的不透明資料。傳送方可以包含它選擇的任何值,並以任何方式使用這些八位位元組。

接收到不包含 ACK 標誌的 PING 幀的接收方必須傳送 PING 幀,其中 ACK 標誌在響應時必須設定,並且要具有相同的有效載荷。PING 響應應該優先於任何其他幀。

PING 幀定義瞭如下的 flag 標識:

  • ACK (0x1):
    設定這個欄位的時候,位 0 表示該 PING 幀是 PING 響應。端點必須在 PING 響應中設定此標誌。 端點不得響應包含此標誌的 PING 幀。

PING 幀不與任何單個流相關聯。如果接收到具有除 0x0 以外的流識別符號欄位值的 PING 幀,則接收方必須以 PROTOCOL_ERROR 型別的連線錯誤進行響應。接收到長度欄位值不是 8 的 PING 幀必須被視為 FRAME_SIZE_ERROR 型別的連線錯誤。

舉個 HTTP/2 PING 幀的例子:

ping 裡面的 ACK 標識位是 false。

pong 裡面的 ACK 標識位是 true。

八. GOAWAY 幀

GOAWAY 幀(型別 = 0x7) 用於啟動連線關閉或發出嚴重錯誤訊號。GOAWAY 允許端點優雅地停止接受新流,同時仍然完成先前建立的流的處理。這可以實現管理員的操作,例如伺服器維護。GOAWAY 幀用來優雅的終止連線或者通知錯誤。

在開始新流的端點和遠端傳送 GOAWAY 幀之間存在固有的競爭條件。為了處理這種情況,GOAWAY 包含在此連線中已經或可能在傳送端點上處理的最後一個流識別符號。例如,如果伺服器傳送 GOAWAY 幀,則標識的流是客戶端發起的流編號最高的流。GOAWAY 幀一旦傳送,如果這個流具有高於所包括的最後流識別符號的識別符號,則傳送方將忽略由接收方發起的流上傳送的幀。儘管可以為新流建立新連線,但 GOAWAY 幀的接收者不得在這個連線上開啟其他流。

如果 GOAWAY 的接收方已經在具有比 GOAWAY 幀中指示的流識別符號更高的流識別符號的流上傳送資料,那麼這些流不被處理或將不被處理。GOAWAY 幀的接收方可以將流視為從未建立它們,從而允許稍後在新連線上重試這些流。

端點應該在關閉連線之前始終傳送 GOAWAY 幀,以便遠端對端可以知道流是否已被部分處理或者沒有處理。例如,如果 HTTP 客戶端在伺服器關閉連線的同時傳送 POST,如果伺服器沒有傳送 GOAWAY 幀以指示它可能具有哪些流,則客戶端無法知道伺服器是否開始處理該 POST 請求。

端點可能選擇關閉連線而不為行為不端的對端傳送 GOAWAY 幀。GOAWAY 幀可能不會立即關閉連線;GOAWAY 幀的接收者不再使用該連線,應該在終止連線之前先發送 GOAWAY 幀。

C
    +-+-------------------------------------------------------------+
    |R|                  Last-Stream-ID (31)                        |
    +-+-------------------------------------------------------------+
    |                      Error Code (32)                          |
    +---------------------------------------------------------------+
    |                  Additional Debug Data (*)                    |
    +---------------------------------------------------------------+

GOAWAY 幀沒有定義任何 flag 標識:

GOAWAY 幀適用於連線,而不是特定的流。端點必須將具有除 0x0 以外的流識別符號的 GOAWAY 幀視為 PROTOCOL_ERROR 型別的連線錯誤。

GOAWAY 幀中的最後一個流識別符號包含最高編號的流識別符號,GOAWAY 幀的傳送者可能已對其採取某些操作或可能尚未採取操作。所有小於或等於此指定識別符號的流都可能通過某種方式被處理。如果沒有流被處理,最後流的識別符號設定為0。

注意:這個案例中,“已處理”表示流中的某些資料已經被傳到軟體的更高的層並可能被進行某些處理。

如果連線在沒有 GOAWAY 幀的情況下終止,則最後一個流識別符號實際上是有效的最高的流識別符號。在具有較低或相等編號的識別符號的流上,在連線關閉之前未完全關閉的時候,不可能再重新嘗試請求,事務或任何協議活動,但 HTTP GET,PUT 或 DELETE 等冪等操作除外。可以使用新連線安全地重試使用更高編號的流的任何協議活動。

編號低於或等於最後一個流識別符號的流上的活動可能仍然成功完成。GOAWAY 幀的傳送者可以通過傳送 GOAWAY 幀優雅地關閉連線,保持連線處於"開啟"狀態,直到所有正在進行的流都處理完成。

如果情況發生變化,端點可以傳送多個 GOAWAY 幀。例如,在正常關閉期間傳送帶有 NO_ERROR 的 GOAWAY 的端點可能隨後遇到需要立即終止連線的情況。來自最後一個 GOAWAY 幀的最後一個流識別符號指示這些流已經被成功處理了。端點不得增加它們在最後一個流識別符號中傳送的值,因為對端可能已經在另一個連線上重試了未處理的請求。

當伺服器關閉連線時,無法重試請求的客戶端將丟失所有正在傳輸的請求。對於可能不使用 HTTP/2 為客戶提供服務的中介軟體尤其如此。嘗試正常關閉連線的伺服器應該傳送一個初始 GOAWAY 幀,最後一個流識別符號設定為 2^31-1 和 NO_ERROR code。這向客戶端發出即將關閉的訊號,並禁止發起進一步的請求。在允許任何傳輸中流建立的時間(至少一個往返時間)之後,伺服器可以傳送具有更新的最後流識別符號的另一個 GOAWAY 幀。這可確保在不丟失請求的情況下徹底關閉連線。

在傳送 GOAWAY 幀之後,傳送端能丟棄流識別符號大於最終流標識的流的幀。但是,任何改變連線狀態的幀都不能完全忽略。例如,必須對 HEADERS,PUSH_PROMISE 和 CONTINUATION 幀進行最低限度的處理,以確保為頭壓縮保持的狀態是一致的;類似地,DATA 幀必須被計算入連線的流量控制視窗中。如果無法處理這些幀可能會導致流量控制或報頭壓縮狀態變得不同步。

GOAWAY 幀還包含一個 32 位錯誤程式碼,其中包含關閉連線的原因。端點可以將不透明資料附加到任何 GOAWAY 幀的有效載荷中。其他除錯資料僅用於診斷目的,不帶語義值。除錯資訊可能包含安全或隱私敏感資料。記錄或以其他方式持久儲存的除錯資料必須有足夠的安全措施來防止未經授權的訪問。

GOAWAY 幀也比較簡單。R 是保留標誌位。這個幀的 Stream ID 是 0,說明它即將要關閉連線,promised 流識別符號必須是傳送方傳送的下一個流的有效選擇,promised-stream-ID 是 3 。這裡的 Error Code 是 NO_ERROR (0x0)。

九. WINDOW_UPDATE 幀

WINDOW_UPDATE幀(型別 = 0x8) 用於實現流量控制;有關概述。

流量控制在兩個級別上執行:在每個單獨的流上和整個連線上。

所有型別的流量控制都是逐跳的,即僅在兩個端點之間。中介軟體不在依賴的連線之間轉發 WINDOW_UPDATE 幀。但是,任何接收方對資料傳輸的限制都可能間接導致流量控制資訊向原始傳送方傳播。

流量控制僅針對直接建立 TCP 連線的兩端。如果對端是代理伺服器,代理伺服器不需要向上遊轉發 WINDOW_UPDATE 幀。不過接收端縮小流量控制的視窗會最終傳遞到源傳送端。

流量控制僅適用於被識別為受流量控制的幀。在 HTTP/2 中定義的幀型別中,這僅包括 DATA 幀。除非接收方無法為處理幀分配資源,否則必須接受和處理免於流量控制的幀。如果接收方無法接受幀,則可以使用 FLOW_CONTROL_ERROR 型別的流錯誤或連線錯誤進行響應。

C
    +-+-------------------------------------------------------------+
    |R|              Window Size Increment (31)                     |
    +-+-------------------------------------------------------------+

WINDOW_UPDATE 幀的有效負載是一個保留位加上無符號 31 位整數,表示除現有流量控制視窗外,傳送方可以傳送的八位位元組數。流量控制視窗增量的合法範圍是 1 到 2^31-1 (2,147,483,647) 個八位位元組。

WINDOW_UPDATE 幀沒有定義任何 flag 標誌。WINDOW_UPDATE 幀可以特定於流或整個連線。在前一種情況下,幀的流識別符號指示受影響的流; 在後者中,值 "0" 表示整個連線都受這個幀的影響。

接收方必須將流量控制視窗增量為 0 的 WINDOW_UPDATE 幀的接收視為 PROTOCOL_ERROR 型別的流錯誤;連線的流量控制視窗上的錯誤必須被視為連線錯誤。

WINDOW_UPDATE 可以由已經發送帶有 END_STREAM 標誌的幀的對端傳送。這意味著接收方可以在 "半封閉(遠端)" 或 "關閉" 流上接收 WINDOW_UPDATE 幀。接收方不得將此視為錯誤。

接收到流量控制幀的接收方必須始終考慮其對連線的流量控制視窗的影響,除非接收方將其視為連線錯誤。即使幀出錯,這也是必要的。因為傳送方將這個幀計入了流量控制視窗,如果接收方沒有這樣做,傳送方和接收方的流量控制就會不相同了。長度不是 4 個八位位元組的 WINDOW_UPDATE 幀必須被視為 FRAME_SIZE_ERROR 型別的連線錯誤。

1. The Flow-Control Window

HTTP/2 中的流量控制是通過每個流上每個傳送者保留一個視窗來實現的。流量控制視窗是一個簡單的整數值,表示允許傳送方傳輸多少個八位位元組的資料;因此,它的大小是接收方緩衝容量的能力。

流量控制視窗對流和連線的流量控制視窗都適用。傳送方不得傳送長度超過接收方公佈的任一流量控制視窗中的可用空間長度的流量控制幀。如果在任一流量控制視窗中沒有可用空間,則可以傳送設定了 END_STREAM 標誌的長度為零的幀(即,空的 DATA 幀)。對於流量控制計算,不計算9個八位位元組的幀頭。在傳送流量控制幀之後,傳送方通過傳送幀的長度,來減少兩個視窗中可用的空間。

幀的接收方傳送 WINDOW_UPDATE 幀,當它消耗資料並釋放流量控制視窗中的空間。為流級和連線級的流量控制視窗傳送單獨的 WINDOW_UPDATE 幀。接收到 WINDOW_UPDATE 幀的傳送方按幀中指定的量更新相應的視窗。

傳送方應該禁止使流量控制視窗超過 2^31-1 個八位位元組。如果傳送方收到導致流量控制視窗超過此最大值的 WINDOW_UPDATE,則必須根據需要終止流或連線。對於流,傳送方傳送 RST_STREAM,錯誤程式碼為 FLOW_CONTROL_ERROR; 對於連線,傳送錯誤程式碼為 FLOW_CONTROL_ERROR 的 GOAWAY 幀。

來自發送方的流量控制幀和來自接收方的 WINDOW_UPDATE 幀相互完全非同步。此屬性允許接收方積極更新發送方保留的視窗大小,以防止流停止運轉。

2. Initial Flow-Control Window Size

首次建立 HTTP/2 連線時,將建立新流,初始流量控制視窗大小為 65,535 個八位位元組。連線的流量控制視窗也是 65,535 個八位位元組。兩個端點都可以通過在 SETTINGS 幀中包含 SETTINGS_INITIAL_WINDOW_SIZE 的值來調整新流的初始視窗大小,該幀構成連線前奏的一部分。只能使用 WINDOW_UPDATE 幀更改連線的流量控制視窗。

在接收為 SETTINGS_INITIAL_WINDOW_SIZE 設定值的 SETTINGS 幀之前,端點在傳送流控幀時只能使用預設的初始視窗大小。類似地,連線的流量控制視窗設定為預設初始視窗大小,直到收到 WINDOW_UPDATE 幀。

除了更改尚未啟用的流的流量控制視窗之外,SETTINGS 幀還可以更改具有活動流量控制視窗的流的初始流量控制視窗大小(即,"開啟" 或 "半關閉(遠端)" 狀態)。當 SETTINGS_INITIAL_WINDOW_SIZE 的值發生變化時,接收方必須通過新值和舊值之間的差異來調整它維護的所有流量控制視窗的大小。

對 SETTINGS_INITIAL_WINDOW_SIZE 的更改可能導致流量控制視窗中的可用空間變為負數。傳送方必須跟蹤負流量控制視窗並且不得傳送新的流量控制幀,直到它收到使流量控制視窗變為正的 WINDOW_UPDATE 幀。例如,如果客戶端在建立連線時立即傳送 60 KB,並且伺服器將初始視窗大小設定為 16 KB,則客戶端將在收到 SETTINGS 幀時重新計算可用的流量控制視窗為 -44 KB。客戶端保留負流量控制視窗,直到 WINDOW_UPDATE 幀將視窗恢復為正值,之後客戶端可以恢復傳送。

SETTINGS 幀不能改變連線的流量控制視窗。

端點必須將因為處理對 SETTINGS_INITIAL_WINDOW_SIZE 的更改而導致任何流量控制視窗超過最大大小的這種情況視為 FLOW_CONTROL_ERROR 型別的連線錯誤。

3. Reducing the Stream Window Size

希望使用比當前大小更小的流量控制視窗的接收方可以傳送新的 SETTINGS 幀。但是,接收方必須準備接收超過此視窗大小的資料,因為傳送方可能會在處理 SETTINGS 幀之前傳送超過下限的資料。

在傳送減少初始流量控制視窗大小的 SETTINGS 幀之後,接收方可以繼續處理超過流量控制限制的流。 允許流繼續禁止接收方立即減少它為流量控制視窗預留的空間。由於需要 WINDOW_UPDATE 幀以允許傳送方繼續傳送,因此這些流的進度也會停滯。接收方也可以為受影響的流傳送一個錯誤程式碼為FLOW_CONTROL_ERROR 的 RST_STREAM。

4. For Example

在上圖的抓包中可以看到,WINDOW_UPDATE 幀的 R 位是 0,Window Size Increment 是 2147418112 。

十. CONTINUATION 幀

CONTINUATION 幀(型別 = 0x9) 用於繼續一系列 header block fragments 頭塊片段。只要前一幀在同一個流上並且是沒有設定 END_HEADERS 標誌的 HEADERS,PUSH_PROMISE 或 CONTINUATION 幀,就可以傳送任意數量的 CONTINUATION 幀。此幀專門用於傳遞較大 HTTP 頭部時的持續幀。

C
    +---------------------------------------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+

CONTINUATION 幀的有效載荷中包含一個 header block fragments 頭塊片段。

CONTINUATION 幀中定義瞭如下的 flag:

  • END_HEADERS (0x4):
    當這個欄位被設定的時候,位 2 表示該幀結束一個頭塊。如果未設定 END_HEADERS 位,則此幀必須後跟另一個 CONTINUATION 幀。接收方必須將接收任何其他型別的幀或不同流上的幀視為 PROTOCOL_ERROR 型別的連線錯誤。

CONTINUATION 幀改變了中定義的連線狀態。CONTINUATION 幀必須與一個流相關聯。如果接收到其流識別符號欄位為 0x0 的 CONTINUATION 幀,則接收方必須以 PROTOCOL_ERROR 型別的連線錯誤進行響應。

一個 CONTINUATION 幀必須在 HEADERS,PUSH_PROMISE 或 CONTINUATION 幀之前,並且沒有設定 END_HEADERS 標誌。觀察到有違反此規則的接收方必須響應 PROTOCOL_ERROR 型別的連線錯誤。

可以把 CONTINUATION 幀當做特殊的 HEADERAS 幀。那麼為什麼要設計這個新的型別的幀呢?而不是直接使用 HEADERAS 幀?如果重複使用 HEADERAS 幀,那麼後續的 HEADERAS 幀的負載就需要經過特殊處理才能和之前的拼接起來,這些幀首部是否需要重複?如果幀之間存在分歧該怎麼辦?協議制定者覺得這些都是模稜兩可的問題,在未來可能還會引起麻煩,所以工作組決定增加一個明確的幀型別,以避免實現混淆。

由於 HEADERAS 幀和 CONTINUATION 幀必須是有序的,所以使用 CONTINUATION 幀會破壞或者減少多路複用的好處。CONTINUATION 幀是解決重要場景(大首部)的工具,但只能在必要時使用。

十一. Error Codes

錯誤程式碼是在 RST_STREAM 和 GOAWAY 幀中使用的 32 位欄位,用於表示流或連線錯誤的原因。錯誤程式碼共享公共程式碼空間。某些錯誤程式碼僅適用於流或整個連線,並且在其他上下文中沒有定義的語義。

定義了以下錯誤程式碼:

  • NO_ERROR (0x0):
    關聯條件不是錯誤的結果。例如,GOAWAY 可能包含此程式碼以指示正常關閉連線。

  • PROTOCOL_ERROR (0x1):
    端點檢測到非特定協議錯誤。當更具體的錯誤程式碼不可用時,將使用此錯誤。

  • INTERNAL_ERROR (0x2):
    端點遇到意外的內部錯誤。

  • FLOW_CONTROL_ERROR (0x3):
    端點檢測到其對端違反了流量控制協議。

  • SETTINGS_TIMEOUT (0x4):
    端點發送了 SETTINGS 幀,但沒有及時收到響應。

  • STREAM_CLOSED (0x5):
    端點在流半關閉後收到一幀。stream 已經處於半關閉狀態不再接收 frame 幀的時候,又接收到了 frame 幀。

  • FRAME_SIZE_ERROR (0x6):
    端點收到的幀大小無效。

  • REFUSED_STREAM (0x7):
    端點在執行任何應用程式處理之前拒絕了流。

  • CANCEL (0x8):
    端點用於指示不再需要該流。

  • COMPRESSION_ERROR (0x9):
    端點無法維護連線的頭壓縮上下文。

  • CONNECT_ERROR (0xa):
    為響應 CONNECT 請求而建立的連線被重置或異常關閉。

  • ENHANCE_YOUR_CALM (0xb):
    端點檢測到其對端正在表現出可能產生過多負載的行為。提醒對方"冷靜"點。

  • INADEQUATE_SECURITY (0xc):
    底層傳輸具有不滿足最低安全要求的屬性。

  • HTTP_1_1_REQUIRED (0xd):
    端點要求使用 HTTP/1.1 而不是 HTTP/2。

未知或不支援的錯誤程式碼不得觸發任何特殊行為。這些可以被實現視為等同於 INTERNAL_ERROR。


Reference:

RFC 7540