十分鐘講完 QUIC 協(xié)議,你懂了嗎?
這里先來(lái)回顧一下 HTTP 的發(fā)展過(guò)程。首先,我們想要一種能夠在網(wǎng)絡(luò)上獲取文檔內(nèi)容的協(xié)議,通過(guò)一種叫做 GET 請(qǐng)求的方式進(jìn)行獲取,后來(lái)這種 GET 請(qǐng)求被寫入了官方文檔,HTTP/1.0 應(yīng)運(yùn)而生。HTTP/1.0 的出現(xiàn)可以說(shuō)是顛覆性的,它里面涵蓋的一些標(biāo)準(zhǔn)我們目前還仍在使用,例如 HTTP header,協(xié)議號(hào)的概念,不過(guò),這個(gè)版本的 HTTP 還有一些明顯的缺陷,比如它不支持持久性連接,每次請(qǐng)求響應(yīng)后,都需要斷開連接,這樣效率很差。沒過(guò)了多久,制定了 HTTP/1.1 標(biāo)準(zhǔn),這個(gè)標(biāo)準(zhǔn)是互聯(lián)網(wǎng)上使用最頻繁的一個(gè)標(biāo)準(zhǔn),HTTP/1.1 解決了之前不支持持久性連接的缺陷,而且 HTTP/1.1 還增加了緩存和控制模塊。
但是,即便 HTTP/1.1 解決了一部分連接性能問(wèn)題,它的效率仍不是很高,而且 HTTP 還有一個(gè)隊(duì)頭阻塞問(wèn)題(關(guān)于隊(duì)頭阻塞我已經(jīng)在 HTTP2.0 的那篇文章中進(jìn)行了說(shuō)明和介紹。)
假如有五個(gè)請(qǐng)求被同時(shí)發(fā)出,如果第一個(gè)請(qǐng)求沒有處理完成,就會(huì)導(dǎo)致后續(xù)的請(qǐng)求也無(wú)法得到處理,如下圖所示
如果第一個(gè)請(qǐng)求沒有被處理,那么 2 3 4 5 這四個(gè)請(qǐng)求會(huì)直接阻塞在客戶端,等到請(qǐng)求 1 被處理完畢后,才能逐個(gè)發(fā)出。網(wǎng)絡(luò)通暢的時(shí)候性能影響不大,不過(guò)一旦請(qǐng)求 1 因?yàn)槟承┰驔]有抵達(dá)服務(wù)器,或者請(qǐng)求因?yàn)榫W(wǎng)絡(luò)阻塞沒有及時(shí)返回,影響的就是所有后續(xù)請(qǐng)求,導(dǎo)致后續(xù)請(qǐng)求無(wú)限阻塞下去,問(wèn)題就變得比較嚴(yán)重了。
雖然 HTTP/1.1 使用了 pipling 的設(shè)計(jì)用于解決隊(duì)頭阻塞問(wèn)題,但是在 pipling 的設(shè)計(jì)中,每個(gè)請(qǐng)求還是按照順序先發(fā)先回,并沒有從根本上解決問(wèn)題。隨著協(xié)議的不斷更新,提出了 HTTP/2.0 。
HTTP/2.0
HTTP/2.0 解決隊(duì)頭阻塞的問(wèn)題是采用了 stream 和分幀的方式。
HTTP/2.0 會(huì)將一個(gè) TCP 連接切分成為多個(gè) stream,每個(gè) stream 都有自己的 stream id,這個(gè) stream 可以是客戶端發(fā)往服務(wù)端,也可以是服務(wù)端發(fā)往客戶端。
HTTP/2.0 還能夠?qū)⒁獋鬏數(shù)男畔⒉鸱譃閹?,并?duì)它們進(jìn)行二進(jìn)制格式編碼。也就是說(shuō),HTTP/2.0 會(huì)將 Header 頭和 Data 數(shù)據(jù)分別進(jìn)行拆分,而且拆分之后的二進(jìn)制格式位于多個(gè) stream 中。下面來(lái)看張圖。
可以看到,HTTP/2.0 通過(guò)這兩種機(jī)制,將多個(gè)請(qǐng)求分到了不同的 stream 中,然后將請(qǐng)求進(jìn)行分幀,進(jìn)行二進(jìn)制傳輸,每個(gè) stream 可以不用保證順序亂序發(fā)送,到達(dá)客戶端后,客戶端會(huì)根據(jù)每個(gè) stream 進(jìn)行重組,而且可以根據(jù)優(yōu)先級(jí)來(lái)優(yōu)先處理哪個(gè) stream。
QUIC 協(xié)議
雖然 HTTP/2.0 解決了隊(duì)頭阻塞問(wèn)題,但是每個(gè) HTTP 連接都是由 TCP 進(jìn)行連接建立和傳輸?shù)?,TCP 協(xié)議在處理包時(shí)有嚴(yán)格的順序要求。這也就是說(shuō),當(dāng)某個(gè)包切分的 stream 由于某些原因丟失后,服務(wù)器不會(huì)處理其他 stream,而會(huì)優(yōu)先等待客戶端發(fā)送丟失的 stream 。舉個(gè)例子來(lái)說(shuō),假如有一個(gè)請(qǐng)求有三個(gè) stream,其中 stream2 由于某些原因丟失了,那么 stream1 和 stream 2 的處理也會(huì)阻塞,只有收到重發(fā)的 stream2 之后,服務(wù)器才會(huì)再次進(jìn)行處理。
這就是 TCP 連接的癥結(jié)所在。
鑒于這個(gè)問(wèn)題,我們先把 TCP 放一放,先來(lái)認(rèn)識(shí)一波 QUIC 協(xié)議。
QUIC 的小寫是 quic,諧音 quick,意思就是快。它是 Google 提出來(lái)的一個(gè)基于 UDP 的傳輸協(xié)議,所以 QUIC 又被叫做快速 UDP 互聯(lián)網(wǎng)連接。
首先 QUIC 的第一個(gè)特征就是快,為什么說(shuō)它快,它到底快在哪呢?
我們大家知道,HTTP 協(xié)議在傳輸層是使用了 TCP 進(jìn)行報(bào)文傳輸,而且 HTTPS 、HTTP/2.0 還采用了 TLS 協(xié)議進(jìn)行加密,這樣就會(huì)導(dǎo)致三次握手的連接延遲:即 TCP 三次握手(一次)和 TLS 握手(兩次),如下圖所示。
對(duì)于很多短連接場(chǎng)景,這種握手延遲影響較大,而且無(wú)法消除。
相比之下,QUIC 的握手連接更快,因?yàn)樗褂昧?UDP 作為傳輸層協(xié)議,這樣能夠減少三次握手的時(shí)間延遲。而且 QUIC 的加密協(xié)議采用了 TLS 協(xié)議的最新版本 TLS 1.3,相對(duì)之前的 TLS 1.1-1.2,TLS1.3 允許客戶端無(wú)需等待 TLS 握手完成就開始發(fā)送應(yīng)用程序數(shù)據(jù)的操作,可以支持1 RTT 和 0 RTT,從而達(dá)到快速建立連接的效果。
我們上面還說(shuō)過(guò),HTTP/2.0 雖然解決了隊(duì)頭阻塞問(wèn)題,但是其建立的連接還是基于 TCP,無(wú)法解決請(qǐng)求阻塞問(wèn)題。
而 UDP 本身沒有建立連接這個(gè)概念,并且 QUIC 使用的 stream 之間是相互隔離的,不會(huì)阻塞其他 stream 數(shù)據(jù)的處理,所以使用 UDP 并不會(huì)造成隊(duì)頭阻塞。
在 TCP 中,TCP 為了保證數(shù)據(jù)的可靠性,使用了序號(hào)+確認(rèn)號(hào)機(jī)制來(lái)實(shí)現(xiàn),一旦帶有 synchronize sequence number 的包發(fā)送到服務(wù)器,服務(wù)器都會(huì)在一定時(shí)間內(nèi)進(jìn)行響應(yīng),如果過(guò)了這段時(shí)間沒有響應(yīng),客戶端就會(huì)重傳這個(gè)包,直到服務(wù)器收到數(shù)據(jù)包并作出響應(yīng)為止。
那么 TCP 是如何判斷它的重傳超時(shí)時(shí)間呢?
TCP 一般采用的是自適應(yīng)重傳算法,這個(gè)超時(shí)時(shí)間會(huì)根據(jù)往返時(shí)間 RTT 動(dòng)態(tài)調(diào)整的。每次客戶端都會(huì)使用相同的 syn 來(lái)判斷超時(shí)時(shí)間,導(dǎo)致這個(gè) RTT 的結(jié)果計(jì)算的不太準(zhǔn)確。
雖然 QUIC 沒有使用 TCP 協(xié)議,但是它也保證了可靠性,QUIC 實(shí)現(xiàn)可靠性的機(jī)制是使用了 Packet Number,這個(gè)序列號(hào)可以認(rèn)為是 synchronize sequence number 的替代者,這個(gè)序列號(hào)也是遞增的。與 syn 所不同的是,不管服務(wù)器有沒有接收到數(shù)據(jù)包,這個(gè) Packet Number 都會(huì) + 1,而 syn 是只有服務(wù)器發(fā)送 ack 響應(yīng)之后,syn 才會(huì) + 1。
比如有一個(gè) PN = 10 的數(shù)據(jù)包在發(fā)送的過(guò)程中由于某些原因遲遲沒到服務(wù)器,那么客戶端會(huì)重傳一個(gè) PN = 11 的數(shù)據(jù)包,經(jīng)過(guò)一段時(shí)間后客戶端收到 PN = 10 的響應(yīng)后再回送響應(yīng)報(bào)文,此時(shí)的 RTT 就是 PN = 10 這個(gè)數(shù)據(jù)包在網(wǎng)絡(luò)中的生存時(shí)間,這樣計(jì)算相對(duì)比較準(zhǔn)確。
雖然 QUIC 保證了數(shù)據(jù)包的可靠性,但是數(shù)據(jù)的可靠性是如何保證的呢?
QUIC 引入了一個(gè) stream offset 的概念,一個(gè) stream 可以傳輸多個(gè) stream offset,每個(gè) stream offset 其實(shí)就是一個(gè) PN 標(biāo)識(shí)的數(shù)據(jù),即使某個(gè) PN 標(biāo)識(shí)的數(shù)據(jù)丟失,PN + 1 后,它重傳的仍舊是 PN 所標(biāo)識(shí)的數(shù)據(jù),等到所有 PN 標(biāo)識(shí)的數(shù)據(jù)發(fā)送到服務(wù)器,就會(huì)進(jìn)行重組,以此來(lái)保證數(shù)據(jù)可靠性。到達(dá)服務(wù)器的 stream offset 會(huì)按照順序進(jìn)行組裝,這同時(shí)也保證了數(shù)據(jù)的順序性。
眾所周知,TCP 協(xié)議的具體實(shí)現(xiàn)是由操作系統(tǒng)內(nèi)核來(lái)完成的,應(yīng)用程序只能使用,不能對(duì)內(nèi)核進(jìn)行修改,隨著移動(dòng)端和越來(lái)越多的設(shè)備接入互聯(lián)網(wǎng),性能逐漸成為一個(gè)非常重要的衡量指標(biāo)。雖然移動(dòng)網(wǎng)絡(luò)發(fā)展非常快,但是用戶端的更新卻非常緩慢,我仍然看見有很多地區(qū)很多計(jì)算機(jī)還仍舊使用 xp 系統(tǒng),盡管它早已發(fā)展了很多年。服務(wù)端系統(tǒng)不依賴用戶升級(jí),但是由于操作系統(tǒng)升級(jí)涉及到底層軟件和運(yùn)行庫(kù)的更新,所以也比較保守和緩慢。
QUIC 協(xié)議的一個(gè)重要特點(diǎn)就是可插拔性,能夠動(dòng)態(tài)更新和升級(jí),QUIC 在應(yīng)用層實(shí)現(xiàn)了擁塞控制算法,不需要操作系統(tǒng)和內(nèi)核的支持,遇到擁塞控制算法切換時(shí),只需要在服務(wù)器重新加載一遍即可,不需要停機(jī)和重啟。
我們知道 TCP 的流量控制是通過(guò)滑動(dòng)窗口來(lái)實(shí)現(xiàn)的,如果你對(duì)滑動(dòng)窗口不太熟悉,你可以看下我寫的這篇文章。
TCP 基礎(chǔ)知識(shí)
在文章后面有提到了滑動(dòng)窗口的一些概念。
而 QUIC 也實(shí)現(xiàn)了流量控制,QUIC 的流量控制也是使用了窗口更新window_update,來(lái)告訴對(duì)端它可以接受的字節(jié)數(shù)。
TCP 協(xié)議頭部沒有經(jīng)過(guò)加密和認(rèn)證,所以在傳輸?shù)倪^(guò)程中很可能被篡改,與之不同的是,QUIC 中的報(bào)文頭部都是經(jīng)過(guò)認(rèn)證,報(bào)文也經(jīng)過(guò)加密處理。這樣只要對(duì) QUIC 的報(bào)文有任何修改,接收端都能夠及時(shí)發(fā)現(xiàn),保證了安全性。
總的來(lái)說(shuō),QUIC 相比于 HTTP/2.0 來(lái)說(shuō),具有下面這些優(yōu)勢(shì)
- 使用 UDP 協(xié)議,不需要三次連接進(jìn)行握手,而且也會(huì)縮短 TLS 建立連接的時(shí)間。
- 解決了隊(duì)頭阻塞問(wèn)題
- 實(shí)現(xiàn)動(dòng)態(tài)可插拔,在應(yīng)用層實(shí)現(xiàn)了擁塞控制算法,可以隨時(shí)切換。
- 報(bào)文頭和報(bào)文體分別進(jìn)行認(rèn)證和加密處理,保障安全性。
- 連接能夠平滑遷移
連接平滑遷移指的是,你的手機(jī)或者移動(dòng)設(shè)備在 4G 信號(hào)下和 WiFi 等網(wǎng)絡(luò)情況下切換,不會(huì)斷線重連,用戶甚至無(wú)任何感知,能夠直接實(shí)現(xiàn)平滑的信號(hào)切換。
QUIC 相關(guān)資料
QUIC 協(xié)議比較復(fù)雜,想自己完全實(shí)現(xiàn)一套對(duì)筆者來(lái)說(shuō)還比較困難。
讀者有興趣的話可以先看看開源實(shí)現(xiàn)有哪些。
1)Chromium:https://github.com/hanpfei/chromium-net
這個(gè)是官方支持的。優(yōu)點(diǎn)自然很多,Google 官方維護(hù)基本沒有坑,隨時(shí)可以跟隨 chrome 更新到最新版本。不過(guò)編譯 Chromium 比較麻煩,它有單獨(dú)的一套編譯工具。暫時(shí)不建議考慮這個(gè)方案。
2)proto-quic:https://github.com/google/proto-quic
從 chromium 剝離的一個(gè) QUIC 協(xié)議部分,但是其 github 主頁(yè)已宣布不再支持,僅作實(shí)驗(yàn)使用。不建議考慮這個(gè)方案。
3)goquic:https://github.com/devsisters/goquic
goquic 封裝了 libquic 的 go 語(yǔ)言封裝,而 libquic 也是從 chromium 剝離的,好幾年不維護(hù)了,僅支持到 quic-36, goquic 提供一個(gè)反向代理,測(cè)試發(fā)現(xiàn)由于 QUIC 版本太低,最新 chrome 瀏覽器已無(wú)法支持。不建議考慮這個(gè)方案。
4)quic-go:https://github.com/lucas-clemente/quic-go
quic-go 是完全用 go 寫的 QUIC 協(xié)議棧,開發(fā)很活躍,已在 Caddy 中使用,MIT 許可,目前看是比較好的方案。
那么,對(duì)于中小團(tuán)隊(duì)或個(gè)人開發(fā)者來(lái)說(shuō),比較推薦的方案是最后一個(gè),即采用 caddy https://github.com/caddyserver/caddy/wiki/QUIC 來(lái)部署實(shí)現(xiàn) QUIC。caddy 這個(gè)項(xiàng)目本意并不是專門用來(lái)實(shí)現(xiàn) QUIC 的,它是用來(lái)實(shí)現(xiàn)一個(gè)免簽的 HTTPS web 服務(wù)器的(caddy 會(huì)自動(dòng)續(xù)簽證書)。而QUIC 只是它的一個(gè)附屬功能(不過(guò)現(xiàn)實(shí)是——好像用它來(lái)實(shí)現(xiàn) QUIC 的人更多)。
從 Github 的技術(shù)趨勢(shì)來(lái)說(shuō),有關(guān) QUIC 的開源資源越來(lái)越多,有興趣可以自已逐一研究研究:https://github.com/search?q=quic