10 分鐘講完 QUIC 協議

建議閱讀本文需要搭配作者 HTTP 相關文章食用。

歷史 HTTP 系列文章:

看完這篇HTTP,跟面試官扯皮就沒問題了

HTTP 2。0 ,有點炸 !

這裡先來回顧一下 HTTP 的發展過程。首先,我們想要一種能夠在網路上獲取文件內容的協議,透過一種叫做 GET 請求的方式進行獲取,後來這種 GET 請求被寫入了官方文件,HTTP/1。0 應運而生。HTTP/1。0 的出現可以說是顛覆性的,它裡面涵蓋的一些標準我們目前還仍在使用,例如 HTTP header,協議號的概念,不過,這個版本的 HTTP 還有一些明顯的缺陷,比如它不支援永續性連線,每次請求響應後,都需要斷開連線,這樣效率很差。沒過了多久,制定了 HTTP/1。1 標準,這個標準是網際網路上使用最頻繁的一個標準,HTTP/1。1 解決了之前不支援永續性連線的缺陷,而且 HTTP/1。1 還增加了快取和控制模組。

但是,即便 HTTP/1。1 解決了一部分連線效能問題,它的效率仍不是很高,而且 HTTP 還有一個隊頭阻塞問題(關於隊頭阻塞我已經在 HTTP2。0 的那篇文章中進行了說明和介紹。)

假如有五個請求被同時發出,如果第一個請求沒有處理完成,就會導致後續的請求也無法得到處理,如下圖所示

10 分鐘講完 QUIC 協議

如果第一個請求沒有被處理,那麼 2 3 4 5 這四個請求會直接阻塞在客戶端,等到請求 1 被處理完畢後,才能逐個發出。網路通暢的時候效能影響不大,不過一旦請求 1 因為某些原因沒有抵達伺服器,或者請求因為網路阻塞沒有及時返回,影響的就是所有後續請求,導致後續請求無限阻塞下去,問題就變得比較嚴重了。

雖然 HTTP/1。1 使用了 pipling 的設計用於解決隊頭阻塞問題,但是在 pipling 的設計中,每個請求還是按照順序先發先回,並沒有從根本上解決問題。隨著協議的不斷更新,提出了 HTTP/2。0 。

HTTP/2。0

HTTP/2。0 解決隊頭阻塞的問題是採用了 stream 和分幀的方式。

HTTP/2。0 會將一個 TCP 連線切分成為多個 stream,每個 stream 都有自己的 stream id,這個 stream 可以是客戶端發往服務端,也可以是服務端發往客戶端。

HTTP/2。0 還能夠將要傳輸的資訊拆分為幀,並對它們進行二進位制格式編碼。也就是說,HTTP/2。0 會將 Header 頭和 Data 資料分別進行拆分,而且拆分之後的二進位制格式位於多個 stream 中。下面來看張圖。

10 分鐘講完 QUIC 協議

可以看到,HTTP/2。0 透過這兩種機制,將多個請求分到了不同的 stream 中,然後將請求進行分幀,進行二進位制傳輸,每個 stream 可以不用保證順序亂序傳送,到達客戶端後,客戶端會根據每個 stream 進行重組,而且可以根據優先順序來優先處理哪個 stream。

QUIC 協議

雖然 HTTP/2。0 解決了隊頭阻塞問題,但是每個 HTTP 連線都是由 TCP 進行連線建立和傳輸的,TCP 協議在處理包時有嚴格的順序要求。這也就是說,當某個包切分的 stream 由於某些原因丟失後,伺服器不會處理其他 stream,而會優先等待客戶端傳送丟失的 stream 。舉個例子來說,假如有一個請求有三個 stream,其中 stream2 由於某些原因丟失了,那麼 stream1 和 stream 2 的處理也會阻塞,只有收到重發的 stream2 之後,伺服器才會再次進行處理。

這就是 TCP 連線的癥結所在。

