HTTP-3 發佈了,我們來談談 HTTP-3

經過了多年的努力,在 6 月 6 號,IETF (互聯網工程任務小組) 正式發佈了 HTTP/3 的 RFC, 這是超文本傳輸協議(HTTP)的第三個主要版本,完整的 RFC 超過了 20000 字,非常詳細的解釋了 HTTP/3。

HTTP 歷史

HTTP3 是在保持 QUIC 穩定性的同事使用 UDP 來實現高速度, 同時又不會犧牲 TLS 的安全性.

QUIC 協議概覽

QUIC(Quick UDP Internet Connections, 快速 UDP 網絡連接) 是基於 UDP 的協議, 利用了 UDP 的速度和效率, 同時整合 TCP, TLS 和 HTTP/2 的優點並加以優化. 用一張圖可以清晰的表示他們之間的關係.

QUIC 是用來替代 TCP, SSL/TLS 的傳輸層協議, 在傳輸層之上還有應用層. 我們熟知的應用層協議有 HTTP, FTP, IMAP 等, 這些協議理論上都可以運行在 QUIC 上, 其中運行在 QUIC 之上的協議被稱爲 HTTP/3, 這就是HTTP over QUICHTTP/3的含義

因此想要了解HTTP/3, QUIC 是繞不過去的, 下面是幾個重要的 QUIC 特性.

0 RTT 建立連接

RTT: round-trip time, 僅包括請求訪問來回的時間

HTTP/2的連接建立需要 3 RTT, 如果考慮會話複用, 即把第一次握手計算出來的對稱密鑰緩存起來, 那也需要 2 RTT. 更進一步的, 如果 TLS 升級到 1.3, 那麼 HTTP/2 連接需要 2RTT, 考慮會話複用需要 1RTT. 如果 HTTP/2 不急於 HTTPS, 則可以簡化, 但實際上幾乎所有瀏覽器的設計都要求 HTTP/2 需要基於 HTTPS.

HTTP/3首次連接只需要1RTT, 後面的鏈接只需要0RTT, 意味着客戶端發送給服務端的第一個包就帶有請求數據, 其主要連接過程如下:

  1. 首次連接, 客戶端發送Inchoate Client Hello, 用於請求連接;

  2. 服務端生成 g, p, a, 根據 g, p, a 算出 A, 然後將 g, p, A 放到 Server Config 中在發送Rejection消息給客戶端.

  3. 客戶端接收到 g,p,A 後, 自己再生成 b, 根據 g,p,a 算出 B, 根據 A,p,b 算出初始密鑰 K, B 和 K 算好後, 客戶端會用 K 加密 HTTP 數據, 連同 B 一起發送給服務端.

  4. 服務端接收到 B 後, 根據 a,p,B 生成與客戶端同樣的密鑰, 再用這密鑰解密收到的 HTTP 數據. 爲了進一步的安全 (前向安全性), 服務端會更新自己的隨機數 a 和公鑰, 在生成新的密鑰 S, 然後把公鑰通過Server Hello發送給客戶端. 連同Server Hello消息, 還有 HTTP 返回數據.

這裏使用 DH 密鑰交換算法, DH 算法的核心就是服務端生成 a,g,p3 個隨機數, a 自己持有, g 和 p 要傳輸給客戶端, 而客戶端會生成 b 這 1 個隨機數, 通過 DH 算法客戶端和服務端可以算出同樣的密鑰. 在這過程中 a 和 b 並不參與網絡傳輸, 安全性大大提升. 因爲 p 和 g 是大數, 所以即使在網絡傳輸中 p, g, A, B 都被劫持, 靠現在的計算力算力也無法破解.

連接遷移

TCP 連接基於四元組 (源 IP, 源端口, 目的 IP, 目的端口), 切換網絡時至少會有一個因素髮生變化, 導致連接發送變化. 當連接發送變化是, 如果還是用原來的 TCP 連接, 則會導致連接失敗, 就得等到原來的連接超時後重新建立連接, 所以我們有時候發現切換到一個新的網絡時, 即使網絡狀況良好, 但是內容還是需要加載很久. 如果實現的好, 當檢測到網絡變化時, 立即建立新的 TCP 連接, 即使這樣, 建立新的連接還是需要幾百毫秒時間.

