http1.1
http1.1的優(yōu)點
- 1. 簡單
HTTP 基本的報文格式就是 header + body,頭部信息也是 key-value 簡單文本的形式,易于理解,降低了學(xué)習(xí)和使用的門檻。
- 2. 靈活和易于擴(kuò)展
HTTP 協(xié)議里的各類請求方法、URI/URL、狀態(tài)碼、頭字段等每個組成要求都沒有被固定死,都允許開發(fā)人員自定義和擴(kuò)充。
同時 HTTP 由于是工作在應(yīng)用層( OSI 第七層),則它下層可以隨意變化,比如:
- HTTPS 就是在 HTTP 與 TCP 層之間增加了
SSL/TLS安全傳輸層; - HTTP/1.1 和 HTTP/2.0 傳輸協(xié)議使用的是 TCP 協(xié)議,而到了 HTTP/3.0 傳輸協(xié)議改用了
UDP 協(xié)議。
- 3. 應(yīng)用廣泛和跨平臺
互聯(lián)網(wǎng)發(fā)展至今,HTTP 的應(yīng)用范圍非常的廣泛,從臺式機(jī)的瀏覽器到手機(jī)上的各種 APP,從看新聞、刷貼吧到購物、理財、吃雞,HTTP 的應(yīng)用遍地開花,同時天然具有跨平臺的優(yōu)越性。
http1.1的缺點
缺點很明顯
- 無狀態(tài)雙刃劍
無狀態(tài)的好處,因為服務(wù)器不會去記憶 HTTP 的狀態(tài),所以不需要額外的資源來記錄狀態(tài)信息,這能減輕服務(wù)器的負(fù)擔(dān),能夠把更多的 CPU 和內(nèi)存用來對外提供服務(wù)。
無狀態(tài)的壞處,既然服務(wù)器沒有記憶能力,它在完成有關(guān)聯(lián)性的操作時會非常麻煩。
對于無狀態(tài)的問題,解法方案有很多種,其中比較簡單的方式用 Cookie技術(shù)和Session技術(shù),而Session又是基于Cookie實現(xiàn)的,這里不過多介紹倆張方案的實現(xiàn)。
- 明文傳輸
明文意味著在傳輸過程中的信息,是可方便閱讀的,比如 Wireshark 抓包都可以直接肉眼查看,為我們調(diào)試工作帶了極大的便利性。
這正是這樣,HTTP 的所有信息都暴露在了光天化日下,相當(dāng)于信息裸奔,也就是http傳輸非常不安全。
- 傳輸過程數(shù)據(jù)可能被篡改
由于數(shù)據(jù)沒有進(jìn)行加密,傳輸過程都是明文,則很有可能被中間站攔截并篡改,所以http無法證明報文的完整性,且容易被篡改。
- 無法驗證通訊雙方身份
不驗證通信雙方的身份,因此有可能遭遇偽裝并劫持流量。
對于無狀態(tài)這一點特點來說,不管是http1.1還是http2.0和http3.0都保持這一特點,因為這一特點并不是完全是一個缺點。
而對于明文傳輸、保證不了報文的完整性和無法驗證雙方身份的這3個缺點,https基本都解決啦,https是在應(yīng)用層和傳輸層中間增加了一層SSL/TSL協(xié)議,https具體解決這3個問題的實現(xiàn)方式這里也不過多介紹。但是需要注意的一點是普通的https僅驗證了服務(wù)端的身份(通過CA證書),其實并沒有驗證客戶端的身份,所以有的https服務(wù)仍然可以通過抓包工具獲取。
你可能也發(fā)現(xiàn)了,有的https服務(wù)可以被抓包,有的卻抓不到,原因就是看https服務(wù)是否開啟的雙向驗證,開始了雙向驗證之后不僅是在服務(wù)端需要按照證書,客戶端也需要安裝證書,也就是只有開啟了雙向驗證的https來可以驗證雙方身份,且無法被抓到工具獲取。
http1.1的性能如何
HTTP 協(xié)議是基于 TCP/IP,并且使用了「請求 - 應(yīng)答」的通信模式,所以性能的關(guān)鍵就在這兩點里。
「請求 - 應(yīng)答」的通信模式是在同一個TCP連接里客戶端發(fā)出一個請求之后只能等待該請求被響應(yīng)之后,客戶端才可以發(fā)生下一個請求,如果上個請求一直沒有被響應(yīng),那么就是會被阻塞,可以發(fā)現(xiàn)http的性能的關(guān)鍵就在于此。
- 長鏈接
早期 HTTP/1.0 性能上的一個很大的問題,那就是每發(fā)起一個請求,都要新建一次 TCP 連接(三次握手),請求完成后,都要斷開TCP鏈接(4次揮手),俗稱短鏈接,而且是串行請求,做了無謂的 TCP 連接建立和斷開,增加了很大的通信開銷。
為了解決上述 TCP 連接問題,HTTP/1.1 提出了長連接的通信方式,也叫持久連接,且是http1.1的默認(rèn)方式了。這種方式的好處在于減少了 TCP 連接的重復(fù)建立和斷開所造成的額外開銷,減輕了服務(wù)器端的負(fù)載。
短連接的特點是:只有任意一方執(zhí)行完代碼或者任意一方顯示的明確提出斷開連接時則會斷開鏈接。
長連接的特點是:雙方代碼執(zhí)行完成后并不會斷開鏈接,而只要任意一端顯示的明確提出斷開連接,才會進(jìn)行4次揮手?jǐn)嚅_TCP連接。
- 管道傳輸
HTTP/1.1 默認(rèn)采用了長連接的方式,這使得管道(pipeline)網(wǎng)絡(luò)傳輸成為了可能。
即可在同一個 TCP 連接里面,客戶端可以發(fā)起多個請求,只要第一個請求發(fā)出去了,不必等其回來,就可以發(fā)第二個請求出去,可以減少整體的響應(yīng)時間。
還記得開頭說的「請求 - 應(yīng)答」的通信模式嗎?該模式下同一個tcp客戶端只能同時發(fā)送一個請求,只有該請求被響應(yīng)之后才可以發(fā)送下一個請求,這種模式對應(yīng)http的性能影響很大,而管道傳輸其實就是想解決這樣問題的,但是遺憾的是管道傳輸并沒有本質(zhì)上解決上述的問題,原因繼續(xù)往下看。
- 隊頭阻塞
舉例來說,客戶端需要請求兩個資源。以前的做法是,在同一個 TCP 連接里面,先發(fā)送 A 請求,然后等待服務(wù)器做出回應(yīng),收到后再發(fā)出 B 請求。那么,管道機(jī)制則是允許瀏覽器同時發(fā)出 A 請求和 B 請求,如下圖:

