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 的那篇文章中進行了說明和介紹。)
假如有五個請求被同時發出,如果第一個請求沒有處理完成,就會導致後續的請求也無法得到處理,如下圖所示
如果第一個請求沒有被處理,那麼 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 中。下面來看張圖。
可以看到,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 握手(兩次),如下圖所示。
對於很多短連線場景,這種握手延遲影響較大,而且無法消除。
相比之下,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。
比如有一個 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 會按照順序進行組裝,這同時也保證了資料的順序性。
眾所周知,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