QUIC 不受四元組的影響, 當這四個元素髮生變化時, 原連接依然維持. 原理如下:

QUIC 不以四元素作爲表示, 而是使用一個 64 位的隨機數, 這個隨機數被稱爲Connection ID, 即使 IP 或者端口發生變化, 只要 Connection ID 沒有變化, 那麼連接依然可以維持.

隊頭阻塞 / 多路複用

HTTP/1.1HTTP/2都存在隊頭阻塞的問題 (Head Of Line blocking).

TCP 是個面向連接的協議, 即發送請求後需要收到ACK消息, 以確認對象已接受數據. 如果每次請求都要在收到上次請求的ACK消息後再請求, 那麼效率無疑很低. 後來HTTP/1.1提出了Pipeline技術, 允許一個 TCP 連接同時發送多個請求. 這樣就提升了傳輸效率.

在這樣的背景下, 隊頭阻塞發生了. 比如, 一個 TCP 連接同時傳輸 10 個請求, 其中 1,2,3 個請求給客戶端接收, 但是第四個請求丟失, 那麼後面第 5-10 個請求都被阻塞. 需要等第四個請求處理完畢後才能被處理. 這樣就浪費了帶寬資源.

因此, HTTP 一般又允許每個主機建立 6 個 TCP 連接, 這樣可以更加充分的利用帶寬資源, 但每個連接中隊頭阻塞的問題還是存在的.

HTTP/2 的多路複用解決了上述的隊頭阻塞問題. 在 HTTP/2 中, 每個請求都被拆分爲多個Frame通過一條 TCP 連接同時被傳輸, 這樣即使一個請求被阻塞, 也不會影響其他的請求.

但是, HTTP/2 雖然可以解決請求這一粒度下的阻塞, 但HTTP/2的基礎 TCP 協議本身卻也存在隊頭阻塞的問題. HTTP/2 的每個請求都會被拆分成多個 Frame, 不同請求的 Frame 組合成 Stream, Stream 是 TCP 上的邏輯傳輸單元, 這樣 HTTP/2 就達到了一條連接同時發送多個請求的目標, 其中Stram1已經正確送達, Stram2 中的第三個 Frame 丟失, TCP 處理數據是有嚴格的前後順序, 先發送的 Frame 要先被處理, 這樣就會要求發送方重新發送第三個 Frame, Steam3 和 Steam4 雖然已到達但卻不能被處理, 那麼這時整條鏈路都會被阻塞.

不僅如此, 由於 HTTP/2 必須使用 HTTPS, 而 HTTPS 使用 TLS 協議也存在隊頭阻塞問題. TLS 基於 Record 組織數據, 將一對數據放在一起加密, 加密完成後又拆分成多個 TCP 包傳輸. 一般每個 Record 16K, 包含 12 個 TCP 包, 這樣如果 12 個 TCP 包中有任何一個包丟失, 那麼整個 Record 都無法解密.

隊頭阻塞會導致 HTTP/2 在更容易丟包的弱網絡環境下比 HTTP/1.1 更慢.

QUIC 是如何解決隊頭阻塞的問題的? 主要有兩點:

擁塞控制

擁塞控制的目的是避免過多的數據一下子湧入網絡, 導致網絡超出最大負荷. QUIC 的擁塞控制與 TCP 類似, 並在此基礎上做了改進. 先來看看 TCP 的擁塞控制.

QUIC 重新實現了 TCP 協議中的 Cubic 算法進行擁塞控制, 下面是 QUIC 改進的擁塞控制的特性:

1. 熱插拔

TCP 中如果要修改擁塞控制策略, 需要在系統層面今次那個操作, QUIC 修改擁塞控制策略只需要在應用層操作, 並且 QUIC 會根據不同的網絡環境, 用戶來動態選擇擁塞控制算法.

2. 前向糾錯 FEC

QUIC 使用前向糾錯 (FEC, Forword Error Correction) 技術增加協議的容錯性. 一段數據被切分爲 10 個包後, 一次對每個包進行異或運算, 運算結果會作爲 FEC 包與數據包一起被傳輸, 如果傳輸過程中有一個數據包丟失, 那麼就可以根據剩餘 9 個包以及 FEC 包推算出丟失的那個包的數據, 這樣就大大增加了協議的容錯性.