但是服務(wù)器必須按照接收請求的順序發(fā)送對這些管道化請求的響應(yīng)。
如果服務(wù)端在處理 A 請求時耗時比較長,那么后續(xù)的請求的處理都會被阻塞住,這稱為「隊頭堵塞」。所以管道傳輸只解決了請求隊頭阻塞而沒有解決響應(yīng)隊頭阻塞。也就是說管道傳輸其實很雞肋,沒什么卵用。
TIP:實際上 HTTP/1.1 管道化技術(shù)不是默認(rèn)開啟,而且瀏覽器基本都沒有支持。
有沒有想過為什么響應(yīng)時必須按照請求的順序返回呢?如果沒有按照請求的順序返回會發(fā)生什么情況呢?我舉個??,如果沒有按照請求順序響應(yīng)會發(fā)生什么結(jié)果:
如果有倆個請求a和b,請求a先到達(dá)服務(wù)器,但由于某種原因?qū)е耡的響應(yīng)被阻塞,而請求b隨后到達(dá)服務(wù)器并得到了及時的響應(yīng),那么返回的響應(yīng)的順序其實發(fā)生了顛倒。
客戶端可能會錯誤地將屬于請求a和響應(yīng)b關(guān)聯(lián),此時數(shù)據(jù)就會發(fā)生了錯亂。這就導(dǎo)致客戶端難以確定哪個請求對應(yīng)哪個響應(yīng),從而引發(fā)數(shù)據(jù)錯亂。
這就是為什么要求響應(yīng)和請求的順序要一致,一致時就是可以順序匹配請求和響應(yīng),將不會出現(xiàn)上述的數(shù)據(jù)錯亂問題。
本質(zhì)原因就是請求和響應(yīng)不能一一對應(yīng)而只能按照順序來匹配,可以繼續(xù)往下看,http2.0是怎么解決這個對應(yīng)的問題的。
http2.0
可以發(fā)現(xiàn)http1.1的性能其實很一般,http2.0對其做了很多改進(jìn),使性能發(fā)生了質(zhì)的提升。廢話不多說,上圖。

