JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

QUIC 與 HTTP/3:重新發明傳輸層

TCP 扛起網際網路四十年,但它已經來不及修補了。一起認識 QUIC——一個在使用者空間、在 UDP 之上重建的可靠、加密、多工傳輸協定——以及它如今承載的網頁 HTTP/3。

為什麼你沒辦法直接修好 TCP

到現在你已經很熟悉傳輸層了:同步序號的三向交握、滑動視窗、像個謹慎駕駛般溫和加速、猛力剎車的壅塞控制,以及當多條串流共用一條 TCP 管道時你遇過的佇列前端阻塞。那麼,這裡有個合理的問題:既然 TCP 有已知的缺陷,為什麼不直接把 TCP 改好就好?這個答案是整個網路學裡最重要的實務教訓之一,而它幾乎與聰不聰明無關。

TCP 住在作業系統核心裡,而它由散布在整個網際網路上的路由器、防火牆與家用閘道所實作。其中許多盒子——統稱為中間盒(middlebox)——會窺探 TCP 標頭,並悄悄改寫或丟棄任何看起來不像它們多年前被寫死要預期的那種 TCP 的東西。於是一個全新的 TCP 選項常常被剝掉,或讓連線整個斷掉。這種僵化叫做協定骨化(protocol ossification):協定被凍結在原地,不是被它的設計者,而是被數百萬台對它寫死了假設的裝置凍住的。

還有第二個原因。因為 TCP 住在核心裡,升級它意味著要升級數十億套作業系統——手機、伺服器、嵌入式裝置——再等上好幾年,等大家都重開機到新版本。網頁等不了。於是 QUIC 的設計者一口氣做了兩個激進的選擇:把新傳輸建立在 UDP 之上,好讓中間盒別來碰它;並讓它跑在使用者空間、在應用程式內部,這樣它就能像瀏覽器一樣快地發布與更新。

把 UDP 當成一塊白板,而不是次等的 TCP

我們很容易以為 QUIC 選 UDP 是因為 UDP 比較簡單或比較弱。並非如此。記得:UDP 是一個刻意的選擇,不是壞掉的 TCP——它只提供帶有埠號與檢查碼的、光禿禿的盡力而為資料報,而且關鍵是,它本身不強加任何排序、不重傳、也沒有自己的壅塞控制。對 QUIC 來說,這份空白正是重點。UDP 被當成一個薄薄的、眾所周知的信封,中間盒早已習慣不檢查就轉發它,於是 QUIC 可以自由地在裡面蓋出任何它想要的東西。

在那個 UDP 信封裡,QUIC 用使用者空間的程式碼,重建了 TCP 給你的一切,外加幾件 TCP 永遠做不到的事:帶有確認與重傳的可靠交付、壅塞控制、流量控制,以及——從一開始就內建、而非事後外掛的——加密。而且它承載的不是一條依序的位元組串流,而是許多彼此獨立的串流,每條各有自己的排序,因此其中一條串流的遺失再也不會凍住其他串流。這正是你在傳輸層那一級追蹤過的佇列前端阻塞的解藥。

一個來回就連上——甚至零個

QUIC 真正發光的地方就在這裡,而它歸根究柢在於來回次數。回想一下:一次往返時間(RTT)就是一趟去了又回的完整旅程,而你贏不過光速——再多的頻寬都不會縮短一個 RTT。所以在有用資料開始流動之前的來回次數,就是純粹、無可避免的延遲。用經典的 TCP 加 TLS,你得先付 TCP 交握(SYN、SYN-ACK、ACK),再在上面付 TLS 交握,等上兩到三個來回,你網頁的第一個位元組才能開始動。

QUIC 把傳輸交握與密碼學交握折成一個。因為可靠性與加密是一起設計的,一條全新的 QUIC 連線只要一個來回(1-RTT)就準備好了:用戶端最開頭的那一波封包,就已經在協商金鑰,而伺服器的回應就能讓真正的資料開始流。更棒的是,如果你以前跟這台伺服器談過,QUIC 可以記住上次的一個祕密,並在最開頭的那個封包裡就送出加密的應用資料——零個來回的建立(0-RTT)——讓請求搭著開啟連線的同一波封包就出發。

連線識別碼:在更換位址後存活下來

