深入剖析 HTTP3 協議
自 2017 年起 HTTP3 協議已發佈了 34 個 Draft,推出在即,Chrome、Nginx 等軟件都在跟進實現最新的草案。本文將介紹 HTTP3 協議規範、應用場景及實現原理。
2015 年 HTTP2 協議正式推出後,已經有接近一半的互聯網站點在使用它:
(圖片來自 https://w3techs.com/technologies/details/ce-http2)
HTTP2 協議雖然大幅提升了 HTTP/1.1 的性能,然而,基於 TCP 實現的 HTTP2 遺留下 3 個問題:
- 有序字節流引出的 隊頭阻塞(Head-of-line blocking),使得 HTTP2 的多路複用能力大打折扣;
- TCP 與 TLS 疊加了握手時延,建鏈時長還有 1 倍的下降空間;
- 基於 TCP 四元組確定一個連接,這種誕生於有線網絡的設計,並不適合移動狀態下的無線網絡,這意味着 IP 地址的頻繁變動會導致 TCP 連接、TLS 會話反覆握手,成本高昂。
HTTP3 協議解決了這些問題:
- HTTP3 基於 UDP 協議重新定義了連接,在 QUIC 層實現了無序、併發字節流的傳輸,解決了隊頭阻塞問題(包括基於 QPACK 解決了動態表的隊頭阻塞);
- HTTP3 重新定義了 TLS 協議加密 QUIC 頭部的方式,既提高了網絡攻擊成本,又降低了建立連接的速度(僅需 1 個 RTT 就可以同時完成建鏈與密鑰協商);
- HTTP3 將 Packet、QUIC Frame、HTTP3 Frame 分離,實現了連接遷移功能,降低了 5G 環境下高速移動設備的連接維護成本。
本文將會從 HTTP3 協議的概念講起,從連接遷移的實現上學習 HTTP3 的報文格式,再圍繞着隊頭阻塞問題來分析多路複用與 QPACK 動態表的實現。雖然正式的 RFC 規範還未推出,但最近的草案 Change 只有微小的變化,所以現在學習 HTTP3 正當其時,這將是下一代互聯網最重要的基礎設施。本文也是我在 2020 年 8 月 3 號 Nginx 中文社區與 QCON 共同組織的 QCON 公開課中部分內容的文字總結。
HTTP3 協議到底是什麼?
就像 HTTP2 協議一樣,HTTP3 並沒有改變 HTTP1 的語義。那什麼是 HTTP 語義呢?在我看來,它包括以下 3 個點:
- 請求只能由客戶端發起,而服務器針對每個請求返回一個響應;
- 請求與響應都由 Header、Body(可選)組成,其中請求必須含有 URL 和方法,而響應必須含有響應碼;
- Header 中各 Name 對應的含義保持不變。
HTTP3 在保持 HTTP1 語義不變的情況下,更改了編碼格式,這由 2 個原因所致:
首先,是爲了減少編碼長度。下圖中 HTTP1 協議的編碼使用了 ASCII 碼,用空格、冒號以及 \ r\n 作爲分隔符,編碼效率很低:
HTTP2 與 HTTP3 採用二進制、靜態表、動態表與 Huffman 算法對 HTTP Header 編碼,不只提供了高壓縮率,還加快了發送端編碼、接收端解碼的速度。
其次,由於 HTTP1 協議不支持多路複用,這樣高併發只能通過多開一些 TCP 連接實現。然而,通過 TCP 實現高併發有 3 個弊端:
- 實現成本高。TCP 是由操作系統內核實現的,如果通過多線程實現併發,併發線程數不能太多,否則線程間切換成本會以指數級上升;如果通過異步、非阻塞 socket 實現併發,開發效率又太低;
- 每個 TCP 連接與 TLS 會話都疊加了 2-3 個 RTT 的建鏈成本;
- TCP 連接有一個防止出現擁塞的慢啓動流程,它會對每個 TCP 連接都產生減速效果。
因此,HTTP2 與 HTTP3 都在應用層實現了多路複用功能:
(圖片來自:https://blog.cloudflare.com/http3-the-past-present-and-future/)
HTTP2 協議基於 TCP 有序字節流實現,因此**應用層的多路複用並不能做到無序地併發,在丟包場景下會出現隊頭阻塞問題。**如下面的動態圖片所示,服務器返回的綠色響應由 5 個 TCP 報文組成,而黃色響應由 4 個 TCP 報文組成,當第 2 個黃色報文丟失後,即使客戶端接收到完整的 5 個綠色報文,但 TCP 層不會允許應用進程的 read 函數讀取到最後 5 個報文,併發成了一紙空談:
當網絡繁忙時,丟包概率會很高,多路複用受到了很大限制。因此, HTTP3 採用 UDP 作爲傳輸層協議,重新實現了無序連接,並在此基礎上通過有序的 QUIC Stream 提供了多路複用 ,如下圖所示:
(圖片來自:https://blog.cloudflare.com/http3-the-past-present-and-future/)
最早這一實驗性協議由 Google 推出,並命名爲 gQUIC,因此,IETF 草案中仍然保留了 QUIC 概念,用來描述 HTTP3 協議的傳輸層和表示層。HTTP3 協議規範由以下 5 個部分組成:
- QUIC 層由 https://tools.ietf.org/html/draft-ietf-quic-transport-29 描述,它定義了連接、報文的可靠傳輸、有序字節流的實現;
- TLS 協議會將 QUIC 層的部分報文頭部暴露在明文中,方便代理服務器進行路由。https://tools.ietf.org/html/draft-ietf-quic-tls-29 規範定義了 QUIC 與 TLS 的結合方式;
- 丟包檢測、RTO 重傳定時器預估等功能由 https://tools.ietf.org/html/draft-ietf-quic-recovery-29 定義,目前擁塞控制使用了類似 TCP New RENO 的算法,未來有可能更換爲基於帶寬檢測的算法(例如 BBR);
- 基於以上 3 個規範,https://tools.ietf.org/html/draft-ietf-quic-http-29 定義了 HTTP 語義的實現,包括服務器推送、請求響應的傳輸等;
- 在 HTTP2 中,由 HPACK 規範定義 HTTP 頭部的壓縮算法。由於 HPACK 動態表的更新具有時序性,無法滿足 HTTP3 的要求。在 HTTP3 中,QPACK 定義 HTTP 頭部的編碼:https://tools.ietf.org/html/draft-ietf-quic-qpack-16。注意,以上規範的最新草案都到了 29,而 QPACK 相對簡單,它目前更新到 16。
自 1991 年誕生的 HTTP/0.9 協議已不再使用, 但 1996 推出的 HTTP/1.0、1999 年推出的 HTTP/1.1、2015 年推出的 HTTP2 協議仍然共存於互聯網中(HTTP/1.0 在企業內網中還在廣爲使用,例如 Nginx 與上游的默認協議還是 1.0 版本),即將面世的 HTTP3 協議的加入,將會進一步增加協議適配的複雜度 。接下來,我們將深入 HTTP3 協議的細節。
連接遷移功能是怎樣實現的?
對於當下的 HTTP1 和 HTTP2 協議,傳輸請求前需要先完成耗時 1 個 RTT 的 TCP 三次握手、耗時 1 個 RTT 的 TLS 握手(TLS1.3),由於它們分屬內核實現的傳輸層、openssl 庫實現的表示層,所以難以合併在一起,如下圖所示:
(圖片來自:https://blog.cloudflare.com/http3-the-past-present-and-future/)
在 IoT 時代,移動設備接入的網絡會頻繁變動,從而導致設備 IP 地址改變。**對於通過四元組(源 IP、源端口、目的 IP、目的端口)定位連接的 TCP 協議來說,這意味着連接需要斷開重連,所以上述 2 個 RTT 的建鏈時延、TCP 慢啓動都需要重新來過。**而 HTTP3 的 QUIC 層實現了連接遷移功能,允許移動設備更換 IP 地址後,只要仍保有上下文信息(比如連接 ID、TLS 密鑰等),就可以複用原連接。
在 UDP 報文頭部與 HTTP 消息之間,共有 3 層頭部,定義連接且實現了 Connection Migration 主要是在 Packet Header 中完成的,如下圖所示:
這 3 層 Header 實現的功能各不相同:
- Packet Header 實現了可靠的連接。當 UDP 報文丟失後,通過 Packet Header 中的 Packet Number 實現報文重傳。連接也是通過其中的 Connection ID 字段定義的;
- QUIC Frame Header 在無序的 Packet 報文中,基於 QUIC Stream 概念實現了有序的字節流,這允許 HTTP 消息可以像在 TCP 連接上一樣傳輸;
- HTTP3 Frame Header 定義了 HTTP Header、Body 的格式,以及服務器推送、QPACK 編解碼流等功能。
爲了進一步提升網絡傳輸效率,Packet Header 又可以細分爲兩種:
- Long Packet Header 用於首次建立連接;
- Short Packet Header 用於日常傳輸數據。
其中,Long Packet Header 的格式如下圖所示:
建立連接時,連接是由服務器通過 Source Connection ID 字段分配的,這樣,後續傳輸時,雙方只需要固定住 Destination Connection ID,就可以在客戶端 IP 地址、端口變化後,繞過 UDP 四元組(與 TCP 四元組相同),實現連接遷移功能。下圖是 Short Packet Header 頭部的格式,這裏就不再需要傳輸 Source Connection ID 字段了:
上圖中的 Packet Number 是每個報文獨一無二的序號,基於它可以實現丟失報文的精準重發。如果你通過抓包觀察 Packet Header,會發現 Packet Number 被 TLS 層加密保護了,這是爲了防範各類網絡攻擊的一種設計。下圖給出了 Packet Header 中被加密保護的字段:
其中,顯示爲 E(Encrypt)的字段表示被 TLS 加密過。當然,Packet Header 只是描述了最基本的連接信息,其上的 Stream 層、HTTP 消息也是被加密保護的:
現在我們已經對 HTTP3 協議的格式有了基本的瞭解,接下來我們通過隊頭阻塞問題,看看 Packet 之上的 QUIC Frame、HTTP3 Frame 幀格式。
Stream 多路複用時的隊頭阻塞是怎樣解決的?
其實,解決隊頭阻塞的方案,就是允許微觀上有序發出的 Packet 報文,在接收端無序到達後也可以應用於併發請求中。比如上文的動態圖中,如果丟失的黃色報文對其後發出的綠色報文不造成影響,隊頭阻塞問題自然就得到了解決:
在 Packet Header 之上的 QUIC Frame Header,定義了有序字節流 Stream,而且 Stream 之間可以實現真正的併發。HTTP3 的 Stream,借鑑了 HTTP2 中的部分概念,所以在討論 QUIC Frame Header 格式之前,我們先來看看 HTTP2 中的 Stream 長成什麼樣子:
(圖片參見:https://developers.google.com/web/fundamentals/performance/http2)
每個 Stream 就像 HTTP1 中的 TCP 連接,它保證了承載的 HEADERS frame(存放 HTTP Header)、DATA frame(存放 HTTP Body)是有序到達的,多個 Stream 之間可以並行傳輸。在 HTTP3 中,上圖中的 HTTP2 frame 會被拆解爲兩層,我們先來看底層的 QUIC Frame。
一個 Packet 報文中可以存放多個 QUIC Frame,當然所有 Frame 的長度之和不能大於 PMTUD(Path Maximum Transmission Unit Discovery,這是大於 1200 字節的值),你可以把它與 IP 路由中的 MTU 概念對照理解:
前 4 個字節的 Frame Type 字段描述的類型不同,接下來的編碼也不相同,下表是各類 Frame 的 16 進制 Type 值:
在上表中,我們只要分析 0x08-0x0f 這 8 種 STREAM 類型的 Frame,就能弄明白 Stream 流的實現原理,自然也就清楚隊頭阻塞是怎樣解決的了。Stream Frame 用於傳遞 HTTP 消息,它的格式如下所示:
可見,Stream Frame 頭部的 3 個字段,完成了多路複用、有序字節流以及報文段層面的二進制分隔功能,包括:
- Stream ID 標識了一個有序字節流。當 HTTP Body 非常大,需要跨越多個 Packet 時,只要在每個 Stream Frame 中含有同樣的 Stream ID,就可以傳輸任意長度的消息。多個併發傳輸的 HTTP 消息,通過不同的 Stream ID 加以區別;
- 消息序列化後的 “有序” 特性,是通過 Offset 字段完成的,它類似於 TCP 協議中的 Sequence 序號,用於實現 Stream 內多個 Frame 間的累計確認功能;
- Length 指明瞭 Frame 數據的長度。
你可能會奇怪,爲什麼會有 8 種 Stream Frame 呢?這是因爲 0x08-0x0f 這 8 種類型其實是由 3 個二進制位組成,它們實現了以下 3 標誌位的組合:
- 第 1 位表示是否含有 Offset,當它爲 0 時,表示這是 Stream 中的起始 Frame,這也是上圖中 Offset 是可選字段的原因;
- 第 2 位表示是否含有 Length 字段;
- 第 3 位 Fin,表示這是 Stream 中最後 1 個 Frame,與 HTTP2 協議 Frame 幀中的 FIN 標誌位相同。
Stream 數據中並不會直接存放 HTTP 消息,因爲 HTTP3 還需要實現服務器推送、權重優先級設定、流量控制等功能,所以 Stream Data 中首先存放了 HTTP3 Frame:
其中,Length 指明瞭 HTTP 消息的長度,而 Type 字段(請注意,低 2 位有特殊用途,在 QPACK 章節中會詳細介紹)包含了以下類型:
- 0x00:DATA 幀,用於傳輸 HTTP Body 包體;
- 0x01:HEADERS 幀,通過 QPACK 編碼,傳輸 HTTP Header 頭部;
- 0x03:CANCEL_PUSH 控制幀,用於取消 1 次服務器推送消息,通常客戶端在收到 PUSH_PROMISE 幀後,通過它告知服務器不需要這次推送;
- 0x04:SETTINGS 控制幀,設置各類通訊參數;
- 0x05:PUSH_PROMISE 幀,用於服務器推送 HTTP Body 前,先將 HTTP Header 頭部發給客戶端,流程與 HTTP2 相似;
- 0x07:GOAWAY 控制幀,用於關閉連接(注意,不是關閉 Stream);
- 0x0d:MAX_PUSH_ID,客戶端用來限制服務器推送消息數量的控制幀。
總結一下,QUIC Stream Frame 定義了有序字節流,且多個 Stream 間的傳輸沒有時序性要求,這樣,HTTP 消息基於 QUIC Stream 就實現了真正的多路複用,隊頭阻塞問題自然就被解決掉了。
QPACK 編碼是如何解決隊頭阻塞問題的?
最後,我們再看下 HTTP Header 頭部的編碼方式,它需要面對另一種隊頭阻塞問題。
與 HTTP2 中的 HPACK 編碼方式相似,HTTP3 中的 QPACK 也採用了靜態表、動態表及 Huffman 編碼:
(圖片參見:https://www.oreilly.com/content/http2-a-new-excerpt/)
先來看靜態表的變化。在上圖中,GET 方法映射爲數字 2,這是通過客戶端、服務器協議實現層的硬編碼完成的。在 HTTP2 中,共有 61 個靜態表項:
而在 QPACK 中,則上升爲 98 個靜態表項,比如 Nginx 上的 ngx_htt_v3_static_table 數組所示:
你也可以從這裏找到完整的 HTTP3 靜態表。對於 Huffman 以及整數的編碼,QPACK 與 HPACK 並無多大不同,但動態表編解碼方式差距很大。
所謂動態表,就是將未包含在靜態表中的 Header 項,在其首次出現時加入動態表,這樣後續傳輸時僅用 1 個數字表示,大大提升了編碼效率。因此,動態表是天然具備時序性的,如果首次出現的請求出現了丟包,後續請求解碼 HPACK 頭部時,一定會被阻塞!
QPACK 是如何解決隊頭阻塞問題的呢?事實上,QPACK 將動態表的編碼、解碼獨立在單向 Stream 中傳輸,僅當單向 Stream 中的動態表編碼成功後,接收端才能解碼雙向 Stream 上 HTTP 消息裏的動態表索引。
我們又引入了單向 Stream 和雙向 Stream 概念,不要頭疼,它其實很簡單。單向指只有一端可以發送消息,雙向則指兩端都可以發送消息。還記得上一小節的 QUIC Stream Frame 頭部嗎?其中的 Stream ID 別有玄機,除了標識 Stream 外,它的低 2 位還可以表達以下組合:
因此,當 Stream ID 是 0、4、8、12 時,這就是客戶端發起的雙向 Stream(HTTP3 不支持服務器發起雙向 Stream),它用於傳輸 HTTP 請求與響應。單向 Stream 有很多用途,所以它在數據前又多出一個 Stream Type 字段:
Stream Type 有以下取值:
- 0x00:控制 Stream,傳遞各類 Stream 控制消息;
- 0x01:服務器推送消息;
- 0x02:用於編碼 QPACK 動態表,比如面對不屬於靜態表的 HTTP 請求頭部,客戶端可以通過這個 Stream 發送動態表編碼;
- 0x03:用於通知編碼端 QPACK 動態表的更新結果。
由於 HTTP3 的 STREAM 之間是亂序傳輸的,因此,若先發送的編碼 Stream 後到達,雙向 Stream 中的 QPACK 頭部就無法解碼,此時傳輸 HTTP 消息的雙向 Stream 就會進入 Block 阻塞狀態(兩端可以通過控制幀定義阻塞 Stream 的處理方式)。
小結
最後對本文內容做個小結。
基於四元組定義連接並不適用於下一代 IoT 網絡,HTTP3 創造出 Connection ID 概念實現了連接遷移,通過融合傳輸層、表示層,既縮短了握手時長,也加密了傳輸層中的絕大部分字段,提升了網絡安全性。
HTTP3 在 Packet 層保障了連接的可靠性,在 QUIC Frame 層實現了有序字節流,在 HTTP3 Frame 層實現了 HTTP 語義,這徹底解開了隊頭阻塞問題,真正實現了應用層的多路複用。
QPACK 使用獨立的單向 Stream 分別傳輸動態表編碼、解碼信息,這樣亂序、併發傳輸 HTTP 消息的 Stream 既不會出現隊頭阻塞,也能基於時序性大幅壓縮 HTTP Header 的體積。
- 本文作者: 陶輝
- 本文鏈接: https://www.taohui.tech/2021/02/04 / 網絡協議 / 深入剖析 HTTP3 協議 /
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://www.taohui.tech/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/