那 HTTP/2 相比 HTTP/1.1 性能上的改進(jìn):
- 內(nèi)置TLS協(xié)議。
- 頭部壓縮:使用靜態(tài)表、動態(tài)表和HPack實現(xiàn)。
- 二進(jìn)制格式:Header+Body都使用二進(jìn)制傳輸。
- 并發(fā)傳輸:使用Stream、fream實現(xiàn),Stream是實現(xiàn)并發(fā)傳輸?shù)年P(guān)鍵。
- 服務(wù)器主動推送資源。
頭部壓縮
HTTP 協(xié)議的報文是由「Header + Body」構(gòu)成的,對于 Body 部分,HTTP/1.1協(xié)議可以使用頭字段 「Content-Encoding」指定 Body 的壓縮方式,比如用gzip 壓縮,這樣可以節(jié)約帶寬,但報文中的另外一部分 Header,是沒有針對它的優(yōu)化手段。
HTTP/1.1 報文中 Header 部分存在的問題:
-
含很多固定的字段,比如 Cookie、User Agent、Accept 等,這些字段加起來也高達(dá)幾百字節(jié)甚至上千字節(jié),所以有必要壓縮; -
大量的請求和響應(yīng)的報文里有很多字段和字段值都是
重復(fù)的,這樣會使得大量帶寬被這些冗余的數(shù)據(jù)占用了,所以有必須要避免重復(fù)性; - 字段是 ASCII 編碼的,雖然易于人類觀察,但
效率低,所以有必要改成二進(jìn)制編碼;
HTTP/2 對 Header 部分做了大改造,把以上的問題都解決了。
HTTP/2 沒使用常見的 gzip 壓縮方式來壓縮頭部,而是開發(fā)了HPACK 算法,HPACK 算法主要包含三個組成部分:
靜態(tài)字典動態(tài)字典-
Huffman 編碼(壓縮算法)
客戶端和服務(wù)器兩端都會建立和維護(hù)「字典」,用長度較小的索引號表示重復(fù)的字符串,再用Huffman 編碼壓縮數(shù)據(jù),可達(dá)到 50%~90% 的高壓縮率。
靜態(tài)字典
首先TCP連接建立后,客戶端和服務(wù)端都會有一張靜態(tài)字典,它是寫入到 HTTP/2 框架里的,不會變化的,靜態(tài)表里共有61 組,如下圖:

表中的
Index 表示索引(Key),Header Value表示索引對應(yīng)的 Value,Header Name表示字段的名字,比如 Index 為 2 代表Header頭中method: GET,Index 為 8 代表Header頭中的狀態(tài)碼 Status :200。
你可能注意到,表中有的 Index 沒有對應(yīng)的 Header Value,這是因為這些 Value 并不是固定的而是變化的,這些 Value 都會經(jīng)過 Huffman 編碼后,才會發(fā)送出去,具體是怎么實現(xiàn)的呢?繼續(xù)往下看:
我們來看個具體的例子,比如Header頭中下面這個 server 頭部字段,在 HTTP/1.1 的形式如下:
server: nghttpx\r\n
先給出結(jié)論:在http1.1中算上冒號空格和末尾的\r\n,共占用了
17 字節(jié),而使用了靜態(tài)表和Huffman 編碼,可以將它壓縮成8 字節(jié),壓縮率大概47%。
根據(jù) RFC7541 規(guī)范,如果頭部字段屬于靜態(tài)表范圍,并且Value 是變化的,整個頭部格式如下圖:

我抓了個 HTTP/2 協(xié)議的網(wǎng)絡(luò)包,你可以從下圖看到,高亮部分就是 server: nghttx 頭部字段的二進(jìn)制數(shù)據(jù),只用了 8 個字節(jié)而已。

對照著頭部格式來一步一步分析:
如果頭部字段屬于靜態(tài)表范圍,并且Value 是變化時,
第一個字節(jié)的前倆位固定為01,后6位是頭部字段server在靜態(tài)表中的索引值,也就是54,轉(zhuǎn)化為二進(jìn)制為110110,拼接起來之后第一個字節(jié)為01110110。第二個字節(jié)中的第一個位
H表示Value 是否經(jīng)過 Huffman 編碼,1表示經(jīng)過 Huffman 編碼,0則相反。后面的7個bit位表示Value 的長度,也就是nghttx經(jīng)過huffman編碼后的長度為7,也就是0111,至于為什么是7,繼續(xù)看下面,這里寫暫且認(rèn)為是7,拼接上首位1后,第二個字節(jié)表示為10000111。-
接著計算
nghttx的長度。value的值是通過huffman編碼算出來的,而Huffman 編碼的原理是將高頻出現(xiàn)的信息用「較短」的編碼表示,從而縮減字符串長度。于是,在統(tǒng)計大量的 HTTP 頭部后,HTTP/2 根據(jù)
出現(xiàn)頻率將 ASCII 碼編改為了Huffman 編碼表,可以在RFC7541 文檔找到這張靜態(tài) Huffman 表,我就不把表的全部內(nèi)容列出來了,我只列出字符串 nghttpx 中每個字符對應(yīng)的 Huffman 編碼,如下圖:
通過查表后,字符串nghttpx的 Huffman 編碼在下圖看到,共6個字節(jié),每一個字符的 Huffman 編碼,我用相同的顏色將他們對應(yīng)起來了,最后的 7 位是補(bǔ)位的。
最終,server 頭部的二進(jìn)制數(shù)據(jù)對應(yīng)的靜態(tài)頭部格式如下:

動態(tài)字典
靜態(tài)表只包含了61 種高頻出現(xiàn)在頭部的字符串,不在靜態(tài)表范圍內(nèi)的頭部字符串就要自行構(gòu)建動態(tài)表,它的 Index從62 起步,會在編碼解碼的時候隨時更新,也就是靜態(tài)字典喝動態(tài)字典是結(jié)合使用的。
比如,第一次發(fā)送請求時的request頭部中的「Cookie」字段數(shù)據(jù)有上百個字節(jié),經(jīng)過 Huffman 編碼發(fā)送出去后,客戶端就會更新自己的動態(tài)字典,添加一個Index 號 62的數(shù)據(jù)。
當(dāng)服務(wù)器收到請求之后會更新自己的動態(tài)表,也添加一個新的 Index 號 62。
那么在下一次請求的時候,就不用重復(fù)發(fā)這個字段的數(shù)據(jù)了,只用發(fā) 1個字節(jié)的 Index 號就好了,因為雙方都可以根據(jù)自己的動態(tài)表獲取到字段的數(shù)據(jù)。
細(xì)心的人可能發(fā)現(xiàn),如果客戶端請求發(fā)出后,由于網(wǎng)絡(luò)原因服務(wù)端并沒有收到請求,此時會出現(xiàn)的情況是客戶端已經(jīng)更新Index為62的記錄,而服務(wù)并沒有更新。
如果此時客戶端再次請求時,攜帶的是62的Index,而服務(wù)端收到62之后不清楚是什么意思,因為服務(wù)端并沒有存儲62的數(shù)據(jù),此時就會出現(xiàn)問題,
這是http2.0的一個潛在問題,http3.0對該問題進(jìn)行了修復(fù)。
需要注意的是:新建的連接初始化時只有靜態(tài)表,只有在同一個連接上后續(xù)的請求時才會動態(tài)的增加動態(tài)字典,連接銷毀時對應(yīng)的動態(tài)字典也就隨之消失。如果消息字段在 1 個連接上只發(fā)送了 1 次,或者重復(fù)傳輸時,字段總是略有變化,動態(tài)表就無法被充分利用了。
因此,隨著在同一 HTTP/2 連接上發(fā)送的報文越來越多,客戶端和服務(wù)器雙方的「字典」積累的越來越多,理論上最終每個頭部字段都會變成 1 個字節(jié)的 Index,這樣便避免了大量的冗余數(shù)據(jù)的傳輸,大大節(jié)約了帶寬。
理想很美好,現(xiàn)實很骨感。動態(tài)表越大,占用的內(nèi)存也就越大,如果占用了太多內(nèi)存,是會影響服務(wù)器性能的,因此 Web 服務(wù)器都會提供類似 http2_max_requests的配置,用于限制一個連接上能夠傳輸?shù)恼埱髷?shù)量,避免動態(tài)表無限增大,請求數(shù)量到達(dá)上限后,就會關(guān)閉 HTTP/2 連接來釋放內(nèi)存。
綜上,HTTP/2 頭部的編碼通過「靜態(tài)表、動態(tài)表、Huffman 編碼」共同完成的。
二進(jìn)制
HTTP/2 厲害的地方在于將 HTTP/1 的文本格式改成二進(jìn)制格式傳輸數(shù)據(jù),極大提高了 HTTP 傳輸效率,而且二進(jìn)制數(shù)據(jù)使用位運算能高效解析。
二進(jìn)制數(shù)據(jù)傳輸?shù)幕締挝皇?code>二進(jìn)制幀,即為fream,下圖為fream的結(jié)構(gòu)

