天天看點

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

文|曾柯(花名:毅絲 )

螞蟻集團進階工程師

負責螞蟻集團的接入層建設工作

主要方向為高性能安全網絡協定的設計及優化

本文 10279 字 閱讀 18 分鐘

PART. 1 引言

作為系列文章的第一篇,引言部分就先稍微繁瑣一點,讓大家對這個系列文章有一些簡單的認知。

先介紹下這個系列文章的誕生背景。QUIC、HTTP/3 等字眼想來對大家而言并不陌生。從個人的視角來看,大部分開發者其實都已經有了一些背景知識,比如 HTTP/3 的核心是依賴 QUIC 來實作傳輸層及 TLS 層的能力。而談及其中細節之時,大家卻又知之甚少,相關的文章大多隻是淺嘗辄止的對一些 HTTP/3 中的機制和特性做了介紹,少有深入的分析,而對于這些機制背後誕生原因,設計思路的分析,就更難得一見了。

從個人并不大量的 RFC 閱讀及 draft 寫作經曆來看,和撰寫論文文獻一樣,為了保證一份 RFC 的精簡以及表述準确,當然也是為了編寫過程的簡單。在涉及到其他相關協定時,作者往往是通過直接引用的方式來進行表述。這也就意味着直接通過閱讀 RFC 來學習和了解網絡協定是一個曲線相對比較陡峭的過程,往往讀者在閱讀到一個關鍵部分的時候,就不得不跳轉到其他文檔,然後重複這個令人頭痛的過程,而當讀者再次回到原始文檔時,可能都已經忘了之前的上下文是什麼。

而 HTTP/3,涉及到 QUIC、TLS、HTTP/2、QPACK 等标準文檔,而這些标準文檔各自又有大量的關聯文檔,是以學習起來并不是一個容易的事。

當然,系列文章的立題為“深入 HTTP/3”,而不是“深入 QUIC”,其背後的原因就是 HTTP/3 并不僅僅隻是 QUIC 這麼一個點,其中還包含有大量現有 HTTP 協定和 QUIC 的有機結合。在系列文章的後續,也會對這一部分做大篇幅的深入分析。

一個協定的性能優秀與否,除了本身的設計之外,也離不開大量的軟硬體優化,架構落地,專項設計等工程實踐經驗,是以本系列除了會針對 HTTP/3 本身特性進行分享之外,也會針對 HTTP/3 在螞蟻落地的方案進行分享。

引言的最後,也是本文的正式開始。

據統計,人類在學習新的知識時,比較習慣從已有的知識去類比和推斷,以産生更深刻的感性和理性認識。我想對大部分同學而言,“TCP 為什麼要三次握手以及四次揮手?”這個問題,頗有點經典的不能再經典的味道,是以今天這篇文章也将從 QUIC 連結的建立流程及關閉流程入手,開始我們系列的第一篇文章。

PART. 2 連結建立

2.1 重溫 TCP

“TCP 為什麼要三次握手?”

在回答問題之前我們需要了解 TCP 的本質,TCP 協定被設計為一種面向連接配接的、可靠的、基于位元組流的全雙工傳輸層通信協定。

“可靠”意味着使用 TCP 協定傳輸資料時,如果 TCP 協定傳回發送成功,那麼資料一定已經成功的傳輸到了對端,為了保證資料的“可靠”傳輸,我們首先需要一個應答機制來确認對端已經收到了資料,而這就是我們熟悉的 ACK 機制。

“流式”則是一種使用上的抽象(即收發端不用關注底層的傳輸,隻需将資料當作持續不斷的位元組流去發送和讀取就好了)。“流式”的使用方式強依賴于資料的有序傳輸,為了這種使用上的抽象,我們需要一個機制來保證資料的有序,TCP 協定棧的設計則是給每個發送的位元組标示其對應的 seq(實際應用中 seq 是一個範圍,但其真實效果就是做到了每個位元組都被有序标示),接收端通過檢視目前收到資料的 seq,并與自身記錄的對端目前 seq 進行比對,以此确認資料的順序。