鑑於這個問題,我們先把 TCP 放一放,先來認識一波 QUIC 協議。

QUIC 的小寫是 quic,諧音 quick,意思就是

。它是 Google 提出來的一個基於 UDP 的傳輸協議,所以 QUIC 又被叫做

快速 UDP 網際網路連線

首先 QUIC 的第一個特徵就是快,為什麼說它快,它到底快在哪呢?

我們大家知道,HTTP 協議在傳輸層是使用了 TCP 進行報文傳輸,而且 HTTPS 、HTTP/2。0 還採用了 TLS 協議進行加密,這樣就會導致三次握手的連線延遲:即 TCP 三次握手(一次)和 TLS 握手(兩次),如下圖所示。

10 分鐘講完 QUIC 協議

對於很多短連線場景,這種握手延遲影響較大,而且無法消除。

相比之下,QUIC 的握手連線更快,因為它使用了 UDP 作為傳輸層協議,這樣能夠減少三次握手的時間延遲。而且 QUIC 的加密協議採用了 TLS 協議的最新版本

TLS 1。3

,相對之前的

TLS 1。1-1。2

,TLS1。3 允許客戶端無需等待 TLS 握手完成就開始傳送應用程式資料的操作,可以支援1 RTT 和 0 RTT,從而達到

快速建立連線

的效果。

我們上面還說過,HTTP/2。0 雖然解決了隊頭阻塞問題,但是其建立的連線還是基於 TCP,無法解決請求阻塞問題。

而 UDP 本身沒有建立連線這個概念,並且 QUIC 使用的 stream 之間是相互隔離的,不會阻塞其他 stream 資料的處理,所以使用 UDP 並不會造成隊頭阻塞。

在 TCP 中,TCP 為了保證資料的可靠性,使用了

序號+確認號

機制來實現,一旦帶有 synchronize sequence number 的包傳送到伺服器,伺服器都會在一定時間內進行響應,如果過了這段時間沒有響應,客戶端就會重傳這個包,直到伺服器收到資料包並作出響應為止。

那麼 TCP 是如何判斷它的重傳超時時間呢?

TCP 一般採用的是

自適應重傳演算法

,這個超時時間會根據往返時間 RTT 動態調整的。每次客戶端都會使用相同的 syn 來判斷超時時間,導致這個 RTT 的結果計算的不太準確。

雖然 QUIC 沒有使用 TCP 協議,但是它也保證了可靠性,QUIC 實現可靠性的機制是使用了

Packet Number

,這個序列號可以認為是 synchronize sequence number 的替代者,這個序列號也是遞增的。與 syn 所不同的是,不管伺服器有沒有接收到資料包,這個 Packet Number 都會 + 1,而 syn 是隻有伺服器傳送 ack 響應之後,syn 才會 + 1。

10 分鐘講完 QUIC 協議

比如有一個 PN = 10 的資料包在傳送的過程中由於某些原因遲遲沒到伺服器,那麼客戶端會重傳一個 PN = 11 的資料包,經過一段時間後客戶端收到 PN = 10 的響應後再回送響應報文,此時的 RTT 就是 PN = 10 這個資料包在網路中的生存時間,這樣計算相對比較準確。

雖然 QUIC 保證了資料包的可靠性,但是資料的可靠性是如何保證的呢?

QUIC 引入了一個

stream offset

的概念,一個 stream 可以傳輸多個 stream offset,每個 stream offset 其實就是一個 PN 標識的資料,即使某個 PN 標識的資料丟失,PN + 1 後,它重傳的仍舊是 PN 所標識的資料,等到所有 PN 標識的資料傳送到伺服器,就會進行重組,以此來保證資料可靠性。到達伺服器的 stream offset 會按照順序進行組裝,這同時也保證了資料的順序性。

10 分鐘講完 QUIC 協議

