一、QUIC協議
QUIC ,即 快速UDP網絡連接 ( Quick UDP Internet Connections ), 是由 Google 提出的實驗性網絡傳輸協議 ,位於 OSI 模型傳輸層。 QUIC 旨在解決 TCP 協議的缺陷,並最終替代 TCP 協議, 以減少數據傳輸,降低連接建立延 遲時間,加快網頁傳輸速度。
標准文檔地址:https://quicwg.org/base-drafts/rfc9000.html
1. QUIC框圖
1.1 為什么QUIC在應用層實現
- 新的傳輸層協議通常會經過嚴格的設計,分析和評估可重復的結果,證明候選協議對 現有協議的正確性和公平性,開發新的傳輸層協議和它在操作系統進行廣泛部署之間 通常需要花費數年的時間。
- 再者,用戶與服務器之間要經過許多防火牆、NAT(地址轉換)、路由器和其他中間設 備,這些設備很多只認TCP和UDP。如果使用另一種傳輸層協議,那么就會有可能無法 建立連接或者報文無法轉發,這些中間設備會認為除TCP和UDP協議以外的協議都是不 安全或者有問題的。
1.2 QUIC協議術語
QUIC連接:CLient和Server之間的通信關心,Client發起連接,Server接收連接
流(Stream):一個QUIC連接內,單向或者雙向的有序字節流。一個QUIC連 接可以同時包含多個Stream
幀(Frame):QUIC連接內的最小通信單元。一個QUIC數據包(packet)中的 數據部分包含一個或多個幀
1.3 QUIC和TCP對比
2. QUIC報文格式
2.1 QUIC數據包格式
- Header是明文的,包含4個字段:Flags、Connection ID、QUIC Version、Packet Number
- Data 是加密的,可以包含 1 個或多個 frame,每個 frame 又分為 type 和 payload, 其中 payload 就是應用數據
2.2 QUIC Stream幀
數據幀有很多類型:Stream、ACK、Padding、Window_Update、Blocked 等,這里重點介 紹下用於傳輸應用數據的 Stream 幀。
Frame Type:幀類型,占用1個字節
- Bit7:必須設置為 1,表示 Stream 幀
- Bit6:如果設置為 1,表示發送端在這個 stream 上已經結束發送數據,流將處於半關閉狀態
- Bit5:如果設置為 1,表示 Stream 頭中包含 Data length 字段
- Bit4-2:表示 offset 的長度。000 表示 0 字節,001 表示 2 字節,010 表示 3 字節,以此類推
- Bit1-0:表示 Stream ID 的長度。00 表示 1 字節,01 表示 2 字節,10 表示 3 字節,11 表示 4 字 節
Stream ID:流 ID,用於標識數據包所屬的流。后面的流量控制和多路復用會涉及到。
Offset:偏移量,表示該數據包在整個數據中的偏移量,用於數據排序。
Data Length: 數據長度,占用 2 個字節,表示實際應用數據的長度.
Data: 實際的應用數據
3. QUIC的特點
- 連接建立低時延
- 多路復用
- 無隊頭阻塞
- 靈活的擁塞控制機制
- 連接遷移
- 數據包頭和包內數據的身份認證和加密
- FEC前向糾錯
- 可靠性傳輸
- 其他
3.1 連接建立低延時
3.1.1 典型TCP+TLS連接
- 首先,執行三次握手,建立TCP連接(藍色部分)
- 然后,執行TLS握手,建立TLS連接(黃色)
- 此后開始傳輸業務數據
注意到,三次握手中的 ACK 包與 handshake 合並在一起發送。 這是 TCP 實現中使用的 延遲確認 技術, 旨在減少協議開銷,改善網絡性能。
客戶端和服務器之間要進行多輪協議交互,才能建立 TLS 連接,延遲相當嚴重。 平時訪問 https 網站明顯比 http 網站慢,三次握手和 TLS 握手難辭其咎。
3.1.2 首次連接對比
- 共3RTT:TCP+TLS1.2中: TCP三次握手建立連接需要1個RTT;TLS需要2個RTT完成 身份驗證;傳輸數據
- 共2RTT:TCP+TLS1.3中: TCP三次握手建立連接需要1個RTT;TLS需要1個RTT完成 身份驗證;傳輸數據
- 共1RTT: 首次QUIC連接中,Client 向 Server 發送消息,請求傳輸配置參數和加 密相關參數;Server 回復其配置參數;傳輸數據
3.1.2 再次連接對比
- 再次連接的概念:Client已經訪問過Server,在本地存放了Cookie。
- 2RTT:TCP+TLS1.2中: TCP三次握手建立連接需要1個RTT;TLS需要1個RTT完成身份 驗證(由於緩存的存在,減少1RTT);傳輸數據
- 1RTT:TCP+TLS1.3 中: TCP三次握手建立連接需要1個RTT;傳輸數據
- 0RTT:在客戶端與服務端的再次QUIC連接中,Client 本地已有 Server 的全部配置 參數(緩存),據此計算出初始密鑰,直接發送加密的數據包。
3.2 多路復用
在一個網頁里面總是會有多個數據要傳輸,總是希望多個數據能夠同時傳輸,以此 來提高用戶的體驗。
3.2.1 HTTP1.1
每個TCP連接同時只能處理一個請求—響應,為了提高響應速度,需要 同時創建多個連接,但是多個連接管理比較復雜。
3.2.2 HTTP2
- 每個TCP連接里面有多個邏輯上獨立的多個流(stream)
- 每個流可以傳輸不同的文件數據
- 解決了HTTP1一個連接無法同時傳輸多個數據的問題
- 缺點:容易出現隊頭阻塞問題
3.2.3 QUIC(HTTP3)
- 借鑒了HTTP2中流的概念
- 流之間互相獨立,即不同流之間的數據之間交付順序無關(如果 stream2的數據丟失,只會影響排在stream2后面的數據,stream1和 stream3的數據不會被影響)
- 建立在UDP之上,沒有依賴性
3.3 無隊頭阻塞
HTTP/2會出現隊頭阻塞問題,而在基於QUIC的HTTP/3中則很好地解決這一問題。
- UDP中的各個數據報獨立,基於UDP的QUIC中的各個stream獨立
- 即使stream2里有一個包丟失,因為stream之間互相獨立無關聯,所以不會 阻塞stream3、stream4,依然可以交付給上層
3.4 靈活的擁塞控制和機制
TCP 的擁塞控制實際上包含了四個算法:慢啟動,擁塞避免,快速重傳,快速恢復。
- New Reno:基於丟包檢測
- CUBIC:基於丟包檢測
- BBR:基於網絡帶寬
QUIC協議可以自己設置或實現擁塞控制協議。
QUIC 協議當前默認使用了 TCP 協議的 Cubic 擁塞控制算法。 這個機制還是可插拔的,能夠非常靈活地生效,變更和停止,可以根據場景來切換不同的方法。
QUIC協議在用戶空間實現,應用程序不需要停機和升級就能實現擁塞控制 的變更,在服務端只需要修改一下配置,reload 一下,完全不需要停止服務 就能實現擁塞控制的切換。甚至可以為每一個請求都設置一種擁塞控制算法。
而TCP在內核態,其擁塞控制難以進行修改和升級。
3.5 連接遷移
連接遷移:當客戶端切換網絡時,和服務器的連接並不會斷開,仍然可以正常通信。
對於 TCP 協議而言,這是不可能做到的。因為 TCP 的連接基於 4 元組:源 IP、源端口、 目的 IP、目的端口,只要其中 1 個發生變化,就需要重新建立連接。
但 QUIC 的連接是 基於 64 位的 Connection ID,網絡切換並不會影響 Connection ID 的變化,連接在邏輯上 仍然是通的。IP地址或者端口號發生變化時,只要ID不變,依然能夠維持原有連接,上層業務邏輯感 知不到變化,不會中斷。
客戶端的Connection ID是唯一的。
3.6 數據包頭和包內數據的身份認證和加密
相比於TCP,QUIC的安全性是內置的,也就是說是必須的。
在惡意環境下性能與TLS類似,友好環境下優於TLS,因為TLS使用一個會話密鑰, 如果這個密鑰被截獲的話就不能保證之前數據的安全性。
而QUIC使用兩個密鑰——初始密鑰和會話密鑰,並且QUIC提供密碼保護,TLS不提供。
除此之外,QUIC的包頭經過身份認證,包內數據是加密的。這樣如果QUIC數據 被惡意修改的話接收端是可以發現的,降低了安全風險。
而且數據加密之后,可以通過像防火牆或者nat這些中間件。
兩個密鑰的生成可參考:https://blog.csdn.net/chuanglan/article/details/85106706
3.7 FEC前向糾錯
FEC是Forward Error Correction前向錯誤糾正的意思,就是通過多發一些冗余的包, 當有些包丟失時,可以通過冗余的包恢復出來,而不用重傳。這個算法在多媒 體網關擁塞控制有重要的地位。QUIC的FEC是使用的XOR的方式,即發N + 1個包, 多發一個冗余的包,在正常數據的N個包里面任意一個包丟了,可以通過這個冗 余的包恢復出來,使用異或可以做到切換網絡操持連接。
3.8 可靠性傳輸
QUIC 是基於 UDP 協議的,而 UDP 是不可靠傳輸協議,那 QUIC 是如何實現可靠傳輸的呢?
可靠性傳輸有2個重要特點:
- 完整性:發送端發出的數據包,接收端都能收到
- 有序性:接收端能按序組裝數據包,解碼得到有效的數據
3.8.1 有序性設計
QUIC每個Stream幀中都有offset字段和StreamID字段,這使得亂序接收的數據能夠有序排列。
3.8.2 完整性設計
發送端通過包號(PKN)和確認應答(SACK)確認發送數據完整性。
- 客戶端:發送 3 個數據包給服務器(PKN = 1, 2,3)
- 服務器:通過 SACK 告知客戶端已經收到了 1 和 3,沒有收到 2
- 客戶端:重傳第 2 個數據包(PKN=4)
盡管QUIC會重傳數據包,但是新的數據包的PKN的繼續遞增的,即之前發送的數據包(PKN=2)和重傳的 數據包(PKN=4),雖然數據一樣,但包號不同。這也解決了TCP中,原始包和重傳包的序列號一樣帶來的重傳歧義問題。
由於TCP原始包和重傳包的序列號是一樣的,客戶端不知道服 務器返回的 ACK 包到底是原始包的,還是重傳包的。但 QUIC 的原始包和重傳包的序列號是不同的,也就可以判 斷 ACK 包的歸屬。
4. QUIC開源庫
- google的gquic 起源最早, 不過它不是單獨項目, 代碼在chromium項目里邊, 用的 是c++寫的, 可能不是很適合。
- 微軟的msquic, 用c寫的, 跨平台, 不過開始得比較晚。
- facebook的quic 用的是c++寫的. 暫不考慮。
- nginx的quic 沒有自帶client, 但它可與ngtcp2聯調。
- litespeed的 lsquic 是基於MIT的, 開始於2017年, 還算比較穩定, 用c語言編寫, 各 主流平台都有通過測試, 有server/client/lib, 它用於自家的各種產品, 暫時看上去 是最合適的。
- ngtcp2, 它是一個實驗性質的quic client, 很簡潔, 實現了幾乎每一版ietf draft. 從 代碼簡潔性上來看, 它無疑是最好的。目前srs流媒體服務器、curl等開源項目有 基於ngtcp2做二次開發。
4.1 ngtcp2
- https://github.com/ngtcp2/ngtcp2
- 采用C語言實現
- 范例client和server使用了c++17的特性,我們需要升級編譯器(比如, clang >= 8.0, 或 gcc >= 8.0)
- 編譯文檔見《QUIC開源庫安裝和實踐.pdf》