一條 TCP 連線是用四個數字來命名的:來源 IP、來源埠、目的 IP、目的埠。任一個改變,這條連線就定義上成了另一條連線。當你的手機從 Wi-Fi 跳到行動網路時,發生的正是這件事——你的 IP 位址變了,於是每一條 TCP 連線都悄悄死去,下載一邊重新連線一邊卡住。記得:一個 IP 位址命名的是一個位置或介面,不是一台裝置,所以當介面變了,位址也跟著它變。

QUIC 的解法是用一個它自己的識別碼來命名連線——連線識別碼(connection ID)——由兩端挑選,並帶在 QUIC 封包內部,位於那會變動的 IP 與埠之上。當你手機的位址在下載到一半時改變,新的封包仍帶著同一個連線識別碼,於是伺服器認得這條連線、繼續傳下去。這叫做連線遷移(connection migration),這正是為什麼一段 QUIC 影片能從你家的 Wi-Fi 一路順暢地騎到街上,連一次卡頓都沒有。

TCP identifies a connection by the 4-tuple:
   ( srcIP , srcPort , dstIP , dstPort )
   change Wi-Fi -> cellular  =>  srcIP changes  =>  connection is dead

QUIC identifies a connection by a Connection ID inside the packet:
   [ UDP header | QUIC: ConnID=0xA17F | streams... | encrypted ]
   change Wi-Fi -> cellular  =>  srcIP changes  =>  ConnID unchanged
                                                =>  same connection, keep going
TCP 被綁在 IP/埠的四元組上;QUIC 在其之上命名連線,因此它能在位址改變後存活。

HTTP/3,以及更廣的傳輸層重新發明

把這一切疊起來,你就得到 HTTP/3,它不過就是把 HTTP 搭在 QUIC 上傳送。你熟悉的取頁邏輯沒變——一樣的方法、一樣的標頭、一樣的狀態碼——但底下的傳輸如今是 QUIC 而非 TCP。於是 HTTP/3 得到了沒有佇列前端阻塞的逐串流多工、絕不可省略的加密、1-RTT 或 0-RTT 的建立,以及能在更換網路後存活的連線。它是同一個網頁,騎在一個從頭重建的傳輸之上。

QUIC 是最顯眼的重新發明,但不是唯一的一個。多路徑 TCP(Multipath TCP)切入的角度不同:它讓單一連線同時把位元組分散到好幾條路徑上——你的手機同時用 Wi-Fi 與行動網路——以獲得更高的吞吐量,並在其中一條路徑斷掉時無縫接手,同時對應用程式而言它看起來仍像普通的 TCP。再往外看,具名資料網路(named-data networking)質疑的是一個更深的假設:與其定址一台主機(它在哪裡?),不如按名字請求內容(我要的是什麼?),任何持有那段具名、已簽章資料塊的鄰近快取都能回答。它是一個研究方向,不是已部署的網際網路,但它重新定義了網路究竟是為了什麼而存在。

把這些串起來的,是一個得來不易的領悟:傳輸並不神聖。TCP 那條可靠、依序的位元組串流,在 1980 年代對終端機與檔案傳輸是對的抽象;但今天的網頁要的是許多彼此獨立的串流、內建的隱私,以及跨網路的移動性。QUIC 恰恰交付了這些,而它之所以做得到,正是因為它走出了核心、走出了 TCP——這證明了在網際網路上,一個協定「住在哪裡」可能和它「做什麼」一樣重要。

追蹤一次 QUIC 取頁

我們把整件事從頭到尾走一遍,情境是一位回訪的訪客透過 HTTP/3 抓取一個頁面。注意它花了多少個來回,以及舊的那種阻塞如今根本不可能發生在哪裡。

  1. 瀏覽器透過 UDP 送出一個 QUIC 封包,它既協商加密金鑰,又因為以前來過,把第一個 HTTP 請求當成 0-RTT 資料一起帶上——零個建立來回。
  2. 伺服器用同一個連線識別碼回應,確認金鑰並開出數條彼此獨立的串流——一條給 HTML、一條給腳本、一條給樣式表——全部在一條 QUIC 連線裡多工進行。
  3. 一個承載部分圖片的封包遺失了;QUIC 重傳它,但只有圖片那條串流要等。腳本與樣式表的串流持續流向頁面,所以沒有傳輸層的佇列前端阻塞。
  4. 載入到一半,使用者走到戶外,手機從 Wi-Fi 切換到行動網路;IP 位址變了,但連線識別碼沒變,於是連線遷移,下載一次都沒卡。
  5. 整個過程中每條串流裡的每個位元組都是加密的,因為 QUIC 把加密烤進了傳輸裡——根本沒有一個「忘了打開」的未加密模式存在。