1.概述
- 和http1兼容。HTTP/2 沒有改動 HTTP 的應用語義。 HTTP 方法、狀態代碼、URI 和標頭字段等核心概念一如往常。 不過,HTTP/2 修改了數據格式化(分幀)以及在客戶端與服務器間傳輸的方式。因此,所有現有的應用都可以不必修改而在新協議下運行。
- 傳輸方式改變。在HTTP/1.x中,每個 TCP 連接同時只能處理一個請求 - 響應,一個請求就會獨占一個鏈接,用戶想要多個並行的請求來提高性能,必須得使用多個TCP連接(對於同一個域名Chrome最多只能同時創建 6 個 TCP 連接)。這也是被人吐槽的原因,也是http2 要解決的一個痛點,解決方法是在鏈接的基礎上提出了stream的概念,通過stream 來區別不同的請求 。
2.連接多路復用
在一個 TCP 連接上,我們可以向對方不斷發送幀,每幀的 stream identifier 的標明這一幀屬於哪個流,然后在對方接收時,根據 stream identifier 拼接每個流的所有幀組成一整塊數據。
把 HTTP/1.1 每個請求都當作一個流,那么多個請求變成多個流,請求響應數據分成多個幀,不同流中的幀交錯地發送給對方,這就是 HTTP/2 中的多路復用。
流的概念實現了單連接上多請求 - 響應並行,解決了線頭阻塞的問題。
(1) 幀
+---+-----------+------------+-------------+-------------+ |Bit| 0...7 | 8...15 | 16...23 | 24...31 | +---+-----------+------------+-------------+-------------+ |0 | Length | Type | +---+-----------+--------------------------+-------------+ |32 |Flags | +---+-+---------+----------------------------------------+ |40 |R| Stream Identifier | +---+-+--------------------------------------------------+ |...| Frame Payload | +---+----------------------------------------------------+
字段含義:
- Length 代表整個 frame 的長度,用一個 24 位無符號整數表示。
- Type 定義 frame 的類型,用 8 bits 表示。幀類型決定了幀主體的格式和語義,如果 type 為 unknown 應該忽略或拋棄。
- Flags 是為幀類型相關而預留的布爾標識。標識對於不同的幀類型賦予了不同的語義。如果該標識對於某種幀類型沒有定義語義,則它必須被忽略且發送的時候應該賦值為 (0x0) 。
- R 是一個保留的比特位。這個比特的語義沒有定義,發送時它必須被設置為 (0x0), 接收時需要忽略。
- Stream Identifier 用作流控制,用 31 位無符號整數表示。客戶端建立的 sid 必須為奇數,服務端建立的 sid 必須為偶數,值 (0x0) 保留給與整個連接相關聯的幀 (連接控制消息),而不是單個流 。
- Frame Payload 是主體內容,由幀類型決定。
共分為十種類型的幀:
- HEADERS: 報頭幀 (type=0x1),用來打開一個流或者攜帶一個首部塊片段
- DATA: 數據幀 (type=0x0),裝填主體信息,可以用一個或多個 DATA 幀來返回一個請求的響應主體
- PRIORITY: 優先級幀 (type=0x2),指定發送者建議的流優先級,可以在任何流狀態下發送 PRIORITY 幀,包括空閑 (idle) 和關閉 (closed) 的流
- RST_STREAM: 流終止幀 (type=0x3),用來請求取消一個流,或者表示發生了一個錯誤,payload 帶有一個 32 位無符號整數的錯誤碼 (Error Codes),不能在處於空閑 (idle) 狀態的流上發送 RST_STREAM 幀
- SETTINGS: 設置幀 (type=0x4),設置此 連接 的參數,作用於整個連接
- PUSH_PROMISE: 推送幀 (type=0x5),服務端推送,客戶端可以返回一個 RST_STREAM 幀來選擇拒絕推送的流
- PING: PING 幀 (type=0x6),判斷一個空閑的連接是否仍然可用,也可以測量最小往返時間 (RTT)
- GOAWAY: GOWAY 幀 (type=0x7),用於發起關閉連接的請求,或者警示嚴重錯誤。GOAWAY 會停止接收新流,並且關閉連接前會處理完先前建立的流
- WINDOW_UPDATE: 窗口更新幀 (type=0x8),用於執行流量控制功能,可以作用在單獨某個流上 (指定具體 Stream Identifier) 也可以作用整個連接 (Stream Identifier 為 0x0),只有 DATA 幀受流量控制影響。初始化流量窗口后,發送多少負載,流量窗口就減少多少,如果流量窗口不足就無法發送,WINDOW_UPDATE 幀可以增加流量窗口大小
- CONTINUATION: 延續幀 (type=0x9),用於繼續傳送首部塊片段序列,見 首部的壓縮與解壓縮
(2) 消息
一個HTTP2請求或響應被分為N個幀傳送,在另一端再重新組合為完整的消息。
(3) 流
流是一個邏輯上的概念,代表 HTTP/2 連接中在客戶端和服務器之間交換的獨立雙向幀序列,每個幀的 Stream Identifier 字段指明了它屬於哪個流。 流有以下特性:
- 單個連接可以包含多個流,兩端之間可以交叉發送不同流的幀
- 流可以由客戶端或服務器來單方面地建立和使用,或者共享
- 流可以由任一方關閉
- 幀在流上發送的順序很重要,最后接收方會把相同 Stream Identifier (同一個流) 的幀重新組裝成完整的消息
3.HTTP頭壓縮
簡單說,HTTP頭壓縮需要在HTTP/2 客戶端和服務端之間:
- 維護一份相同的靜態表(Static Table),包含常見的頭部名稱,以及特別常見的頭部名稱與值的組合;
- 維護一份相同的動態表(Dynamic Table),可以動態地添加內容;
- 基於靜態哈夫曼碼表的哈夫曼編碼(Huffman Coding);
(1) 靜態索引
靜態索引表是固定的,對於客戶端服務端都一樣,目前協議商定的靜態索引包含 61 個鍵值,詳見 Static Table Definition - RFC 7541
前幾個如下
索引 | 字段值 | 鍵值 |
---|---|---|
1 | :authority | |
2 | :method | GET |
3 | :method | POST |
4 | :path | / |
5 | :path | /index.html |
6 | :scheme | http |
7 | :scheme | https |
8 | :status | 200 |
(2) 動態索引
動態索引表是一個 FIFO 隊列維護的有空間限制的表,里面含有非靜態表的索引。
動態索引表是需要連接雙方維護的,其內容基於連接上下文,一個 HTTP2 連接有且僅有一份動態表。
當一個首部匹配不到索引時,可以選擇把它插入動態索引表中,下次同名的值就可能會在表中查到索引並替換。
但是並非所有首部鍵值都會存入動態索引,因為動態索引表是有空間限制的,最大值由 SETTING 幀中的 SETTINGS_HEADER_TABLE_SIZE (默認 4096 字節) 設置
4.HTTP/2 的協議協商機制
客戶端在不知道服務器是否支持HTTP2協議時需要使用HTTP Upgrade 機制。
客戶端首先發起一個 HTTP/1.1 請求,其中包含Upgrade首部字段,還必須包含一個且只能一個 HTTP2-Settings 首部字段。
例如:
GET / HTTP/1.1 Host: server.example.com Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
- Upgrade。"h2c" 標示運行在明文 TCP 之上的 HTTP/2 協議。"h2" 標示使用了 TLS(Transport Layer Security)[TLS12] 的 HTTP/2 協議
- HTTP2-Settings。是一個連接相關的首部字段,它提供了用於管理 HTTP/2 連接的參數(前提是服務端接受了升級請求)
不支持 HTTP/2 的服務端響應請求時,可以認為 Upgrade 首部字段不存在。
支持 HTTP/2 的服務器響應狀態碼 101 (Switching Protocols) 表示接受升級協議的請求。在結束 101 響應之后,服務端可以開始發送 HTTP/2 幀。
例如:
HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: h2c [ HTTP/2 connection ...
客戶端一收到 101(Switching Protocols) 響應(表示成功升級)后,就發送客戶端連接前奏。客戶端連接前奏以一個固定的24字節的序列開始,用十六進制表示為:0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a(相當於一個魔法值,在WireSark可以看到標識為Magic)。
為了避免不必要的延遲,允許客戶端發送完連接前奏后就立即向服務端發送其他的幀,而不必等待服務端的連接前奏。
服務端發送的第一個 HTTP/2 幀必須是由一個 SETTINGS 幀組成的服務端連接前奏。
兩端都要發送一個連接前奏,作為對所使用協議的最終確認,並確定 HTTP/2 連接的初始設置。
5.流量控制
多路復用的流會競爭 TCP 資源,進而導致流被阻塞。流控制機制確保同一連接上的流不會相互干擾。流量控制作用於單個流或整個連接。HTTP/2 通過使用 WINDOW_UPDATE 幀來提供流量控制。 流控制具有以下特征:
- 流量控制是特定於連接的。兩種級別的流量控制都位於單跳的端點之間,而不是整個端到端的路徑。比如 server 前面有一個 front-end proxy 如 Nginx,這時就會有兩個 connection,browser-Nginx, Nginx—server,flow control 分別作用於兩個 connection。詳情見: How is HTTP/2 hop-by-hop flow control accomplished? - stackoverflow
- 流量控制是基於 WINDOW_UPDATE 幀的。接收方公布自己打算在每個流以及整個連接上分別接收多少字節。這是一個以信用為基礎的方案。
- 流量控制是有方向的,由接收者全面控制。接收方可以為每個流和整個連接設置任意的窗口大小。發送方必須尊重接收方設置的流量控制限制。客戶方、服務端和中間代理作為接收方時都獨立地公布各自的流量控制窗口,作為發送方時都遵守對端的流量控制設置。
- 無論是新流還是整個連接,流量控制窗口的初始值是 65535 字節。
- 幀的類型決定了流量控制是否適用於幀。目前,只有 DATA 幀會受流量控制影響,所有其它類型的幀並不消耗流量控制窗口的空間。這保證了重要的控制幀不會被流量控制阻塞。
- 流量控制不能被禁用。
- HTTP/2 只定義了 WINDOW_UPDATE 幀的格式和語義,並沒有規定接收方如何決定何時發送幀、發送什么樣的值,也沒有規定發送方如何選擇發送包。具體實現可以選擇任何滿足需求的算法。
(1) WINDOW_UPDATE 幀格式
+-+-------------------------------------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
- Window Size Increment 表示除了現有的流量控制窗口之外,發送端還可以傳送的字節數。取值范圍是 1 到 2^31 - 1 字節。
WINDOW_UPDATE 幀可以是針對一個流或者是針對整個連接的。如果是前者,WINDOW_UPDATE 幀的流標識符指明了受影響的流;如果是后者,流標識符為 0 表示作用於整個連接。
流量控制功能只適用於被標識的、受流量控制影響的幀。文檔定義的幀類型中,只有 DATA 幀受流量控制影響。除非接收端不能再分配資源去處理這些幀,否則不受流量控制影響的幀必須被接收並處理。如果接收端不能再接收幀了,可以響應一個 FLOW_CONTROL_ERROR 類型的流錯誤或者連接錯誤。
(2) 流量控制窗口
流量控制窗口是一個簡單的整數值,指出了准許發送端傳送的數據的字節數。窗口值衡量了接收端的緩存能力。
新建連接時,流和連接的初始窗口大小都是 2^16 - 1(65535) 字節。可以通過設置連接前言中 SETTINGS 幀的 SETTINGS_INITIAL_WINDOW_SIZE 參數改變流的初始窗口大小,這會作用於所有流。而連接的初始窗口大小不能改,但可以用 WINDOW_UPDATE 幀來改變流量控制窗口,這是為什么連接前言往往帶有一個WINDOW_UPDATE 幀的原因。