英文原文鏈接:QUIC wire specification
QUIC概述
本節我們主要介紹QUIC的關鍵功能和優點。QUIC功能上等於TCP+TLS+HTTP/2,但是基於UDP傳輸的。QUIC優於TCP+TLS+HTTP/2的關鍵點有:
- connect連接建立的低延時
- 靈活的擁塞控制
- 無頭部阻塞的多路複用(TCP是有頭部阻塞的)
- 對頭部和負載進行認證和加密
- 流和連接的流控
- 連接遷移
connection連接低延時
QUIC把加密和傳輸的握手合併,降低了安全連接建立的通信來回次數。QUIC的連接建立過程是0-RTT,也就是說大部分的QUIC連接,數據能立馬發送二不用等待服務器的返回,相比之下TCP+TLS的1-3次握手後才能通信。
QUIC提供一個特定的流(streamid=1)來進行握手,本文不詳細描述握手協議。如果想要連接握手協議,可以訪問QUIC Crypto Handshake。當前的QUIC握手未來會被TLS1.3代替。
靈活的擁塞控制
QUIC比TCP有可插拔的擁塞控制和豐富的信令,相對於TCP,這些新信令能爲QUIC提供很多的信息去做擁塞控制算法。當前,默認的擁塞控制是應用TCP Cubic;我們將會經歷更多的可選的擁塞控制方式。
舉例,每個quic報文,無論是源報文還是重傳報文,都攜帶一個新的sequence號。不同的sequence號幫助發送端確認ACK信息是重傳包的還是原始包的,因此避免了TCP重傳模糊的問題。QUIC ACK也肯定產生包接收和ACK發送之間的延時,因爲有遞增的sequence,也就能準確計算出RTT。
最後,QUIC的ACK報文支持256個ack,所以QUIC的伸縮性強於TCP(用的SACK),當亂序和丟失發生就能發送更多的字節。客戶端和服務端都有更精確的哪些報文已經收到。
數據流與連接的流控
QUIC用的是流和連接級別的流控,類似HTTP/2’s流控。QUIC流級別的流控工作如下。QUIC接受者發送字節偏移量,也就是接受者針對每條流能接受的字節數。當在某條流上的數據的收和發,接受者都會發送WINDOW_UPDATE報文增加流的字節偏移量,來允許對端發送更多的數據。
除了基於流的流控外,QUIC也提供連接級別的流控來限制聚合bffer,其控制QUIC接受者分配一個連接。連接流控的工作方式同流的流控方式一樣,只是字節的發送和接收偏移量是針對所有流的。
同TCP的接收窗口自動調節機制一樣,QUIC對流和連接的流控應用信用自動調節的機制。當接收應用比較慢時,如果需要限制發送者的速率,QUIC自動調節每個WINDOW_UPDATE報文的信用size。
多路複用
HTTP/2在TCP上有頭部阻塞的問題。應爲HTTP/2是多流複用的會造成頭部阻塞的問題,一小片TCP報文的丟失會阻塞住後續所有的分片,直到這小片的重傳能收到,完全不在乎後面的HTTP/2分片。
因爲QUIC設計初衷就是爲了多路複用,對於某一路流的丟包應該隻影響該路流。每路流能馬上被調度當收到報文,哪些沒有報文丟失的流應該能被包重組和正常繼續其應用。
認證和加密頭和數據負載
對認證和加密不太熟悉,本節跳過。
連接遷移
TCP的連接由4元組定義: 源IP,源port,目的IP,目的port。TCP最著名的問題就是連接無法容忍IP地址變化(舉例,WIFI遷移到移動網絡)或者端口的變化(如當客戶端的NAT綁定超時造成端口的變化)。當MPTCP導致TCP連接遷移,有個很大的困擾就是缺少中間件支持和缺少OS操作系統級別的支持。
QUIC連接由64bits的connectID定義,有客戶端生成個隨機數。QUIC能繼續連接,即使IP變化或NAT重綁定發生,只要在遷移過程中connectID保持不變。QUIC也提供了自動的加密認證的客戶端變化方式,因爲遷移的客戶端會繼續用同一個會話key來進行加密和認證。
在某些特定場景中,如果連接可以被IP4元組唯一定義,且該4元組不會變化,可以選擇不包含connectID進行連接。
包類型和格式
QUIC有特殊包(Special Packets)和常規包(Regular Packets)。
有兩種特殊包(Special Packets):
- 版本協商報文(Version Negotiation Packets)
- public重置報文(Public Reset Packets)
常規包(Regular Packets)只包括數據報文。
所有的QUIC報文都應該適配傳輸路徑的MTU大小,以避免IP分片。路徑MTU發現還在研究中,當前推薦IPV6最大的MTU是1350字節,IPv4是1370字節。這裏說的字節數是不包括IP頭和UDP頭的。
QUIC的公共頭(Public Packet Header)
所有QUIC報文的公共頭都是1~51字節,格式如下:
--- src
0 1 2 3 4 8
+--------+--------+--------+--------+--------+--- ---+
| Public | Connection ID (64) ... | ->
|Flags(8)| (optional) |
+--------+--------+--------+--------+--------+--- ---+
9 10 11 12
+--------+--------+--------+--------+
| QUIC Version (32) | ->
| (optional) |
+--------+--------+--------+--------+
13 14 15 16 17 18 19 20
+--------+--------+--------+--------+--------+--------+--------+--------+
| Diversification Nonce | ->
| (optional) |
+--------+--------+--------+--------+--------+--------+--------+--------+
21 22 23 24 25 26 27 28
+--------+--------+--------+--------+--------+--------+--------+--------+
| Diversification Nonce Continued | ->
| (optional) |
+--------+--------+--------+--------+--------+--------+--------+--------+
29 30 31 32 33 34 35 36
+--------+--------+--------+--------+--------+--------+--------+--------+
| Diversification Nonce Continued | ->
| (optional) |
+--------+--------+--------+--------+--------+--------+--------+--------+
37 38 39 40 41 42 43 44
+--------+--------+--------+--------+--------+--------+--------+--------+
| Diversification Nonce Continued | ->
| (optional) |
+--------+--------+--------+--------+--------+--------+--------+--------+
45 46 47 48 49 50
+--------+--------+--------+--------+--------+--------+
| Packet Number (8, 16, 32, or 48) |
| (variable length) |
+--------+--------+--------+--------+--------+--------+
負載會包含類型獨立的頭部字節,描述如下。
公共頭字段如下:
-
Public Flags
* 0x01 = PUBLIC_FLAG_VERSION. 這個flag的含義在於報文由服務器還是客戶端發出。當報文由客戶端發出,設置改bit意味着頭部包含有QUIC version(如下)。客戶端必須設置該bit,直到服務端返回運行的version。服務端同意客戶端的version,但服務端發送的報文中並不設置該標誌位。如果服務端發送的報文設置該bit,意味該報文是version協商報文。version的協商將在後面進行討論。
* 0x02 = PUBLIC_FLAG_RESET. 該bit位表示Public Reset packet報文。
* 0x04 表示在頭部有32字節的多元化標誌。
* 0x08 表示報文有全8字節的connect ID。該bit必須在所有報文中設置,直到有不同的值產生(舉例,客戶端可能需要connect id更少的字節)
* 0x30 這兩個字節的佔位表示packet number需要字節的數量。這兩個bit僅僅正對數據報文。對於public reset和version negotiation報文(服務端發送的),這兩個字節的佔位設置爲0。
* 0x30 表示packet number字段有6個字節的長度
* 0x20 表示packet number字段有4個字節的長度
* 0x10 表示packet number字段有2個字節的長度
* 0x00 表示packet number字段有1個字節的長度
* 0x40 保留爲多路徑用途
* 0x80 未使用,必須設置爲0 -
Connection ID:
這個是客戶端生成的64位bit的隨機數,標識連接的唯一性。因爲QUIC的連接設計初衷是即使客戶端IP遷移,連接也不中斷,IP4元組(源IP,源port,目的IP,目的port)並不需要去確定連接的唯一性。如果對於某個傳輸的方向,IP4元組能代表連接的唯一性(其實就是不可能發生IP遷移等),connect ID字段也就不需要了。 -
QUIC Version:
32位表示QUIC協議的版本。該字段僅僅當public flag設置了FLAG_VERSION後纔有(i.e public_flags & FLAG_VERSION !=0)。客戶端設置這個flag後,且必須包含一個客戶端推薦的quic version,包含任意數據(符合這個版本的)。服務器設置這個flag,僅當客戶端推薦的quic version不支持,服務端返回一個列表包含可接受的quic version,但是不必後續帶有數據。版本字段例子,"Q025"版本,"Q"在第9個字節,"0"在第10個字節,依次類推。(文檔後有版本列表) -
Packet Number:
packet number的長度基於FLAG_BYTE_SEQUENCE_NUMBER的flag設置在public flag。每一個常規報文regular packet(也就是非public reset和version negotiation報文)都需要被髮送方設置packet number。第一個被髮送的報文的packet number應該設置成1,後續的報文的packet number應該+1遞增。
packet number的64位被放在加密的內容中;因此,QUIC的一方不能發送報文,其packet number不在64bits內。如果QUIC的一方發送的packet number是2^64-1,報文產生CONNECTION_CLOSE報文,錯誤碼是QUIC_SEQUENCE_NUMBER_LIMIT_REACHED,並且不會再發送其他的報文。
大部分情況packet number的48bits長度的傳輸,爲了接收端能清晰的對packet number進行組包,QUIC發送端不應該發送packet number大於2^(bitlength-2)。因此48bits長度的packet number不應該大於(2^46)。
任何被截斷的packet number都應該被推斷爲最接近已經收到最大packet number,其包含這個截斷的packet number。這個packet number的傳輸比例與推斷中的地位bits對應。
Public Flag的處理流程如下:
--- src
Check the public flags in public header
|
|
V
+--------------+
| Public Reset | YES
| flag set? |---------------> Public Reset Packet
+--------------+
|
| NO
V
+------------+ +-------------+
| Version | YES | Packet sent | YES
| flag set? |--------->| by server? |--------> Version Negotiation
+------------+ +-------------+ Packet
| |
| NO | NO
V V
Regular Packet Regular Packet with
QUIC Version present in header
---
Special Packets
Version Negotiation Packet
version協商報文僅僅由服務端發送。version協商報文由8bit的public flag和64bit的connect ID。public flag必須設置PUBLIC_FLAG_VERSION,和64位bit的connect ID。報文後續是一個服務器支持version的信息列表,列表每項是4byte的version字段:
--- src
0 1 2 3 4 5 6 7 8
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| Public | Connection ID (64) | ->
|Flags(8)| |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
9 10 11 12 13 14 15 16 17
+--------+--------+--------+--------+--------+--------+--------+--------+---...--+
| 1st QUIC version supported | 2nd QUIC version supported | ...
| by server (32) | by server (32) |
+--------+--------+--------+--------+--------+--------+--------+--------+---...--+
---
Public Reset Packet
Public Reset報文由8bit的public flag和64bits的connect ID。public flag必須設置PUBLIC_FLAG_RESET,和64bit的connect ID。如果這是一個帶tag PRST加密的握手消息,報文的剩餘部分是被加密的(見 [QUIC-CRYPTO]):
--- src
0 1 2 3 4 8
+--------+--------+--------+--------+--------+-- --+
| Public | Connection ID (64) ... | ->
|Flags(8)| |
+--------+--------+--------+--------+--------+-- --+
9 10 11 12 13 14
+--------+--------+--------+--------+--------+--------+---
| Quic Tag (32) | Tag value map ... ->
| (PRST) | (variable length)
+--------+--------+--------+--------+--------+--------+---
---
Tag value map: 這個Tag value map有一下tar-values信息:
- RNON (public reset nonce proof) - a 64-bit unsigned integer. Mandatory.
- RSEQ (rejected packet number) - a 64-bit packet number. Mandatory.
- CADR (client address) - the observed client IP address and port number. 這當前只是用於調試目的,所以是可選的。
常規報文(Regular Packets)
常規報文加上認證和加密的。Public header是加了認證信息,但是並未加密,常規報文的剩餘部分是被加密的。在public header後面,常規報文包含AEAD(authenticated encryption and associated data,認證和被加密的數據)數據。這些數據應該按順序被解密。解密後,明文應該由按順序的frame組成。
數據報文(Frame Packet)
Frame報文的負載由一系列的type前綴的frames組成。報文type的格式後面會描述,總體的格式如下:
--- src
+--------+---...---+--------+---...---+
| Type | Payload | Type | Payload |
+--------+---...---+--------+---...---+
---
QUIC連接的生命週期(Life of a QUIC Connection)
連接建立(Connection Establishment)
QUIC客戶端是一方發起連接的。QUIC的連接由version協商和加密、傳輸握手混合進行,以此降低連接的延時。我們下面先介紹version協商。
每個客戶端發向服務端的初始化報文必須設置version flag,必須定義將要使用version。每個客戶端發送的報文都不許帶version flag,直到收到服務端返回一個不帶version flag的報文。在服務端收到客戶端第一個不帶version flag的報文後,服務端就必須丟棄所有再收到version flag的報文。
當服務端收到一個新的connect ID,它將比較客戶端的版本自己是否支持。如果客戶端的版本自己自持,服務端將在整個連接週期內用該版本。然後,所有服務端的發送報文都應該清除version flag該標誌位。
如果客戶端的版本不被服務器接收,1個RTT的延時就會觸發。服務端將發送Version協商報文給客戶端。這個報文的version flag會被設置,並且會包含服務端支持的version列表。
當客戶端收到version協商報文,會選擇其中一個version並用這個version重發所有報文。這些報文必須也設置version flag和包含該version。最終,客戶端接收到從服務器來的第一個常規報文開始,表示version協商的結束,客戶端之後發送的所有報文都應該去使能version flag。
爲了避免downgrade攻擊,客戶端定義在第一個報文的version和服務器支持的version列表都必須包含在加密的handleshake數據中。客戶端需要確認在handshake中的version列表和version協商列表進行對比,得到相同一致的。服務端需要確認客戶端發來的handshake中的version是否實際支持。
連接建立的後續部分將在handshake文檔中介紹[QUIC-CRYPTO]。加密的handshake被分配固定的stream ID 1。
在連接建立過程中,handshake必須協商各種傳輸參數。當前已經定義的傳輸參數再本文後面有介紹。
數據傳輸(Data Transfer)
QUIC應用連接可靠性,擁塞控制和流控。QUIC流控基本上同HTTP/2的流控一樣。QUIC可靠性和擁塞控制在相關的文檔中描述。QUIC連接用唯一的packet sequence數字字段,對整個連接中的擁塞空着和丟包重傳都一致。
在QUIC連接中傳輸的所有數據,包括加密的handshake,都是在stream中作爲數據傳輸,ACK返回QUIC報文除外。
本節概念上對一個QUIC連接中數據傳輸中流的使用進行介紹。各個各樣的報文會在Frame Type and Formats節進行介紹。
QUIC流的生命週期(Life of a QUIC Stream)
QUIC流是雙向發送的數據被分配到流分配包中的很多獨立序列。stream能被客戶單或服務器創建,能與其他的流一起併發發送數據,並且能停止發送。QUIC流的生命週期模型與HTTP/2的非常相似。[RFC7540]
(QUIC流的HTTP/2用法在本文檔後面進行詳細描述)
針對指定流發送一個流報文,就隱性的創建一個stream。爲了避免stream ID衝突,如果是服務端發起stream的話,stream-ID必須是偶數;客戶端發起stream的話,stream-ID必須是單數。0不是一個有效的stream-ID。Stream 1給加密的handshake作爲第一個客戶端端發起stream使用。當應用HTTP/2 over QUIC時,Stream 3爲發送所有其他流的壓縮頭使用,從而確保可靠有序的發送和頭部處理。
當新流被創建時,連接雙方的stream ID應該連續的增長。舉例,Stream2應該在Stream 3後創建(stream 3是客戶端,stream2是服務端),但是stream 7肯定不能再stream 9後才創建。對端可能接受的流是無序的。舉例,如果在服務端接受packet9包含stream7前,接受到packet10包含stream9,服務器必須能從容處理這樣的亂序情況。
如果一方收到一個stream包但並不想接收它,它可以立即返回一個RST_STREAM報文(下面會介紹)。注意,雖然發起方已經在該stream中發送數據,但這些數據會被丟棄。
一旦流被創建,它就能發送和接收數據。也就是說直到流在某方向結束前,這條流上的報文都能持續的被髮送。
每個QUIC端都能正常終結stream。有3種終結stream的方法:
- 正常終結(Normal termination): 因爲流是雙向的,所以流能是單方向關閉或全關閉。當一方發送的報文帶有FIN標誌位,就代表單方向關閉。FIN標誌着發送FIN的這一方不會再有數據要發送。當QUIC的一方發送並接受了FIN,這方也就被認爲完全關閉了。FIN應該放在最後一個用戶數據的報文中,但是FIT也能在最後一個用戶數據報文後作爲空報文發送(有點浪費)
- 突然結束(Abrupt termination):客戶端和服務器能發送RST_STREAM在任何時候。RST_STREAM報文包含error錯誤碼解釋失敗的原因(錯誤碼列表在本文最後)。當RST_STREAM是流發起方發送,表明有錯誤發生且不會有更多的數據在該流發送。當RST_STREAM是接受者發送,流的發送方在接收到RST_STREAM報文後,應該立即停止任何數據在該流上的發送。流的接收方也應該意識到有個時間間隔在發送方已經發送的數據,和發送方接收到接收方發來的RST_STREAM報文。爲了保證連接級別的流控能正確的被計數,即使RST_STREAM報文已經收到,發送方也需要確認:在該流上的FIN和所有數據字節對端已經收到;或對端收到RST_STREAM。也就是說,RST_STREAM的發送端需要繼續用正確的WINDOW_UPDATEs響應這條流上的數據,保證發送方不會有流控阻塞,保證其完成FIN的發送。
- 當連接斷開,流肯定也斷開,在下面一節會詳細介紹連接斷開。
連接斷開(Connection Termination)
連接保持打開狀態直到變成空閒狀態一段設定的時間。當服務端要斷開一個空閒連接,它不需要通知客戶端,這回導致移動設備的喚醒信號。QUIC連接一段建立,有兩種方式可以結束:
- 顯式關閉(Explicit Shutdown): 一方發送CONNECTION_CLOSE報文給另外一方表明連接開始中斷。一方也可以發送GOAWAY報文給另外一方,而不是用CONNECTION_CLOSE,GOAWAY表明連接很快將關閉。GOAWAY發送到對端後,對端繼續對所有活躍的報文進行處理,但是GOAWAY的發送方不再發送新的報文,也不在接收任何新的數據報文。對活躍流的結束,也可以發送CONNECTION_CLOSE。如果當爲結束的流是活躍的(沒有FIN或RST_STREAM報文被髮送或接收),一方發送CONNECTION_CLOSE報文,那麼對端就認爲流未完成,已經被非正常結束。
- 隱式關閉(Implicit Shutdown):默認的QUIC連接的空閒超時是30秒,在連接協商中有個參數"ICSL"定義。最大值是10分鐘。如果在空閒超時時間內沒有任何網絡活躍,連接會關閉。默認情況下CONNECTION_CLOSE將發送。當發送顯示關閉太浪費,如移動網絡會喚醒手機信號,"靜音"關閉的選項被使能。
QUIC的一方在任何連接獲取的時候,也能通過發送PUBLIC_RESET來終結連接。PUBLIC_RESET的PUBLIC_RESET是等價於TCP的RST。
報文類型和格式(Frame Types and Formats)
QUIC流是以報文方式存在,報文都有報文類型(frame type),類型有完全獨立的解釋,後面跟隨fream header字段。所有的frame都被包含在QUIC報文中,沒有哪個frame會越過QUIC報文的邊界。
報文類型(Frame Types)
對於報文類型有兩種解釋,也就由此定義兩種報文類型:
- 特殊報文(Special Frame Types)
特殊報文包含frame type和flag信息在frame type的字段中 - 常規報文(Regular Frame Types)
常規報文只包含frame type在frame type的字段中
當前定義的特殊報文類型(Special Frame Types):
--- src
+------------------+-----------------------------+
| Type-field value | Control Frame-type |
+------------------+-----------------------------+
| 1fdooossB | STREAM |
| 01ntllmmB | ACK |
| 001xxxxxB | CONGESTION_FEEDBACK |
+------------------+-----------------------------+
---
當前定義的常規報文類型(Regular Frame Types):
--- src
+------------------+-----------------------------+
| Type-field value | Control Frame-type |
+------------------+-----------------------------+
| 00000000B (0x00) | PADDING |
| 00000001B (0x01) | RST_STREAM |
| 00000010B (0x02) | CONNECTION_CLOSE |
| 00000011B (0x03) | GOAWAY |
| 00000100B (0x04) | WINDOW_UPDATE |
| 00000101B (0x05) | BLOCKED |
| 00000110B (0x06) | STOP_WAITING |
| 00000111B (0x07) | PING |
+------------------+-----------------------------+
---
流報文(STREAM Frame)
流報文被隱式的創建流併發送報文,格式如下:
--- src
0 1 … SLEN
+--------+--------+--------+--------+--------+
|Type (8)| Stream ID (8, 16, 24, or 32 bits) |
| | (Variable length SLEN bytes) |
+--------+--------+--------+--------+--------+
SLEN+1 SLEN+2 … SLEN+OLEN
+--------+--------+--------+--------+--------+--------+--------+--------+
| Offset (0, 16, 24, 32, 40, 48, 56, or 64 bits) (variable length) |
| (Variable length: OLEN bytes) |
+--------+--------+--------+--------+--------+--------+--------+--------+
SLEN+OLEN+1 SLEN+OLEN+2
+-------------+-------------+
| Data length (0 or 16 bits)|
| Optional(maybe 0 bytes) |
+------------+--------------+
---
流報文頭部的各個字段描述如下:
- Frame Type: 報文類型是8bit大小,包含各種flag信息(1fdooossB)
* 最左邊的bit設置1,表示這是個流報文。The leftmost bit must be set to 1 indicating that this is a STREAM frame.
* f標誌位是FIN表示,當設置爲1,表示發送端完成該流的發送,並希望半雙工關閉(後續詳細介紹)
* d表示Data length會在STREAM頭部存在,如果設置爲0,表示流報文的長度會是一直到報文末尾。
* ooo表示offset字段的長度,000–111分別表示0, 16, 24, 32, 40, 48, 56, or 64 bits長度。
* ss表示Stream ID的長度,00–11分別表示8, 16, 24, or 32 bits長度。 - Stream ID: 可變長度的無符號整型ID,標識唯一的流。
- Offset: 可變長度的無符號整型,表示流數據塊開始的偏移位置。
- Data length: (可選)16bit長的無符號整型,標識報文中的數據長度。如果不需要此字段,表示offset後到末尾的所有字節都是數據,後面沒有padding數據。
一個流報文肯定要麼有非0的數據長度,要麼數據長度爲0但是FIN標誌位被設置1。