- 幀開頭的前 3 個字節(jié)表示幀數(shù)據(jù)(Frame Playload)的長度。
- 幀長度后面的一個字節(jié)是表示
幀的類型,HTTP/2 總共定義了 10 種類型的幀,一般分為數(shù)據(jù)幀和控制幀兩類,如下表格:
我們主要關(guān)注數(shù)據(jù)幀,我們知道http2.0中的·Header+Body·都是使用·二進(jìn)制幀·來實現(xiàn),如果幀的類型為HEADRERS則表示該幀的數(shù)據(jù)為·Header數(shù)據(jù)·,如果幀的類型為DATA則表示該幀的數(shù)據(jù)為Body數(shù)據(jù)。
幀的類型主要的作用是表明該數(shù)據(jù)是什么類型的數(shù)據(jù)。 - 幀類型后面的一個字節(jié)是
標(biāo)志位,可以保存 8 個標(biāo)志位,用于攜帶簡單的控制信息,比如:-
END_HEADERS表示頭數(shù)據(jù)結(jié)束標(biāo)志,相當(dāng)于 HTTP/1 里頭后的空行(“\r\n”); -
END_Stream表示單方向數(shù)據(jù)發(fā)送結(jié)束,后續(xù)不會再有數(shù)據(jù)幀。 -
PRIORITY表示流的優(yōu)先級;
-
該
標(biāo)志位的作用是非常重要的,想象一個http請求被分成了多個幀數(shù)據(jù)發(fā)生,服務(wù)端在接受到這么多幀數(shù)據(jù)的時,可以根據(jù)數(shù)據(jù)幀的類別區(qū)別出哪些幀是Header數(shù)據(jù)哪些是Body數(shù)據(jù)。但是
一個完整的Header數(shù)據(jù)或者Body數(shù)據(jù)是被分成多個幀數(shù)據(jù)發(fā)送的,服務(wù)端是當(dāng)收到幀類別為HEADRERS且標(biāo)志位為END_HEADERS時表示該請求的header數(shù)據(jù)已經(jīng)全部接受完成了,可以處理header請求了;同理當(dāng)收到幀類別為
DATA且標(biāo)識位為END_Stream時表示該請求的body數(shù)據(jù)全部發(fā)生完成了,可以處理body請求了。可以發(fā)現(xiàn)標(biāo)志位的作用非常重要,但是有沒有發(fā)現(xiàn)一點,如果幀數(shù)據(jù)發(fā)生順序錯亂,會發(fā)生嚴(yán)重的問題,比如當(dāng)收到了一個幀類別為
HEADRERS且標(biāo)志位為END_HEADERS的幀數(shù)據(jù)時,就代表著header頭數(shù)據(jù)發(fā)生完畢了,但是由于網(wǎng)絡(luò)原因?qū)е缕渲械囊粋€header幀數(shù)據(jù)又發(fā)生過來了,這不出現(xiàn)很重的問題了嗎?我已經(jīng)認(rèn)為header數(shù)據(jù)發(fā)生完畢了,然后過一會,你發(fā)過來一個header數(shù)據(jù),我該怎么處理啊,全亂了。
所以同一個stream里的幀數(shù)據(jù)是嚴(yán)格要求順序的發(fā)送的,不可以亂序發(fā)送的,時序問題由tcp的順序性保證(序列號)。
- 幀頭的最后 4 個字節(jié)是
流標(biāo)識符(Stream ID),最高位被保留不用,只有 31 位可以使用,因此流標(biāo)識符的最大值是 2^31,大約是 21 億,它的作用是用來標(biāo)識該Frame屬于哪個Stream,接收方可以根據(jù)這個信息從亂序的幀(這里的亂序是指所屬不同Stream的Fream是亂序的,但是同一個Stream內(nèi)的Fream肯定是有序的)里找到相同 Stream ID 的幀,從而有序組裝信息。
如果你不懂Stream和Fream的關(guān)系,那么繼續(xù)往下看
并發(fā)傳輸
知道了 HTTP/2 的幀結(jié)構(gòu)后,我們再來看看它是如何實現(xiàn)并發(fā)傳輸?shù)摹?/p>
我們都知道 HTTP/1.1 的實現(xiàn)是基于請求-響應(yīng)模型的。同一個TCP連接中,HTTP 完成一個事務(wù)(請求與響應(yīng)),才能處理下一個事務(wù),也就是說在發(fā)出請求等待響應(yīng)的過程中,是沒辦法做其他事情的,如果響應(yīng)遲遲不來,那么后續(xù)的請求是無法發(fā)送的,也造成了隊頭阻塞的問題,這也是http1.1的性能關(guān)鍵。
而 HTTP/2 就很牛逼了,引出了 Stream 概念,多個 Stream 可復(fù)用在一條 TCP 連接。實現(xiàn)了在同一個TCP連接上可以并發(fā)多個請求和響應(yīng)。
為了理解 HTTP/2 的并發(fā)是怎樣實現(xiàn)的,我們先來理解 HTTP/2 中的 Stream、Message、Frame 這 3 個概念。