這是符合現階段網絡傳輸技術的一種方案, 現階段帶寬已經不是網絡傳輸的瓶頸, 往返時間纔是, 所以新的網絡傳輸協議可以適當增加數據冗餘, 減少重傳操作.

3. 單調遞增的 Packer Number

TCP 爲了保證可靠性, 使用Sequence Number和 ACK 來確認消息是否有序到達, 但這樣的設計存在缺陷.

超時發生後客戶端發起重傳, 後來接受到了 ACK 確認消息, 但因爲原始請求和重傳請求接受到的 ACK 消息一樣, 所以客戶端就不知道這個 ACK 對應的是原始請求還是重傳請求. 這就會造成歧義.

如果客戶端認爲是重傳的 ACK, 但實際上是右圖的情形, 會導致 RTT 偏小, 反之會導致 RTT 偏大.

QUCI 解決了上面的的歧義問題, 與Sequence Number不同, Packet Number嚴格單調遞增, 如果Packet N丟失了, 那麼重傳時 Packet 的標識就不會是 N, 而是比 N 大的數字, 比如 N+M, 這樣發送方接收到確認消息時, 就能方便的知道 ACK 對應的原始請求還是重傳請求.

4. ACK Delay

TCP 計算 RTT 時沒有考慮接收方接受到數據發發送方確認消息之間的延遲, 如下圖所示, 這段延遲即 ACK Delay. QUIC 考慮了這段延遲, 使得 RTT 的計算更加準確.

5. 更多的 ACK 塊

一般來說, 接收方收到發送方的消息後都應該發送一個 ACK 恢復, 表示收到了數據. 但每收到一個數據就返回一個 ACK 恢復實在太麻煩, 所以一般不會立即回覆, 而是接受到多個數據後再回復, TCP SACK 最多提供 3 個 ACK block. 但在有些場景下, 比如下載, 只需要服務器返回數據就好, 但按照 TCP 的設計, 每收到三個數據包就要返回一個ACK, 而 QUIC 最多可以捎帶 256 個ACK block, 在丟包率比較嚴重的網絡下, 更多的 ACK 可以減少重傳量, 提升網絡效率.

瀏覽控制

TCP 會對每個 TCP 連接進行流量控制, 流量控制的意思是讓發送方不要發送太快, 要讓接收方來得及接受, 不然會導致數據溢出而丟失, TCP 的流量控制主要通過滑動窗口來實現的. 可以看到, 擁塞控制主要是控制發送方的發送策略, 但沒有考慮接收方的接收能力, 流量控制是對部分能力的不起.

QUIC 只需要建立一條連接, 在這條連接上同時傳輸多條Stream, 好比有一條道路, 量都分別有一個倉庫, 道路中有很多車輛運送物資. QUIC 的流量控制有兩個級別: 連接級別 (Connection Level) 和 Stream 級別(Stream Level).

對於單條的 Stream 的流量控制: Stream 還沒傳輸數據時, 接收窗口 (flow control recevice window) 就是最大接收窗口, 隨着接收方接收到數據後, 接收窗口不斷縮小. 在接收到的數據中, 有的數據已被處理, 而有的數據還沒來得及處理. 如下圖, 藍色塊表示已處理數據, 黃色塊表示違背處理數據, 這部分數據的到來, 使得 Stream 的接收窗口縮小.

隨着數據不斷被處理, 接收方就有能力處理更多數據. 當滿足(flow control receivce offset - consumed bytes) < (max receive window/2)時, 接收方會發送WINDOW_UPDATE frame告訴發送方你可以再多發送數據, 這時候flow control receive offset就會偏移, 接收窗口增大, 發送方可以發送更多數據到接收方.

Stream 級別對防止接收端接收過多數據作用有限, 更需要藉助Connection級別的流量控制. 理解了Stream流量那麼也很好理解Connection的流控. Stream 中,

接收窗口=最大接受窗口 - 已接收數據
而對於Connection來說:
接收窗口 = Stream1 接收窗口 + Stream2 接收窗口 + ... + StreamN 接收窗口

參考鏈接

作者:shuerbuzuo

來源:http://know.shuerbuzuo.cn/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/HTTP3.html#%E6%B5%8F%E8%A7%88%E6%8E%A7%E5%88%B6

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wvjzYrt3_myzIBkS4Ds7XA