QUIC协议原理详解
1.QUIC是啥?
1.1 什么是QUIC
QUIC(Quick UDP Internet Connection)是谷歌推出的一套基于UDP的傳輸協議,它實現了TCP + HTTPS + HTTP/2的功能,目的是保證可靠性的同時降低網絡延遲。因為UDP是一個簡單傳輸協議,基于UDP可以擺脫TCP傳輸確認、重傳慢啟動等因素,建立安全連接只需要一的個往返時間,它還實現了HTTP/2多路復用、頭部壓縮等功能。
眾所周知UDP比TCP傳輸速度快,TCP是可靠協議,但是代價是雙方確認數據而衍生的一系列消耗。其次TCP是系統內核實現的,如果升級TCP協議,就得讓用戶升級系統,這個的門檻比較高,而QUIC在UDP基礎上由客戶端自由發揮,只要有服務器能對接就可以。
1.2 HTTP協議發展
1.2.1 HTTP歷史進程
- HTTP 0.9(1991年)只支持get方法不支持請求頭;
- HTTP 1.0(1996年)基本成型,支持請求頭、富文本、狀態碼、緩存、連接無法復用;
- HTTP 1.1(1999年)支持連接復用、分塊發送、斷點續傳;
- HTTP 2.0(2015年)二進制分幀傳輸、多路復用、頭部壓縮、服務器推送等;
- HTTP 3.0(2018年)QUIC 于2013年實現、2018年正式更名為HTTP3;
1.2.2 HTTP 1.0和HTTP1.1
- 隊頭阻塞:下個請求必須在前一個請求返回后才能發出,導致帶寬無法被充分利用,后續請求被阻塞(HTTP 1.1 嘗試使用流水線(Pipelining)技術,但先天 FIFO(先進先出)機制導致當前請求的執行依賴于上一個請求執行的完成,容易引起隊頭阻塞,并沒有從根本上解決問題)
- 協議開銷大:header里攜帶的內容過大,且不能壓縮,增加了傳輸的成本
- 單向請求:只能單向請求,客戶端請求什么,服務器返回什么
HTTP 1.0和HTTP 1.1的區別:
| 僅支持保持短暫的TCP連接(連接無法復用) | 默認支持長連接(請求可復用TCP連接) |
| 不支持斷點續傳 | 支持斷點續傳(通過在 Header 設置參數) |
| 前一個請求響應到達之后下一個請求才能發送,存在隊頭阻塞 | 優化了緩存控制策略 |
| / | 管道化,可以一次發送多個請求,但是響應仍是順序返回,仍然無法解決隊頭阻塞的問題 |
| / | 新增錯誤狀態碼通知 |
| / | 請求消息和響應消息都支持Host頭域 |
1.2.3 HTTP2
解決 HTTP1 的一些問題,但是解決不了底層 TCP 協議層面上的隊頭阻塞問題。
1.二進制傳輸:二進制格式傳輸數據解析起來比文本更高效;
2.多路復用:重新定義底層 http 語義映射,允許同一個連接上使用請求和響應雙向數據流。同一域名只需占用一個 TCP 連接,通過數據流(Stream)以幀為基本協議單位,避免了因頻繁創建連接產生的延遲,減少了內存消耗,提升了使用性能,并行請求,且慢的請求或先發送的請求不會阻塞其他請求的返回;
3.Header壓縮:減少請求中的冗余數據,降低開銷;
4.服務端可以主動推送:提前給客戶端推送必要的資源,這樣就可以相對減少一點延遲時間;
5.流優先級:數據傳輸優先級可控,使網站可以實現更靈活和強大的頁面控制;
6.可重置:能在不中斷 TCP 連接的情況下停止數據的發送;
缺點:HTTP 2中,多個請求在一個TCP管道中的,出現了丟包時,HTTP 2的表現反倒不如HTTP 1.1了。因為 TCP 為了保證可靠傳輸,有個特別的“丟包重傳”機制,丟失的包必須要等待重新傳輸確認,HTTP 2出現丟包時,整個 TCP 都要開始等待重傳,那么就會阻塞該TCP連接中的所有請求。而對于 HTTP 1.1 來說,可以開啟多個 TCP 連接,出現這種情況反到只會影響其中一個連接,剩余的 TCP 連接還可以正常傳輸數據。
1.2.4 HTTP3——QUIC
HTTP 是建立在 TCP 協議之上,所有 HTTP 協議的瓶頸及其優化技巧都是基于 TCP 協議本身的特性,HTTP2 雖然實現了多路復用,底層 TCP 協議層面上的問題并沒有解決(HTTP 2.0 同一域名下只需要使用一個 TCP 連接。但是如果這個連接出現了丟包,會導致整個 TCP 都要開始等待重傳,后面的所有數據都被阻塞了),而 HTTP3 的 QUIC 就是為解決 HTTP2 的 TCP 問題而生。
2. QUIC的關鍵特性
關于 QUIC 的原理,相關介紹的文章很多,這里再列舉一下 QUIC 的重要特性。這些特性是 QUIC 得以被廣泛應用的關鍵。不同業務也可以根據業務特點利用 QUIC 的特性去做一些優化。同時,這些特性也是我們去提供 QUIC 服務的切入點。
2.1 連接遷移
2.1.1 TCP的連接重連之痛
一條 TCP 連接是由四元組標識的(源 IP,源端口,目的 IP,目的端口)。什么叫連接遷移呢?就是當其中任何一個元素發生變化時,這條連接依然維持著,能夠保持業務邏輯不中斷。當然這里面主要關注的是客戶端的變化,因為客戶端不可控并且網絡環境經常發生變化,而服務端的 IP 和端口一般都是固定的。
比如大家使用手機在 WIFI 和 4G 移動網絡切換時,客戶端的 IP 肯定會發生變化,需要重新建立和服務端的 TCP 連接。
又比如大家使用公共 NAT 出口時,有些連接競爭時需要重新綁定端口,導致客戶端的端口發生變化,同樣需要重新建立 TCP 連接。
所以從 TCP 連接的角度來講,這個問題是無解的。
2.1.2 基于UDP的QUIC連接遷移實現
當用戶的地址發生變化時,如 WIFI 切換到 4G 場景,基于 TCP 的 HTTP 協議無法保持連接的存活。QUIC 基于連接 ID 唯一識別連接。當源地址發生改變時,QUIC 仍然可以保證連接存活和數據正常收發。
那 QUIC 是如何做到連接遷移呢?很簡單,QUIC是基于UDP協議的,任何一條 QUIC 連接不再以 IP 及端口四元組標識,而是以一個 64 位的隨機數作為 ID 來標識,這樣就算 IP 或者端口發生變化時,只要 ID 不變,這條連接依然維持著,上層業務邏輯感知不到變化,不會中斷,也就不需要重連。
由于這個 ID 是客戶端隨機產生的,并且長度有 64 位,所以沖突概率非常低。
圖2-1 TCP 和 QUIC 在 Wi-Fi 和 cellular 切換時,唯一標識的不同情況
2.2 低連接延時
2.2.1 TLS的連接時延問題
以一次簡單的瀏覽器訪問為例,在地址欄中輸入https://www.abc.com,實際會產生以下動作:
◎DNS遞歸查詢www.abc.com,獲取地址解析的對應IP;
◎TCP握手,我們熟悉的TCP三次握手需要需要1個RTT;
◎TLS握手,以目前應用最廣泛的TLS 1.2而言,需要2個RTT。對于非首次建連,可以選擇啟用會話重用,則可縮小握手時間到1個RTT;
◎HTTP業務數據交互,假設abc.com的數據在一次交互就能取回來。那么業務數據的交互需要1個RTT;經過上面的過程分析可知,要完成一次簡短的HTTPS業務數據交互,需要經歷:新連接 4RTT + DNS;會話重用 3RTT + DNS。
所以,對于數據量小的請求而言,單一次的請求握手就占用了大量的時間,對于用戶體驗的影響非常大。同時,在用戶網絡不佳的情況下,RTT延時會變得較高,極其影響用戶體驗。
下圖對比了TLS各版本與場景下的延時對比:
圖2-2 TLS各個版本握手時延
從對比我們可以看到,即使用上了TLS 1.3,精簡了握手過程,最快能做到0-RTT握手(首次是1-RTT);但是對用戶感知而言, 還要加上1RTT的TCP握手開銷。
Google有提出Fastopen的方案來使得TCP非首次握手就能附帶用戶數據,但是由于TCP實現僵化,無法升級應用,相關RFC到現今都是experimental狀態。這種分層設計帶來的延時,有沒有辦法進一步降低呢? QUIC通過合并加密與連接管理解決了這個問題,我們來看看其是如何實現真正意義上的0-RTT的握手, 讓與server進行第一個數據包的交互就能帶上用戶數據。
2.2.2 真0-RTT的QUIC握手
QUIC 由于基于 UDP,無需 TCP 連接,在最好情況下,短連接下 QUIC 可以做到 0RTT 開啟數據傳輸。而基于 TCP 的 HTTPS,即使在最好的 TLS1.3 的 early data 下仍然需要 1RTT 開啟數據傳輸。而對于目前線上常見的 TLS1.2 完全握手的情況,則需要 3RTT 開啟數據傳輸。對于 RTT 敏感的業務,QUIC 可以有效的降低連接建立延遲。
究其原因一方面是TCP和TLS分層設計導致的:分層的設計需要每個邏輯層次分別建立自己的連接狀態。另一方面是TLS的握手階段復雜的密鑰協商機制導致的。要降低建連耗時,需要從這兩方面著手。
QUIC具體握手過程如下:
1.客戶端判斷本地是否已有服務器的全部配置參數(證書配置信息),如果有則直接跳轉到(5),否則繼續 ;
2.客戶端向服務器發送inchoate client hello(CHLO)消息,請求服務器傳輸配置參數;
3.服務器收到CHLO,回復rejection(REJ)消息,其中包含服務器的部分配置參數;
4.客戶端收到REJ,提取并存儲服務器配置參數,跳回到(1) ;
5.客戶端向服務器發送full client hello消息,開始正式握手,消息中包括客戶端選擇的公開數。此時客戶端根據獲取的服務器配置參數和自己選擇的公開數,可以計算出初始密鑰K1;
6.服務器收到full client hello,如果不同意連接就回復REJ,同(3);如果同意連接,根據客戶端的公開數計算出初始密鑰K1,回復server hello(SHLO)消息,SHLO用初始密鑰K1加密,并且其中包含服務器選擇的一個臨時公開數;
7.客戶端收到服務器的回復,如果是REJ則情況同(4);如果是SHLO,則嘗試用初始密鑰K1解密,提取出臨時公開數;
8.客戶端和服務器根據臨時公開數和初始密鑰K1,各自基于SHA-256算法推導出會話密鑰K2;
9.雙方更換為使用會話密鑰K2通信,初始密鑰K1此時已無用,QUIC握手過程完畢。之后會話密鑰K2更新的流程與以上過程類似,只是數據包中的某些字段略有不同。
圖2-3 QUIC 0-RTT 握手
2.3 可自定義的擁塞控制
Quic使用可插拔的擁塞控制,相較于TCP,它能提供更豐富的擁塞控制信息。比如對于每一個包,不管是原始包還是重傳包,都帶有一個新的序列號(seq),這使得Quic能夠區分ACK是重傳包還是原始包,從而避免了TCP重傳模糊的問題。Quic同時還帶有收到數據包與發出ACK之間的時延信息。這些信息能夠幫助更精確的計算RTT。此外,Quic的ACK Frame 支持256個NACK 區間,相比于TCP的SACK(Selective Acknowledgment)更彈性化,更豐富的信息會讓client和server 哪些包已經被對方收到。
QUIC 的傳輸控制不再依賴內核的擁塞控制算法,而是實現在應用層上,這意味著我們根據不同的業務場景,實現和配置不同的擁塞控制算法以及參數。GOOGLE 提出的 BBR 擁塞控制算法與 CUBIC 是思路完全不一樣的算法,在弱網和一定丟包場景,BBR 比 CUBIC 更不敏感,性能也更好。在 QUIC 下我們可以根據業務隨意指定擁塞控制算法和參數,甚至同一個業務的不同連接也可以使用不同的擁塞控制算法。
圖2-4 BBR擁塞弱網下算法效果對比
2.4 無隊頭阻塞
2.4.1 TCP的隊頭阻塞問題
雖然 HTTP2 實現了多路復用,但是因為其基于面向字節流的 TCP,因此一旦丟包,將會影響多路復用下的所有請求流。QUIC 基于 UDP,在設計上就解決了隊頭阻塞問題。
TCP 隊頭阻塞的主要原因是數據包超時確認或丟失阻塞了當前窗口向右滑動,我們最容易想到的解決隊頭阻塞的方案是不讓超時確認或丟失的數據包將當前窗口阻塞在原地。QUIC也正是采用上述方案來解決TCP 隊頭阻塞問題的。
TCP 為了保證可靠性,使用了基于字節序號的 Sequence Number 及 Ack 來確認消息的有序到達。
圖2-5 HTTP2隊頭阻塞
如上圖,應用層可以順利讀取stream1中的內容,但由于stream2中的第三個segment發生了丟包,TCP 為了保證數據的可靠性,需要發送端重傳第 3 個 segment 才能通知應用層讀取接下去的數據。所以即使stream3 stream4的內容已順利抵達,應用層仍然無法讀取,只能等待stream2中丟失的包進行重傳。
在弱網環境下,HTTP2的隊頭阻塞問題在用戶體驗上極為糟糕。
2.4.2 QUIC的無隊頭阻塞解決方案
QUIC 同樣是一個可靠的協議,它使用 Packet Number 代替了 TCP 的 Sequence Number,并且每個 Packet Number 都嚴格遞增,也就是說就算 Packet N 丟失了,重傳的 Packet N 的 Packet Number 已經不是 N,而是一個比 N 大的值,比如Packet N+M。
QUIC 使用的Packet Number 單調遞增的設計,可以讓數據包不再像TCP 那樣必須有序確認,QUIC 支持亂序確認,當數據包Packet N 丟失后,只要有新的已接收數據包確認,當前窗口就會繼續向右滑動。待發送端獲知數據包Packet N 丟失后,會將需要重傳的數據包放到待發送隊列,重新編號比如數據包Packet N+M 后重新發送給接收端,對重傳數據包的處理跟發送新的數據包類似,這樣就不會因為丟包重傳將當前窗口阻塞在原地,從而解決了隊頭阻塞問題。那么,既然重傳數據包的Packet N+M 與丟失數據包的Packet N 編號并不一致,我們怎么確定這兩個數據包的內容一樣呢?
QUIC使用Stream ID 來標識當前數據流屬于哪個資源請求,這同時也是數據包多路復用傳輸到接收端后能正常組裝的依據。重傳的數據包Packet N+M 和丟失的數據包Packet N 單靠Stream ID 的比對一致仍然不能判斷兩個數據包內容一致,還需要再新增一個字段Stream Offset,標識當前數據包在當前Stream ID 中的字節偏移量。
有了Stream Offset 字段信息,屬于同一個Stream ID 的數據包也可以亂序傳輸了(HTTP/2 中僅靠Stream ID 標識,要求同屬于一個Stream ID 的數據幀必須有序傳輸),通過兩個數據包的Stream ID 與 Stream Offset 都一致,就說明這兩個數據包的內容一致。
3. QUIC協議組成
QUIC 的 packet 除了個別報文比如 PUBLIC_RESET 和 CHLO,所有報文頭部都是經過認證的,報文 Body 都是經過加密的。這樣只要對 QUIC 報文任何修改,接收端都能夠及時發現,有效地降低了安全風險。
如圖3-1所示,紅色部分是 Stream Frame 的報文頭部,有認證。綠色部分是報文內容,全部經過加密。
圖3-1 QUIC的協議組成
- Flags:用于表示Connection ID長度、Packet Number長度等信息;
- Connection ID:客戶端隨機選擇的最大長度為64位的無符號整數。但是,長度可以協商;
- QUIC Version:QUIC協議的版本號,32位的可選字段。如果Public Flag & FLAG_VERSION != 0,這個字段必填。客戶端設置Public Flag中的Bit0為1,并且填寫期望的版本號。如果客戶端期望的版本號服務端不支持,服務端設置Public Flag中的Bit0為1,并且在該字段中列出服務端支持的協議版本(0或者多個),并且該字段后不能有任何報文;
- Packet Number:長度取決于Public Flag中Bit4及Bit5兩位的值,最大長度6字節。發送端在每個普通報文中設置Packet Number。發送端發送的第一個包的序列號是1,隨后的數據包中的序列號的都大于前一個包中的序列號;
- Stream ID:用于標識當前數據流屬于哪個資源請求;
- Offset:標識當前數據包在當前Stream ID 中的字節偏移量;
QUIC報文的大小需要滿足路徑MTU的大小以避免被分片。當前QUIC在IPV6下的最大報文長度為1350,IPV4下的最大報文長度為1370。
4. 結語
QUIC具有眾多優點,它融合了UDP協議的速度、性能與TCP的安全與可靠,同時也解決了HTTP1、HTTP1.1、HTTP2中引入的一些缺點,大大優化了互聯網傳輸體驗。
騰訊云CDN也緊跟技術浪潮,于今年年初迭代中加入了QUIC的功能支持,目前正在內測當中。掃碼可以申請內測資格&查看文檔指引
申請內測
參考資料
[1] QUIC 在 Facebook 是如何部署的?
[2] STGW 下一代互聯網標準傳輸協議QUIC大規模運營之路
[3] QUIC 0-RTT實現簡析及一種分布式的0-RTT實現方案
[4] 科普:QUIC協議原理分析
[5] TCP BBR - Exploring TCP congestion control
[6] 淺談QUIC協議原理與性能分析及部署方案
[7] QUIC 是如何解決TCP 性能瓶頸的?
[8] QUIC協議和TCP/UDP協議
[9] QUIC的那些事 | 包類型及格式
總結
以上是生活随笔為你收集整理的QUIC协议原理详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java编写四则运算代码_java编写四
- 下一篇: 我的世界天空之城服务器位置,我的世界天空