“全雙工”則意味着通信的一端的收發過程都是可靠且流式的,并且收和發是兩個完全獨立,互不幹擾的兩個行為。

可以看到,TCP 的這些特性,都是以 seq 和 ACK 字段作為載體來實作的,而所有 TCP 的互動流程,都是在為了上述特性服務,當然三次握手也不例外,我們再來看 TCP 的三次握手的示意圖:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

為了保證通信雙方都能确認對端資料的發送順序,收發端都需要各自記錄對端的目前 seq,并确認對端已經同步了自己的 seq 才可以實作,為了保證這個過程,起碼需要 3 個 RTT。而實際的實作為了效率考慮,将 seq 和 ACK 放在了一個封包裡,這也就形成了我們熟知的三次握手。

當然,三次握手不僅僅是同步了 seq,還可以用來驗證用戶端是一個正常的用戶端,比如 TCP 可能會面臨這些問題:

(1)有一些 TCP 的攻擊請求,隻發 syn 請求,但不回資料,浪費 socket 資源;

(2)已失效的連接配接請求封包段突然又傳送到了服務端,這些資料不再會有後續的響應,如何防止這樣的請求浪費資源?

而這些問題隻是三次握手順手解決的問題,不是專門為了它們設計的三次握手。

細心的你,可能已經發現了一個問題,如果我們約定好 client 和 server 的 seq 都是從 0(或者某個大家都知道的固定值)開始,是不是就可以不用同步 seq 了呢?這樣似乎也就不需要三次握手那麼麻煩了?可以直接開始發送資料?

當然,協定的設計者肯定也想過這個方案,至于為什麼沒這麼實作,我們在下一章來看看 TCP 面臨什麼樣的問題。

2.2 TCP 面臨的問題

2.2.1 seq 攻擊

在上一節我們提到,TCP 依賴 seq 和 ACK 來實作可靠,流式以及全雙工的傳輸模式,而實際過程中卻需要通過三次握手來同步雙端的 seq,如果我們提前約定好通信雙方初始 seq,其實是可以避免三次握手的,那麼為什麼沒有這麼做呢?答案是安全問題。

我們知道,TCP 的資料是沒有經過任何安全保護的,無論是其 header 還是 payload,對于一個攻擊者而言,他可以在世界的任何角落,僞造一個合法 TCP 封包。

一個典型的例子就是攻擊者可以僞造一個 reset 封包強制關閉一條 TCP 連結,而攻擊成功的關鍵則是 TCP 字段裡的 seq 及 ACK 字段,隻要封包中這兩項位于接收者的滑動視窗内,該封包就是合法的,而 TCP 握手采用随機 seq 的方式(不完全随機,而是随着時間流逝而線性增長,到了 2^32 盡頭再復原)來提升攻擊者猜測 seq 的難度,以增加安全性。

為此,TCP 也不得不進行三次握手來同步各自的 seq。當然,這樣的方式對于 off-path 的攻擊者有一定效果,對于 on-path 的攻擊者是完全無效的,一個 on-path 的攻擊者仍然可以随意 reset 連結,甚至僞造封包,篡改使用者的資料。

是以,雖然 TCP 為了安全做出過一些努力,但由于其本質上隻是一個傳輸協定,安全并不是其原生的考量,在目前的網絡環境中,TCP 會遇到大量的安全問題。

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

2.2.2 不可避免的資料安全問題

相信 SSL/TLS/HTTPS 這一類的字眼大家都不陌生,整個 TLS(傳輸安全層)實際上解決的是 TCP 的 payload 安全問題,當然這也是最緊要的問題。

比如對一個使用者而言,他可能能容忍一次轉賬失敗,但他肯定無法容忍錢被轉到攻擊者手上去了。TLS 的出現,為使用者提供了一種機制來保證中間人無法讀取,篡改的 TCP 的 payload 資料,TLS 同時還提供了一套安全的身份認證體系,來防止攻擊者冒充 Web 服務提供者。然而 TCP 的 header 這一層仍然是不在保護範圍内的,對于一個 on/off-path 攻擊者,仍然具備理論上随時關閉 TCP 連結的能力。

2.2.3 為了安全引發的效率問題