你可以從上圖中看到:
- 1 個 TCP 連接包含一個或者多個 Stream,Stream 是 HTTP/2 并發(fā)的關(guān)鍵技術(shù)。一個TCP連接由相同的四元組組成,即源ip、源端口、目標(biāo)ip和目標(biāo)端口,也就是客戶端并發(fā)請求,服務(wù)端也可以并發(fā)響應(yīng);
- Stream 里可以包含 1 個或多個 Message,Message 對應(yīng) HTTP/1 中的請求或響應(yīng),由 HTTP 頭部和包體構(gòu)成;
- Message 里包含一條或者多個 Frame,F(xiàn)rame 是 HTTP/2 最小單位,以二進(jìn)制壓縮格式存放 HTTP/1 中的內(nèi)容(頭部和包體);
我估計你看到這里還是云里霧里的,為什么Stream能實現(xiàn)并發(fā)傳輸呢?Stream到底是個什么呢?
想知道這個問題的答案,先來回顧下http1.1中開啟管道傳輸之后為什么只支持客戶端并發(fā)請求而不支持服務(wù)端的并發(fā)響應(yīng)呢?根本原因就是服務(wù)端的響應(yīng)和客戶端的請求對應(yīng)不上,只能根據(jù)請求和響應(yīng)的順序來匹配。
而Stream其實是一個唯一的ID標(biāo)識,在同一個tcp連接中的 Stream ID 是不能復(fù)用的,只能順序遞增,Stream可以理解為并不是一個真實存在的東西,就是一個唯一的自增ID。
我們知道幀數(shù)據(jù)Fream是數(shù)據(jù)傳輸?shù)幕締挝唬?code>Fream結(jié)構(gòu)中有個流標(biāo)識符是用來表示該Fream所屬的Stream ID。
客戶端的每次請求都會分配一個唯一的Stream ID。比如客戶端發(fā)出一個請求,Stream ID為123,而請求體中Header+Body會被分割成多個Fream,且這些Fream的標(biāo)識符都將是123;當(dāng)服務(wù)端響應(yīng)數(shù)據(jù)時,也會將響應(yīng)體中的Header+Body分割成多個Fream,且這些響應(yīng)體的所有Fream的標(biāo)識符也同樣是123。
看到這里,我想你應(yīng)該明白了Stream是怎么實現(xiàn)并發(fā)傳輸?shù)牧税?。http1.1中由于不知道返回的響應(yīng)是屬于哪個請求的,所以只能默認(rèn)按照順序匹配。而http2.0中請求和響應(yīng)共用一個Stream ID,這樣就可以將請求和響應(yīng)進(jìn)行精準(zhǔn)匹配啦。
因此,我們可以得出個結(jié)論:多個 Stream 跑在一條 TCP 連接,同一個 HTTP 請求與響應(yīng)是跑在同一個 Stream 中,HTTP 消息可以由多個 Frame 構(gòu)成, 一個 Frame 可以由多個 TCP 報文構(gòu)成。
在 HTTP/2 連接上,不同 Stream 的幀是可以亂序發(fā)送的(因此可以并發(fā)不同的 Stream ),因為每個幀的頭部會攜帶 Stream ID 信息,所以接收端可以通過 Stream ID 有序組裝成 HTTP 消息,而同一 Stream 內(nèi)部的幀必須是嚴(yán)格有序的,在上面幀數(shù)據(jù)結(jié)構(gòu)介紹標(biāo)志位的說明為什么必須保持有序。
比如下圖,服務(wù)端并行亂序的地發(fā)送了兩個響應(yīng): Stream 1 和 Stream 3,這兩個 Stream 都是跑在一個 TCP 連接上,客戶端收到后,會根據(jù)相同的 Stream ID 有序組裝成 HTTP 消息,并將組裝完成的消息發(fā)送給相同Stream ID 的請求。

客戶端和服務(wù)器雙方都可以建立 Stream,因為
服務(wù)端可以主動推送資源給客戶端。
客戶端建立的 Stream 必須是奇數(shù)號,而服務(wù)器建立的 Stream 必須是偶數(shù)號。
同一個連接中的 Stream ID 是不能復(fù)用的,只能順序遞增。從幀結(jié)構(gòu)中可以發(fā)現(xiàn),流標(biāo)識符(Stream ID),只有 31 位可以使用,因此流標(biāo)識符的最大值是 2^31,大約是 21 億,所以當(dāng) Stream ID 耗盡時,需要發(fā)一個控制幀 GOAWAY,用來優(yōu)雅關(guān)閉 TCP 連接。
服務(wù)器主動推送資源
HTTP/1.1 不支持服務(wù)器主動推送資源給客戶端,都是由客戶端向服務(wù)器發(fā)起請求后,才能獲取到服務(wù)器響應(yīng)的資源。
比如,客戶端通過 HTTP/1.1 請求從服務(wù)器那獲取到了 HTML 文件,而 HTML 可能還需要依賴 CSS 來渲染頁面,這時客戶端還要再發(fā)起獲取 CSS 文件的請求,需要兩次消息往返,如下圖左邊部分:

如上圖右邊部分,在 HTTP/2 中,客戶端在訪問 HTML 時,服務(wù)器可以直接主動推送 CSS 文件,減少了消息傳遞的次數(shù),
減少了網(wǎng)絡(luò)耗時。
在 Nginx 中,如果你希望客戶端訪問 /test.html 時,服務(wù)器直接推送 /test.css,那么可以這么配置:
location /test.html {
http2_push /test.css;
}
http2.0存在的缺點
http2.0相對于http1.1性能確實提升了很多,但是仍然還存在一些問題
- 隊頭阻塞
- TCP和TLS的握手延時
- 網(wǎng)絡(luò)遷移需要重新建立連接
TCP 隊頭阻塞
HTTP/2 多個請求是跑在一個 TCP 連接中的,那么當(dāng) TCP 丟包時,整個 TCP 都要等待重傳,那么就會阻塞該 TCP 連接中的所有請求。
比如下圖中,Stream 2 有一個 TCP 報文丟失了,那么即使收到了 Stream 3 和 Stream 4 的 TCP 報文,應(yīng)用層也是無法讀取讀取的,相當(dāng)于阻塞了 Stream 3 和 Stream 4 請求。

因為HTTP2.0是基于TCP 是字節(jié)流協(xié)議,TCP 層必須保證收到的字節(jié)數(shù)據(jù)是完整且有序的,如果序列號較低的 TCP 段在網(wǎng)絡(luò)傳輸中丟失了,即使序列號較高的 TCP 段已經(jīng)被接收了,也會被阻塞在
傳輸層,應(yīng)用層也無法從內(nèi)核中讀取到這部分?jǐn)?shù)據(jù),只有當(dāng)所有的TCP段都被接受成功之后,才會將數(shù)據(jù)組裝之后發(fā)送給應(yīng)用層,應(yīng)用層才會讀取數(shù)據(jù)。
可以發(fā)現(xiàn)出現(xiàn)阻塞的本質(zhì)原因是:阻塞出現(xiàn)在了傳輸層,而導(dǎo)致應(yīng)用層讀不到數(shù)據(jù)。
那么如果讓阻塞出現(xiàn)在應(yīng)用層而不要出現(xiàn)在網(wǎng)絡(luò)層是不是就可以避免隊頭阻塞了,可以看下http3.0的實現(xiàn)。
可以發(fā)現(xiàn)Http2.0出現(xiàn)隊頭阻塞的場景只發(fā)送在Tcp丟包的場景下。
可以發(fā)現(xiàn)http2.0不管怎么優(yōu)化也解決不了TCP的隊頭阻塞問題,除非把TCP協(xié)議換了, 這是TCP 協(xié)議固有的問題??梢越永m(xù)往下看http3.0是怎么解決TCP隊頭阻塞的。
TCP和TLS的握手延時
發(fā)起 HTTP 請求時,需要經(jīng)過 TCP 三次握手和 TLS 四次握手(TLS 1.2)的過程,因此共需要 3 個 RTT 的時延才能發(fā)出請求數(shù)據(jù)。
另外,TCP 由于具有「擁塞控制」的特性,所以剛建立連接的 TCP 會有個「慢啟動」的過程,它會對 TCP 連接產(chǎn)生“減速”效果,給人的感覺就是突然卡頓了一下。
網(wǎng)絡(luò)遷移需要重新建立連接
一個 TCP 連接是由四元組(源 IP 地址,源端口,目標(biāo) IP 地址,目標(biāo)端口)確定的,這意味著如果 IP 地址或者端口變動了,就會導(dǎo)致需要 TCP 與 TLS 重新握手,這不利于移動設(shè)備切換網(wǎng)絡(luò)的場景,比如 4G 網(wǎng)絡(luò)環(huán)境切換成 WiFi。
這些問題都是 TCP 協(xié)議固有的問題,無論應(yīng)用層的 HTTP/2 在怎么設(shè)計都無法逃脫。要解決這個問題,就必須把傳輸層協(xié)議替換成 UDP,這個大膽的決定,HTTP/3 做了!
http3.0
http2.0有3個缺陷,http3.0都給解決了,接下來看下是怎么一步一步解決的。

