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

SOFAStack發表於2021-12-22

文|曾柯(花名:毅絲 )

螞蟻集團高階工程師

負責螞蟻集團的接入層建設工作
主要方向為高效能安全網路協議的設計及優化

本文 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 的三次握手的示意圖:

圖片

為了保證通訊雙方都能確認對端資料的傳送順序,收發端都需要各自記錄對端的當前 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 會遇到大量的安全問題。

圖片

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 協議的資料安全通道新建流程為例,其詳細互動如下圖:

圖片

可以看到,在一個 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 重新設計的協議,便也就沒那麼多的歷史包袱,我們先來整理下我們對這個新的協議的訴求:

圖片

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

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

圖片

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

我們需要更深一步去看看 QUIC 建聯的流程,粗略示意圖僅僅只能幫我們粗略感受下 QUIC 相比於 TCP+TLS 流程的高效,我們來進一步看看更精細化的 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 連結上抽象出多個流來即可,整體的思路如下圖:

圖片

只要能保證各個 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 的特性及實現已經有了一些認知,來小結一下:

圖片

此時,我們再來看看 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 連結關閉流程狀態機的轉移圖:

圖片

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

“為什麼需要 TIME_WAIT?”

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

“tcp_tw_reuse 和 tcp_tw_recycle 等核心引數的作用和區別?”

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

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

圖片

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

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

圖片

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

圖片

可以看到,得益於單工的關閉模式,在整個 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 的這種攻擊模式,整個流程如下圖:

圖片

當然,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 在中國工商銀行的探索與實踐

img

相關文章