在目前的網絡環境中,安全通信已經成為了最基本的要求。熟悉 TLS 的同學都知道,TLS 也是需要握手和互動的,雖然 TLS 協定經過多年的實踐和演進,已經設計并落地了大量的優化手段(如 TLS1.3、會話複用、PSK、0-RTT 等技術),但由于 TLS 和 TCP 的分層設計,一個安全資料通道的建立實際上仍是一個相對繁瑣的流程。以一次基于 TLS1.3 協定的資料安全通道建立流程為例,其詳細互動如下圖:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

可以看到,在一個 client 正式開始發送應用層資料之前,需要 3 個 RTT 的互動,這算是一個非常大的開銷。而從流程上來看,TCP 握手和 TLS 的握手似乎比較相似,有融合在一起的可能。的确有相關的文獻探讨過在 SYN 封包裡融合 ClientHello 的可行性,不過由于以下原因,這部分的探索也慢慢不了了之。

  1. TLS 本身也是基于有序傳輸設計的協定,融合在 TCP 中需要做大量的重新設計;
  2. 出于安全的考慮,TCP 的 SYN 封包被設計為不能攜帶資料,如果要攜帶 clienthello,則需要對協定棧做大量改動,而由于 TCP 是一個核心協定棧,改動和疊代是一個痛苦且難以落地的過程;
  3. 新的協定難以和傳統 TCP 相容,大面積使用的可能性也很低。

2.2.4 TCP 的設計問題

出于 TCP 設計的曆史背景,當時的網絡情況并沒有現在這麼複雜,整個網絡的瓶頸在于帶寬,是以整個 TCP 的字段設計非常精簡,然而造成的效果就是将控制通道和資料通道被耦合的設計在了一起,在某些場景下就會形成問題。

比如:

seq 的二義性問題:設想這樣的一個場景,發送端發送了一個 TCP 封包,由于通信的中間裝置發生了阻塞,導緻該封包被延遲轉發了,發送端遲遲未收到 ACK,便重新發送了一個 TCP 封包,在新的 TCP 封包達到接收端時,被延遲轉發的封包也達到了接收端,接收端隻會響應一個 ACK。而用戶端收到 ACK 時,并不清楚這個 ACK 是對延遲轉發的封包的 ACK,還是新的封包的 ACK,帶來的影響也就是 RTT 的估計會不準确,進而影響擁塞控制算法的行為,降低網絡效率。

難用的 TCP keepalive:比如 TCP 連接配接中的另一方因為停電突然斷網,我們并不知道連接配接斷開,此時發送資料失敗會進行重傳,由于重傳包的優先級要高于 keepalive 的資料包,是以 keepalive 的資料包無法發送出去。隻有在長時間的重傳失敗之後我們才能判斷此連接配接斷開了。

隊頭阻塞問題:嚴格來說這并不算 TCP 自身的問題,因為 TCP 本身是一個面向連結的協定,它保證了一個連結上的資料可靠傳輸,也算完成了任務。然而随着網際網路的普及,人們利用網絡傳輸的資料越來越多,如果将所有資料都放在一個 TCP 連結上傳輸,其中某一個資料發生丢包,後面的資料的傳輸都會被 block 住,嚴重影響效率。當然,使用多個 TCP 連結傳輸資料是一種解決方案,但多個連結又會帶來新的開銷問題及連結管理問題。

了解了 TCP 的這些問題,我們就能從 QUIC 的一系列複雜的機制中抽絲剝繭,看清 QUIC 本身設計的源頭思路。

2.3 QUIC 的建聯設計

和 TCP 一樣,QUIC 的首要目标也是提供一個可靠、有序的流式傳輸協定。不僅如此,QUIC 還要保證原生的資料安全以及傳輸的高效。

可以說,QUIC 就是在以一種更簡潔高效的機制去對标 TCP+TLS。當然,和 TCP+TLS 一樣,QUIC 建聯流程的本質都是在為上述特性服務,由于 QUIC 是基于 UDP 重新設計的協定,便也就沒那麼多的曆史包袱,我們先來整理下我們對這個新的協定的訴求:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

整理好需求之後,我們再來看看 QUIC 實作的效果。