可以發(fā)現(xiàn)http3.0有幾個特點
- 傳輸層由TCP改用成UDP協(xié)議。我們深知,UDP 是一個簡單、不可靠的傳輸協(xié)議,而且是 UDP 包之間是無序的,也沒有依賴關(guān)系。而且,UDP 是不需要連接的,也就不需要握手和揮手的過程,所以天然的就比 TCP 快。
-
應(yīng)用層新增QUIC協(xié)議: 雖然UDP是不具備可靠性等特性的,但是UDP的上層協(xié)議QUID,它具有類似 TCP 的連接管理、擁塞窗口、流量控制的網(wǎng)絡(luò)特性,相當(dāng)于將不可靠傳輸?shù)?UDP 協(xié)議變成“可靠”的了,所以不用擔(dān)心數(shù)據(jù)包丟失的問題。 - TLS協(xié)議內(nèi)置在了
QUIC協(xié)議中。避免了TLS的4次握手。 - 由HTTP2.0中的
HPACK編碼格式改為了QPACK編碼格式。
無隊頭阻塞
QUIC 協(xié)議也有類似 HTTP/2 Stream 與多路復(fù)用的概念,也是可以在同一條連接上并發(fā)傳輸多個 Stream的,這個優(yōu)點http3.0是繼承了的。
在來回憶下,為什么HTTP2.0會有TCP隊頭阻塞?
主要原因是:當(dāng)tcp發(fā)送丟包時,tcp所在的傳輸層會發(fā)生阻塞,進(jìn)而應(yīng)用層也發(fā)生阻塞。只有組裝完成所有tcp段之后,應(yīng)用層才會讀取。
而上面的http3.0介紹中TCP改成了UDP,而UDP并不是對數(shù)據(jù)的連續(xù)性,時序性等問題進(jìn)行驗證,所以傳輸層肯定是不會發(fā)生阻塞了。
因為UDP是不可靠的,所以在應(yīng)用層實現(xiàn)了QUIC協(xié)議來保證消息的可靠性。http2.0和http3.0對比可發(fā)現(xiàn)一點:阻塞發(fā)生的層級由http2.0的傳輸層變更到了http3.0的應(yīng)用層。正是由于這一變更,如果http3.0請求時發(fā)生丟包則應(yīng)用層只會阻塞本請求,后續(xù)的請求是不會阻塞的,應(yīng)用層都是可以讀取到的;同理,如果ttp3.0響應(yīng)時發(fā)生丟包則應(yīng)用層也只會阻塞本響應(yīng),后續(xù)的響應(yīng)是不會阻塞的,應(yīng)用層都是可以讀取到的。
所以,QUIC 連接上的多個 Stream 之間并沒有依賴,都是獨立的,某個流發(fā)生丟包了,只會影響該流,其他流不受影響。
更快的連接建立
對于 HTTP/1 和 HTTP/2 協(xié)議,TCP 和 TLS 是分層的,分別屬于內(nèi)核實現(xiàn)的傳輸層、OpenSSL 庫實現(xiàn)的表示層,因此它們難以合并在一起,需要分批次來握手,先 3次TCP 握手,再 4次TLS 握手。
HTTP/3 在傳輸數(shù)據(jù)前雖然需要 QUIC 協(xié)議握手,這個握手過程只需要 1 RTT,握手的目的是為確認(rèn)雙方的「連接 ID」,連接遷移就是基于連接 ID 實現(xiàn)的。
但是 HTTP/3 的 QUIC 協(xié)議并不是與 TLS 分層,而是 QUIC 內(nèi)部包含了 TLS,它在自己的幀會攜帶 TLS 里的“記錄”,再加上 QUIC 使用的是 TLS 1.3,因此僅需 1 個 RTT 就可以「同時」完成建立連接與密鑰協(xié)商,甚至在第二次連接的時候,應(yīng)用數(shù)據(jù)包可以和 QUIC 握手信息(連接信息 + TLS 信息)一起發(fā)送,達(dá)到0-RTT的效果。
至于為什么第二次連接時為 0-RTT,接著繼續(xù)看下面。
網(wǎng)絡(luò)遷移
在前面我們提到,基于 TCP 傳輸協(xié)議的 HTTP 協(xié)議,由于是通過四元組(源 IP、源端口、目的 IP、目的端口)確定一條 TCP 連接。

那么當(dāng)移動設(shè)備的網(wǎng)絡(luò)從 4G 切換到 WiFi 時,意味著 IP 地址變化了,那么就必須要斷開連接,然后重新建立連接,而建立連接的過程包含 TCP 三次握手和 TLS 四次握手的時延,以及 TCP 慢啟動的減速過程,給用戶的感覺就是網(wǎng)絡(luò)突然卡頓了一下,因此連接的遷移成本是很高的。
而 QUIC 協(xié)議沒有用四元組的方式來“綁定”連接,而是通過連接 ID 來標(biāo)記通信的兩個端點,客戶端和服務(wù)器可以各自選擇一組 ID 來標(biāo)記自己,因此即使移動設(shè)備的網(wǎng)絡(luò)變化后,導(dǎo)致 IP 地址變化了,只要仍保有上下文信息(比如連接 ID、TLS 密鑰等),就可以“無縫”地復(fù)用原連接,消除重連的成本,沒有絲毫卡頓感,達(dá)到了連接遷移的功能。


