原創
QUIC協定選擇基於UDP而不是直接基於IP協定主要有兩個原因:網路路徑上的中間盒支援問題和終端上核心支援問題。
網路路徑上的中間盒支援問題:純IP協定在網路上不好傳播,因為網路中的許多元件(如NAT、防火牆、LB等)都是基於TCP和UDP的。
如果不使用UDP,這些元件就無法辨識或正常運作。因此,為了保持網路的兼容性和功能性,QUIC選擇了基於UDP的實作方式。
終端上核心支援問題:終端設備(尤其是用戶終端)的核心普遍支援UDP,但純IP協定的派發給使用者程序的方式未知。如果使用connectionID來派發,那麼核心需要進行相應的調整。這涉及到作業系統核心的重新設計,無論是微軟還是用戶都不太可能接受這樣的改動。因此,為了減少對現有系統和終端設備的改動需求,QUIC選擇了基於UDP的實作方式。
綜上所述,QUIC協議選擇基於UDP而不是直接基於IP協議,主要是出於對網路相容性、功能保持以及對現有系統和終端設備影響最小的考慮QUIC 協議現在市面上已經有基於UDP 協議實現的可靠傳輸協議的成熟方案了,那就是QUIC 協議,已經應用在了HTTP/3。這次,聊聊 QUIC 是如何實現可靠傳輸的?又是如何解決上面 TCP 協定四個面向的缺陷?
QUIC 是如何實現可靠傳輸的?要基於 UDP 實現的可靠傳輸協議,那麼就要在應用層下功夫,也就是要設計好協議的頭部字段。拿 HTTP/3 舉例子,在 UDP 訊息頭部與 HTTP 訊息之間,共有 3 層頭部:
Packet HeaderPacket Header 首次建立連線時和日常傳輸資料時所使用的 Header 是不同的
QUIC 也是需要三次握手來建立連線的,主要目的是為了協商連線 ID。協商出連線 ID 後,後續傳輸時,雙方只需要固定住連線 ID,從而實現連線遷移功能。
所以,你可以看到日常傳輸資料的 Short Packet Header 不需要在傳輸 Source Connection ID 欄位了,只需要傳輸 Destination Connection ID。 Short Packet Header 中的Packet Number 是每個封包獨一無二的編號,它是嚴格遞增的,也就是說就算Packet N 遺失了,重傳的Packet N 的Packet Number 已經不是N,而是一個比N 大的值。
為什麼這樣子設計?
TCP 在重傳封包時的序號和原始封包的序號是相同的,也正是由於這個特性,引入了 TCP 重傳的歧義問題。
當 TCP 發生逾時重傳後,客戶端發起重傳,然後接收到了服務端確認 ACK 。由於客戶端原始封包和重傳封包序號都是一樣的,那麼服務端針對這兩個封包回應的都是相同的 ACK。這樣的話,客戶端就無法判斷出是「原始報文的回應」還是「重傳報文的回應」,這樣在計算 RTT(往返時間) 時應該選擇從發送原始報文開始計算,還是重傳原始報文開始計算呢?RTO (超時時間)是基於 RTT 來計算的,那麼如果 RTT 計算不精準,那麼 RTO (超時時間)也會不精確,這樣可能導致重傳的機率事件增大。
QUIC 封包中的 Pakcet Number 是嚴格遞增的, 即使是重傳報文,它的 Pakcet Number 也是遞增的,這樣就能更加精確計算出報文的 RTT。
另外,還有一個好處,單調遞增的設計,可以讓資料包不再像TCP 那樣必須有序確認,支援亂序確認,當資料包Packet N 遺失後,只要有新的已接收資料包確認,當前視窗就會繼續向右滑動,這樣就不會因為丟包重傳將目前視窗阻塞在原地,從而解決了隊頭阻塞問題。
QUIC Frame Header一個 Packet 封包中可以存放多個 QUIC Frame。每一個 Frame 都有明確的類型,針對類型的不同,功能也不同,自然格式也不同。我這裡只舉例 Stream 類型的 Frame 格式,Stream 可以認為就是一條 HTTP 請求,它長這樣:
QUIC 是如何解決 TCP 隊頭阻塞問題的?
在一條 QUIC 連線上可以並發發送多個 HTTP 請求 (Stream)。但是 QUIC 給每一個 Stream 分配了一個獨立的滑動窗口,這樣使得一個連接上的多個 Stream 之間沒有依賴關係,都是相互獨立的,各自控制的滑動窗口。
假如 Stream2 丟了一個 UDP 包,也只會影響 Stream2 的處理,不會影響其他 Stream,與 HTTP/2 不同,HTTP/2 只要某個流中的封包遺失了,其他流也會因此受影響。
QUIC 是如何做流量控制的? QUIC 是基於UDP 傳輸的,而UDP 沒有流量控制,因此QUIC 實現了自己的流量控制機制,QUIC 的滑動視窗滑動的條件跟TCP 有一點差別,但是同一個Stream 的資料也是要保證順序的,不然無法實作可靠傳輸,因此同一個Stream 的資料包遺失了,也會造成視窗無法滑動。
QUIC 的 每個 Stream 都有各自的滑動窗口,不同 Stream 互相獨立,隊頭的 Stream A 被阻塞後,不妨礙 StreamB、C的讀取。而對於 HTTP/2 而言,所有的 Stream 都跑在一條 TCP 連接上,而這些 Stream 共享一個滑動窗口,因此同一個Connection內,Stream A 被阻塞後,StreamB、C 必須等待。
QUIC 實現了兩種級別的流量控制,分別為Stream 和Connection 兩種級別:Stream 級別的流量控制:Stream 可以認為就是一條HTTP 請求,每個Stream 都有獨立的滑動窗口,所以每個Stream 都可以做流量控制,防止單一Stream 消耗連接(Connection)的全部接收緩衝。 Connection 流量控制:限制連線中所有 Stream 相加起來的總位元組數,防止發送方超過連線的緩衝容量。
QUIC 更快的連接建立對於HTTP/1 和HTTP/2 協議,TCP 和TLS 是分層的,分別屬於內核實現的傳輸層、openssl 庫實現的表示層,因此它們難以合併在一起,需要分批次來握手,先TCP 握手(1RTT),再TLS 握手(2RTT),所以需要3RTT 的延遲才能傳輸數據,就算Session 會話服用,也需要至少2 個RTT。HTTP/3 在傳送資料前雖然需要 QUIC 協定握手,這個握手過程只需要 1 RTT,握手的目的是為確認雙方的「連線 ID」,連線遷移就是基於連線 ID 實現的。但是HTTP/3 的QUIC 協定並不是與TLS 分層,而是QUIC 內部包含了TLS,它在自己的幀會攜帶TLS 裡的“記錄”,再加上QUIC 使用的是TLS1.3,因此僅需1 個RTT 就可以「同時」完成建立連線與金鑰協商,甚至在第二次連線的時候,應用資料包可以和QUIC 握手資訊(連線資訊+ TLS 資訊)一起傳送,達到0-RTT 的效果。 如下圖右邊部分,HTTP/3 當會話恢復時,有效負載資料與第一個資料包一起傳送,可以做到 0-RTT(下圖的右下角):
QUIC 平滑遷移連接基於 TCP 傳輸協定的 HTTP 協議,由於是透過四元組(來源 IP、來源連接埠、目的 IP、目的連接埠)確定一條 TCP 連線。
那麼當行動裝置的網路從 4G 切換到 WIFI 時,表示 IP 位址變化了,那麼就必須斷開連接,然後重新建立 TCP 連線。而建立連線的過程包含 TCP 三次握手和 TLS 四次握手的時延,以及 TCP 慢啟動的減速過程,給用戶的感覺就是網路突然卡頓了一下,因此連線的遷移成本是很高的。
QUIC 協定沒有用四元組的方式來「綁定」連接,而是透過連接ID來標記通訊的兩個端點,客戶端和伺服器可以各自選擇一組ID 來標記自己,因此即使行動裝置的網路變化後,導致IP 位址變更了,只要仍保有上下文資訊(例如連接ID、TLS 金鑰等),就可以「無縫」地複用原連接,消除重連的成本,達到了連接遷移的功能。