先來看一個 QUIC 連結的建立流程,一次 QUIC 連結建立的粗略示意圖如下:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

可以看到,QUIC 相比于 TCP+TLS,隻需要 1.5 個 RTT 就能完成建聯,大大提升了效率。熟悉 TLS 的同學可能會發現 QUIC 的建聯流程似乎跟 TLS 握手沒有太大差別,TLS 本身又是一個強依賴于資料有序可靠傳輸的協定,然而 QUIC 又依賴 TLS 去達成有序且可靠的能力,這似乎成為了一個雞和蛋的問題,那麼 QUIC 是如何解決這個問題的呢?

我們需要更深一步去看看 QUIC 建聯的流程,粗略示意圖僅僅隻能幫我們粗略感受下 QUIC 相比于 TCP+TLS 流程的高效,我們來進一步看看更精細化的 QUIC 建聯流程:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

這裡的圖顯得有些繁瑣,抛去 TLS 握手的細節(關于 QUIC 的 TLS 設計,我們會在系列文章的後續專門用一篇文章講解),整個流程實際上還是和 TCP 一樣是一個請求-響應的模式,然而相比于 TCP+TLS,我們還看到了一些不一樣的地方:

1.圖中多了"init packet"、"handshake packet"、"short-header packet"的概念;

2.圖中多了 pkt_number的概念以及stream+offset的概念;

3.pkt_number 的下标變化似乎有些奇怪。

而這些不同機制就是 QUIC 實作相比于 TCP 來說更高效的點,讓我們來逐一分析。

2.3.1 pkt_number 的設計

pkt_number 從流程圖看起來,和 TCP 的 seq 字段比較類似,然而實際上還是有不少差别,可以說,pkt_number 的設計就是為了解決前面提到的 TCP 的問題的,我們來看看 pkt_number 的設計:

  1. 從 0 開始的下标

前面我們提到過,如果 TCP 的 seq 是一個從 0 開始的字段,那麼其實不需要握手,就可以開始資料的有序發送,是以解決 TLS 和有序可靠傳輸這個雞和蛋問題的方案非常簡單。即 pkt_number 從 0 開始計數,便可直接保證 TLS 資料的有序。

  1. 加密 pkt_number 以保障安全

當然 pkt_number 從 0 開始技術便也就遇上了和 TCP一樣的安全問題,解決方案也很簡單,就是用為 pkt_number 加密,pkt_number 加密後,中間人便無法擷取到加密的 pkt_number 的 key,便也無法擷取到真實的 pkt_number,也就無法通過觀測 pkt_number 來預測後續的資料發送。而這裡又引申出了另一個問題,TLS 需要握手完成後才能得到中間人無法擷取的 key,而 pkt_number 又在 TLS 握手之前又存在,這看起來又是一個雞和蛋的問題,至于其解決方案,這裡先賣一個關子,留到後面 QUIC-TLS 的專題文章再講。

  1. 細粒度的 pkt_number space 的設計

TLS 嚴格來說并不是一個狀态嚴格遞進的協定,每進入一個新的狀态,還是有可能會收到上一個狀态的資料,這麼說有點抽象。

舉個例子,TLS1.3 引入了一個 0-RTT 的技術,該技術允許 client 在通過 clientHello 發起 TLS 請求時,同時發送一些應用層數。我們當然期望這個應用層資料的過程相對于握手過程來說是異步且互不幹擾的,而如果他們都是用同一個 pkt_number 來标示,那麼應用層資料的丢包勢必會導緻對握手過程的影響。是以,QUIC 針對握手的狀态設計了三種不同的 pkt_number space:

(1) init;

(2) Handshake;

(3) Application Data。

分别對應:

(1) TLS 握手中的明文資料傳輸,即圖中的 init packet;

(2) TLS 中通過 traffic secret 加密的握手資料傳輸,即圖中 handshake packet;

(3)握手完成後的應用層資料傳輸及 0-RTT 資料傳輸,即圖中的 short header packet 以及圖中暫未畫出的 0-RTT packet。