眾所周知,TCP 協議的具體實現是由作業系統核心來完成的,應用程式只能使用,不能對核心進行修改,隨著移動端和越來越多的裝置接入網際網路,效能逐漸成為一個非常重要的衡量指標。雖然行動網路發展非常快,但是使用者端的更新卻非常緩慢,我仍然看見有很多地區很多計算機還仍舊使用 xp 系統,儘管它早已發展了很多年。服務端系統不依賴使用者升級,但是由於作業系統升級涉及到底層軟體和執行庫的更新,所以也比較保守和緩慢。

QUIC 協議的一個重要特點就是

可插拔性

,能夠動態更新和升級,QUIC 在應用層實現了擁塞控制演算法,不需要作業系統和核心的支援,遇到擁塞控制演算法切換時,只需要在伺服器重新載入一遍即可,不需要停機和重啟。

我們知道 TCP 的流量控制是透過

滑動視窗

來實現的,如果你對滑動視窗不太熟悉,你可以看下我寫的這篇文章

TCP 基礎知識

在文章後面有提到了滑動視窗的一些概念。

而 QUIC 也實現了流量控制,QUIC 的流量控制也是使用了視窗更新

window_update

,來告訴對端它可以接受的位元組數。

TCP 協議頭部沒有經過加密和認證,所以在傳輸的過程中很可能被篡改,與之不同的是,QUIC 中的報文頭部都是經過認證,報文也經過加密處理。這樣只要對 QUIC 的報文有任何修改,接收端都能夠及時發現,保證了安全性。

總的來說,QUIC 相比於 HTTP/2。0 來說,具有下面這些優勢

使用 UDP 協議,不需要三次連線進行握手,而且也會縮短 TLS 建立連線的時間。

解決了隊頭阻塞問題

實現動態可插拔,在應用層實現了擁塞控制演算法,可以隨時切換。

報文頭和報文體分別進行認證和加密處理,保障安全性。

連線能夠平滑遷移

連線平滑遷移指的是,你的手機或者移動裝置在 4G 訊號下和 WiFi 等網路情況下切換,不會斷線重連,使用者甚至無任何感知,能夠直接實現平滑的訊號切換。

QUIC 相關資料

QUIC 協議比較複雜,想自己完全實現一套對筆者來說還比較困難。

讀者有興趣的話可以先看看開源實現有哪些。

1)Chromium:https://github。com/hanpfei/chromium-net

這個是官方支援的。優點自然很多,Google 官方維護基本沒有坑,隨時可以跟隨 chrome 更新到最新版本。不過編譯 Chromium 比較麻煩,它有單獨的一套編譯工具。暫時不建議考慮這個方案。

2)proto-quic:https://github。com/google/proto-quic

從 chromium 剝離的一個 QUIC 協議部分,但是其 github 主頁已宣佈不再支援,僅作實驗使用。不建議考慮這個方案。

3)goquic:https://github。com/devsisters/goquic

goquic 封裝了 libquic 的 go 語言封裝,而 libquic 也是從 chromium 剝離的,好幾年不維護了,僅支援到 quic-36, goquic 提供一個反向代理,測試發現由於 QUIC 版本太低,最新 chrome 瀏覽器已無法支援。不建議考慮這個方案。

4)quic-go:https://github。com/lucas-clemente/quic-go

quic-go 是完全用 go 寫的 QUIC 協議棧,開發很活躍,已在 Caddy 中使用,MIT 許可,目前看是比較好的方案。

那麼,對於中小團隊或個人開發者來說,比較推薦的方案是最後一個,即採用 caddy https://github。com/caddyserver/caddy/wiki/QUIC 來部署實現 QUIC。caddy 這個專案本意並不是專門用來實現 QUIC 的,它是用來實現一個免籤的 HTTPS web 伺服器的(caddy 會自動續簽證書)。而QUIC 只是它的一個附屬功能(不過現實是——好像用它來實現 QUIC 的人更多)。

從 Github 的技術趨勢來說,有關 QUIC 的開源資源越來越多,有興趣可以自已逐一研究研究:https://github。com/search?q=quic