本篇文章帶大家了解一下QUIC協(xié)議,并以QUIC協(xié)議為例,來(lái)聊聊如何學(xué)習(xí)網(wǎng)絡(luò)協(xié)議,希望對(duì)大家有所幫助!
在之前發(fā)布的關(guān)于 s2n-quic 的文章中,有讀者問(wèn)我如何學(xué)習(xí)像 QUIC 這樣的網(wǎng)絡(luò)協(xié)議。對(duì)于大部分互聯(lián)網(wǎng)從業(yè)者來(lái)說(shuō),雖然大家每天都在跟網(wǎng)絡(luò)打交道,但很少有人會(huì)(需要)關(guān)心 HTTP 之下的網(wǎng)絡(luò)協(xié)議的細(xì)節(jié),大部分時(shí)候,了解個(gè)大概,知道如何使用就可以了。如果你對(duì) QUIC 一點(diǎn)概念都沒(méi)有,那么,下面這個(gè)圖能幫助你很好地了解 QUIC 在 HTTP/3 生態(tài)中的地位:
那么,如果你就是要詳盡地了解一下 QUIC 的知識(shí),該如何入手呢?
作為一個(gè)曾經(jīng)的網(wǎng)絡(luò)協(xié)議和網(wǎng)絡(luò)設(shè)備的開(kāi)發(fā)者,我自己的心得是:從 RFC 入手,輔以 wireshark 抓包,來(lái)快速掌握目標(biāo)協(xié)議。
對(duì)于 QUIC 而言,我們首先需要閱讀的是 RFC9000。協(xié)議的閱讀是非??菰锏氖虑?,需要一定的耐心,如果英文不太好,可以用 google translate 將其翻譯成中文,快速瀏覽一番(泛讀)。第一遍閱讀主要了解里面的主要概念,以及主要流程。
之后,我們就可以撰寫(xiě)使用 QUIC 協(xié)議的程序,然后通過(guò) wireshark 抓包,通過(guò)研究實(shí)際的報(bào)文,對(duì)比 RFC 協(xié)議中的內(nèi)容(精讀),來(lái)更深入地理解協(xié)議的本質(zhì)。
我們還是以上一篇文章中的代碼為基礎(chǔ),構(gòu)建 echo 客戶端和服務(wù)器。為方便大家閱讀,我把代碼也貼上來(lái)。感興趣的同學(xué)可以自行 clone 我的 repo,并運(yùn)行 client/server 代碼。
客戶端代碼(見(jiàn) github: tyrchen/rust-training/live_coding/quic-test/examples/client.rs):
服務(wù)端代碼(見(jiàn) github: tyrchen/rust-training/live_coding/quic-test/examples/server.rs):
這兩段代碼構(gòu)建了一個(gè)最簡(jiǎn)單的 echo server。我們可以使用 wireshark 監(jiān)聽(tīng)本地 loopback 接口下的 UDP 包,進(jìn)行抓包。要注意的是,對(duì)于 QUIC 這樣使用了 TLS 協(xié)議的流量,即便抓到了包,可能只有頭幾個(gè)包可讀,后續(xù)的包都是加密內(nèi)容,無(wú)法閱讀。因此,我們?cè)跇?gòu)建 client/server 時(shí),需要想辦法把服務(wù)器和客戶端之間協(xié)商出來(lái)的 session key 抓取下來(lái),供 wireshark 解密使用。一般 SSL/TLS 庫(kù)都會(huì)提供這個(gè)功能。比如對(duì)于 Rustls,我們可以在 tls config 中使用 key_log。如果你仔細(xì)看上面 server 的代碼,會(huì)看到這句:
let config = Builder::new() .with_certificate(CERT_PEM, KEY_PEM)? .with_key_logging()? # 使能 keylogging .build()?;
使用了 key_log 后,在啟動(dòng) server 的時(shí)候,我們只需要指定 SSLKEYLOGFILE 就可以了:
SSLKEYLOGFILE=session.log cargo run --example server
在抓包完成后,打開(kāi) wireshark 的 preference,選擇 TLS 協(xié)議,把 log 的路徑放進(jìn)去就可以了:
以下是一次完整的客戶端和服務(wù)器的交互的抓包,我們看到,所有 “protected payload” 都被正常顯示出來(lái)了:
因?yàn)槲覀兊?echo client 只做了最簡(jiǎn)單的動(dòng)作(只開(kāi)了一個(gè) bidirectional stream),所以通過(guò)這個(gè)抓包,我們重點(diǎn)可以研究 QUIC 協(xié)議建立連接的過(guò)程。
客戶端發(fā)送的首包
我們看客戶端發(fā)的第一個(gè)報(bào)文:
這個(gè)報(bào)文包含了非常豐富的信息。首先,和 TCP 握手不同的是,QUIC 的首包非常大,有 1200 字節(jié)之多(協(xié)議要求 UDP payload at least 1200 bytes),包含 QUIC 頭,一個(gè) 255 字節(jié)的 CRYPTO frame,以及 890 字節(jié) PADDING frame。從 header 可以看到,這個(gè) QUIC 包的類型是 Initial。
QUIC 報(bào)文類型
對(duì)于 QUIC 包來(lái)說(shuō),Header 是明文,之后的所有 frame payload 都是密文(除了頭幾個(gè)包)。我們看到這個(gè)首包是一個(gè) Long Header 報(bào)文,在 RFC9000 的 17.2 節(jié)中,定義了 Long Header Packet:
Long Header Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2), Type-Specific Bits (4), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Type-Specific Payload (..), }
感興趣的可以自行去閱讀 RFC 相應(yīng)的章節(jié)。對(duì)于 Long Header 報(bào)文,有如下幾種類型:
既然有 Long Header packet,那么就有 Short Header packet,Short Header packet 目前的版本只有一種:
1-RTT Packet { Header Form (1) = 0, Fixed Bit (1) = 1, Spin Bit (1), Reserved Bits (2), Key Phase (1), Packet Number Length (2), Destination Connection ID (0..160), Packet Number (8..32), Packet Payload (8..), }
為什么需要 connection id?
在我們捕獲的這個(gè)報(bào)文頭中,我們看到有 Source Connection ID(SCID)和 Destination Connection ID(DCID)這個(gè)新的概念。你也許會(huì)好奇:QUIC 不是基于 UDP/IP 的協(xié)議么?底層的協(xié)議已經(jīng)有五元組(src ip / src port / dst ip / dst port / protocol)來(lái)描述一個(gè)連接(connection),為什么還需要 connection id 這樣一個(gè)新的概念?
這是為了適應(yīng)越來(lái)越多的移動(dòng)場(chǎng)景。有了 QUIC 層自己的 connection id,底層網(wǎng)絡(luò)(UDP/IP)的變化,并不會(huì)引發(fā) QUIC 連接的中斷,也就是說(shuō),你從家里開(kāi)車出門,即便手機(jī)的網(wǎng)絡(luò)從 WIFI(固網(wǎng)運(yùn)營(yíng)商分配給你的 IP)切換到蜂窩網(wǎng)絡(luò)(移動(dòng)運(yùn)營(yíng)商分配給你的 IP),整個(gè) UDP/IP 網(wǎng)絡(luò)變化了,但你的 QUIC 應(yīng)用只會(huì)感受到細(xì)微的延遲,并不需要重新建立 QUIC 連接。
從這個(gè)使用場(chǎng)景來(lái)看,QUIC 底層使用無(wú)連接的 UDP 是非常必要的。
首包中就包含了 TLS hello?
我們接下來(lái)看看 CRYPTO frame:
可以看到,QUIC 在建立連接的首包就把 TLS Client Hello 囊括在 CRYPTO frame 中。并且使用的 TLS版本是 1.3。在 Client Hello 的 extension 中,QUIC 協(xié)議使用了一個(gè) quic_transport_parameters 的 extension,用來(lái)協(xié)商 QUIC 自己的一些初始值,比如支持多少個(gè) stream,這個(gè)連接中可以最多使用多少個(gè) active connection id 等等。
QUIC 支持哪些 frame?
現(xiàn)在我們已經(jīng)見(jiàn)到了兩種 Frame:CRYPTO 和 PADDING。下表中羅列了 QUIC 所有支持的 frame:
服務(wù)器的回包
我們來(lái)看 server 的回包:
這里有一些新東西。首先,一個(gè) UDP 包內(nèi)部可以包含若干個(gè) QUIC payload,我們看到 server 回復(fù)了一個(gè) QUIC Initial 報(bào)文和一個(gè) QUIC Handshake 報(bào)文。在 Initial 報(bào)文中,我們看到了一個(gè) ACK frame,可見(jiàn) QUIC 雖然構(gòu)建于 UDP,但在 QUIC 協(xié)議內(nèi)部構(gòu)建了類似 TCP 的確認(rèn)機(jī)制。
我們之前看到,在 Initial 報(bào)文的 CRYPTO frame 中,客戶端發(fā)送了 TLS Client Hello,同樣的,服務(wù)器在 Initial 報(bào)文的 CRYPTO frame 中發(fā)送了 TLS Server Hello。這個(gè)我們就略過(guò)不看。
在 Handshake 報(bào)文中:
服務(wù)器發(fā)送了自己的證書(shū),并結(jié)束了 TLS handshake。
客戶端結(jié)束 Handshake
我們?cè)倏吹谌齻€(gè)包,客戶端發(fā)送給服務(wù)器結(jié)束 TLS 握手:
這個(gè)包依舊包含兩個(gè) QUIC 報(bào)文,其中第一個(gè)就是一個(gè) ACK frame,來(lái)確認(rèn)收到了服務(wù)器的 Server Hello 那個(gè) QUIC 報(bào)文;第二個(gè)包含一個(gè) ACK frame,確認(rèn)服務(wù)器的 Handshake,隨后有一個(gè) CRYPTO frame 結(jié)束客戶端的 TLS handshake。
TLS 握手結(jié)束之后,客戶端和服務(wù)器就開(kāi)始應(yīng)用層的數(shù)據(jù)交換,此刻,所有數(shù)據(jù)都是加密的。
客戶端發(fā)送一個(gè) “hello” 文本
在我們的 echo client/server 代碼中,客戶端連接到服務(wù)器后,就可以等待用戶在 stdin 的輸入,然后將其發(fā)送到服務(wù)器。服務(wù)器收到客戶端數(shù)據(jù),原封不動(dòng)發(fā)回,客戶端再將其顯示到 stdout 上。在這個(gè)過(guò)程的前后,客戶端和服務(wù)器間有一些用于連接管理的 QUIC 報(bào)文,比如 PING。我們就略過(guò),只看發(fā)送應(yīng)用層數(shù)據(jù)的報(bào)文。下圖是客戶端發(fā)送的包含 “hello” 文本的報(bào)文:
可以看到,這里 QUIC 報(bào)文是個(gè) Short Header packet,除了 ACK frame 外,它還有一個(gè) STREAM frame。這個(gè) stream 的 stream ID 最低兩位是 00,代表是客戶端發(fā)起的,雙向的 stream。由于使用了兩位來(lái)表述類型,所以 QUIC 的 stream 有如下類型:
我們看 STREAM frame 的 length(6) 和 Data(68 65 6c 6c 6f 0a)。Data 里的內(nèi)容如果用 ASCII 表示,正好是 “hello<LF>”,它的長(zhǎng)度是 6 個(gè)字節(jié)。
服務(wù)器回復(fù) “hello” 文本
最后是服務(wù)器 echo back:
這個(gè)和上面的報(bào)文如出一轍,就不解釋了。
賢者時(shí)刻
相信通過(guò)上面對(duì)照著 wireshark 抓包進(jìn)行的 QUIC 簡(jiǎn)介,能讓你對(duì) QUIC 協(xié)議有一個(gè)初步的認(rèn)識(shí)。上篇文章,我們說(shuō) QUIC 支持多路復(fù)用,并且解決了傳輸層隊(duì)頭阻塞的問(wèn)題。通過(guò)這篇文章的介紹,你能回答以下兩個(gè)問(wèn)題么?
-
QUIC 通過(guò)哪個(gè) frame 類型來(lái)做多路復(fù)用的?
-
QUIC 如何解決傳輸層隊(duì)頭阻塞的?