三種不同的 space 保證了三個流程的丢包檢測互不影響。關于這部分在系列文章後續(關于 QUIC 丢包檢測)還會再次深入剖析。

  1. 永遠自增的 pkt_number

這裡的永遠自增指的是 pkt_number 的明文随每個 QUIC packet 發送,都會自增 1。pkt_number 的自增解決的是二義性問題,接收端收到 pkt_number 對應的 ACK 之後,可以明确的知道到底是重傳的封包被 ACK 了,還是新的封包被 ACK 了,這樣 RTT 的估計及丢包檢測,就可以更加精細,不過僅僅隻靠自增的 pkt_number 是無法保證資料的有序的,我們再來看看 QUIC 提供了什麼樣的機制保證資料的有序。

2.3.2 基于 stream 的有序傳輸

我們知道 QUIC 是基于 UDP 實作的協定,而 UDP 是不可靠的面向封包的協定,這和 TCP 基于 IP 層的實作并沒有什麼本質上的不同,都是:

(1) 底層隻負責盡力而為的,以 packet 為機關的傳輸;

(2) 上層協定實作更關鍵的特性,如可靠,有序,安全等。

從前面我們知道 TCP 的設計導緻了連結次元的隊頭阻塞問題,而僅僅依靠 pkt_number 也無法實作資料的有序,是以 QUIC 必須要一種更細粒度的機制來解決這些問題:

  1. 流既是一種抽象,也是一種機關

TCP 隊頭阻塞的根因來自于其一條連結隻有一個發送流,流上任意一個 packet 的阻塞都會導緻其他資料的失敗。當然解決方案也不複雜,我們隻需要在一條 QUIC 連結上抽象出多個流來即可,整體的思路如下圖:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

隻要能保證各個 stream 的發送獨立,那麼我們實際上就避免了 QUIC 連結本身的隊頭阻塞,即一個流阻塞我們也可以在其他流上發送資料。

有了單連結多流的抽象,我們再來看 QUIC 的傳輸有序性設計,實際上 QUIC 在 stream 層面之上,還有更細粒度的機關,稱作 frame。一個承載資料的 frame 攜帶有一個 offset 字段,表明自己相對于初始資料的偏移量。而初始偏移量為 0,這個思路等價于 pkt_number 等于 0,即不需要握手即可開始資料的發送。熟悉 HTTP/2、GRPC 的同學應該比較清楚這個 offset 字段的設計,和流式資料的傳輸是一樣的。

  1. 一個 TLS 握手也是一個流

雖然 TLS 資料并沒有一個固定的 stream 來标示,但其可以被看作為一個特定的 stream,或者說是所有其他 stream 能建立起來的初始 stream,因為它其實也是基于 offset 字段和固定的 frame 來承載的,這也就是 TLS 資料有序的保障所在。

  1. 基于 frame 的控制

有了 frame 這一層的抽象之後,我們當然可以做更多的事情。除了承載實際資料之外,也可以承載一些控制資料,QUIC 的設計汲取了 TCP 的經驗教訓,對于 keepalive、ACK、stream 的操作等行為,都設定了專門的控制 frame,這也就實作了資料和控制的完全解耦,同時保證了基于 stream 的細粒度控制,讓 QUIC 成為更精細化的傳輸層協定。

講到這裡,其實可以看到我們從 QUIC 建聯流程的探讨中,已經明确了 QUIC 設計的目标,正如文章中一直在強調的概念:

“無論是建聯還是什麼流程,都是在為實作 QUIC 的特性而服務”。

我們現在對 QUIC 的特性及實作已經有了一些認知,來小結一下:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

此時,我們再來看看 QUIC 的建聯過程中的一些設計,就不會再被其複雜的流程所困擾,更能直擊它的本質,因為這些設計是在 QUIC 建聯大架構确認下來之後,一些細枝末節的點,而這些點往往又會在 RFC 中占據不小的篇幅,消耗讀者的心力。

舉個例子,比如針對 QUIC 的放大攻擊及其處理方式:放大攻擊的原理為,TLS 握手過程中 clientHello 資料很少。但 server 可能響應很多資料,這就可能形成放大攻擊,比如 attacker A 發起大量 clientHello,但把自己的 src ip 修改為 client B,這樣 attacker A 就成倍的放大了自己的流量,以攻擊 client B,其解決方案也很簡單,QUIC 要求每個 client 的首包都 padding 到一定的長度,并且在服務端提供了 address validation 機制,同時在握手完成之前,限制服務端響應的資料大小。

RFC9000 中花了一章節來介紹這個機制,但其本質來說隻是針對 QUIC 目前握手流程的問題的修補,而不是為了設計這個機制再去設計了握手流程。

PART. 3 連結關閉

從 TCP 看 QUIC 連結的優雅關閉

連結關閉是一個簡單的訴求,可以簡單梳理為兩個目标:

1.使用者可以主動優雅關閉連結,并能通知對方,釋放資源。

2.當通信一端無法建立的連結時,有一個通知對方的機制。

然而訴求很簡單,TCP 的實作卻很不簡單,我們來看看這個 TCP 連結關閉流程狀态機的轉移圖:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

這個流程看起來就夠複雜了,牽扯出來的問題就更不少,比如經典面試題:

“為什麼需要 TIME_WAIT?”

“TIME_WAIT 的連接配接數過多需要怎麼處理?”

“tcp_tw_reuse 和 tcp_tw_recycle 等核心參數的作用和差別?”

而這一切問題的根因都來源于 TCP 的連結和流的綁定,或者說是控制信令和資料通道的耦合。

我們不禁要提出一個靈魂拷問“我們需要的是全雙工的資料傳輸模式,但我們真的需要在連結次元做這個事情嗎?”這麼說似乎有一些抽象,還是以 TCP 的 TIME_WAIT 設計為例子來說明,我們來自底向上的看看 TCP 的問題:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

再回到我們的問題上,如果我們将流和連結區分開,在連結次元保證流的控制指令可靠傳輸,連結本身實作一個簡單的單工關閉過程,即通信一端主動關閉連結,則整個連結關閉,是否一切就簡單起來了呢?

當然這就是 QUIC 的解法,有了這一層思路之後,我們來整理下 QUIC 連結關閉的訴求:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

抛開流的關閉流程設計(關于流的這部分會在系列文章後續關于 stream 的設計進行分享),在連結次元我們就可以得到一個清爽的狀态機:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

可以看到,得益于單工的關閉模式,在整個 QUIC 連結關閉的流程裡,關閉指令隻有一個,即圖中的 CONNECTION_CLOSE,而關閉的狀态也隻有兩個,即圖中的 closing 和 draing。我們先來看看兩種狀态下終端的行為:

  • closing 狀态:當使用者主動關閉連結時,即進入該狀态,該狀态下,終端收到所有的應用層資料都将隻會回複 CONNECTION_CLOSE
  • draining 狀态:當終端收到 CONNECTION_CLOSE 時,即進入該狀态,該狀态下終端不再回複任何資料

更簡單的是,CONNECTION_CLOSE 是一個不需要被 ACK 的指令,也就意味着不需要重傳。因為從連結次元而言,我們隻需要保證最後能成功關閉的連結,并且新的連結不被老的關閉指令影響即可,這種簡單的 CONNECTION_CLOSE 指令就能實作所有的訴求。

3.2 更安全的 reset 的方式

當然,連結關閉也分為多種情況,和 TCP 一樣,除了上一節提到的 QUIC 一端主動關閉連結的模式,QUIC 也需要提供無法回複響應時的,直接 reset 對端連結的能力。

而 QUIC reset 對端連結的方式相比于 TCP 來說更加安全,該機制被稱作 stateless reset,這并不是一個十分複雜的機制。在 QUIC 連結建立好之後,QUIC 雙方會同步一個 token,而後續的連結關閉将通過校驗這個 token 來來判斷該對端是否有權限來 reset 這個連結,這種機制從根本上規避了前面提到的 TCP 被惡意 reset 的這種攻擊模式,整個流程如下圖:

深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

當然,stateless reset 的方案并不是銀彈,安全的代價是更窄的使用範圍。因為為了保障安全,token 必須通過安全的資料通道進行傳輸(在 QUIC 中被限定為 NEW_TOKEN frame),并且接收端需要維持一個記錄這個 token 的狀态,也隻有通過這個狀态才可以保證 token 的有效性。

是以 stateless reset 被限定為 QUIC 連結關閉的最後手段,并且也隻能在隻能使用在用戶端和服務端均處于一個相對正常的情況下正常工作,比如這樣的情況 stateless reset 就不适用,服務端并沒有監聽在 443 端口,但用戶端發送資料到 443 端口,而這種情況在 TCP 協定棧下是可以 RST 掉的。

3.3 工程考量的逾時斷鍊

keepalive 機制本身并沒有什麼花樣,都是一個計時器加探測封包即可搞定,而 QUIC 得益于連結和資料流的拆分,關閉連結變成了一個非常簡單的事情,keepalive 也就變得更簡單易用,QUIC 在連結次元上提供了名為 PING frame 的控制指令,用于主動探測保活。

更簡單的是,QUIC 在逾時後關閉連結的方式是 silently close,即不通知對端,本機直接釋放掉連結所有的資源。silently close 的好處是資源可以立即得到釋放,特别是對于 QUIC 這種單連結上既要維護 TLS 狀态,也要維護流狀态的協定來說,有很大的收益。

但其劣勢在于後續如果有之前連結資料到來,則隻能通過 stateless reset 的方式通知對端關閉連結,stateless reset 的關閉相對來說 CONNECTION_CLOSE 開銷更大。是以這部分可以說完全是一個 tradeoff,而這部分的設計方案的最終敲定,更多來自于大量工程實踐的經驗與結果。

PART. 4 從 QUIC 連結的建立與關閉看協定的演進

從 TCP 到 QUIC,雖然隻是網絡協定技術的演進,但我們也可以管中窺豹地看一下整個網絡發展的趨勢。連結的建立與關閉隻是我們對 QUIC 協定的切入點,正如文章一直在強調的部分,無論是建聯還是什麼流程,都是在為實作 QUIC 的特性而服務,而本文除了在詳細分析 QUIC 的連結建立的關閉流程之外,更是在總結這些特性的由來及設計思路。

通讀全文,我們可以看到,一個現代化的網絡協定已經繞不開安全這個訴求了,可以說“安全是一切的基礎,效率則是永恒的追求”。

而 QUIC 首先從收斂分層協定的思路出發,統一了安全和可靠兩種互動訴求,這似乎也在提示我們,未來協定的發展,似乎也不必再完全遵從 OSI 的模型。分層是為了各個元件更良好分工協作,而收斂則是極緻的性能追求,TCP+TLS 可以收斂到 QUIC,那麼就如華為提出的 NEW IP 技術一樣,如果我們把智能路由等技術結合起來,所有三層及以上網絡協定也未嘗不能收斂到一個全新的 IPSEC 協定中去。

當然這些都來的太遠了,QUIC 本身是一個非常接地氣的協定,在其以開源為主導的标準形成過程中,吸收了大量工程經驗,使其不至于有太多理想化的特性,且可擴充性非常強,我想未來這樣的工作模式,也将是一種主流的模式。

結 語

撰寫本文是一個痛苦的過程,正如閱讀 RFC 一樣,想要将 QUIC 某個方向的技術完全自包含在一篇文章之内幾乎不可能,而本文選擇将一些其他的依賴技術用弱化的方式來表達,并期望能在将來以單一文章的方式去着重介紹。

是以讀者若想要全面了解 HTTP/3 或者 QUIC,也請關注後續的文章,并拉通閱讀,方能有更深的體會。

當然,本文都是基于作者自己的個人了解,難免存在纰漏之處,如果讀者發現有相關問題,歡迎随時一起深入探讨。

本周推薦閱讀

雲原生運作時的下一個五年 積跬步至千裡:QUIC 協定在螞蟻集團落地之綜述 網商雙十一基于 ServiceMesh 技術的業務鍊路隔離技術及實踐 Service Mesh 在中國工商銀行的探索與實踐
深入 HTTP/3(一)|從 QUIC 連結的建立與關閉看協定的演進

繼續閱讀