http1.1
http1.1的優點
- 1. 簡單
HTTP 基本的報文格式就是 header + body
,頭部信息也是 key-value
簡單文本的形式,易于理解,降低了學習和使用的門檻。
- 2. 靈活和易于擴展
HTTP 協議里的各類請求方法、URI/URL、狀態碼、頭字段等每個組成要求都沒有被固定死,都允許開發人員自定義和擴充。
同時 HTTP 由于是工作在應用層( OSI
第七層),則它下層可以隨意變化,比如:
- HTTPS 就是在 HTTP 與 TCP 層之間增加了
SSL/TLS
安全傳輸層; - HTTP/1.1 和 HTTP/2.0 傳輸協議使用的是 TCP 協議,而到了 HTTP/3.0 傳輸協議改用了
UDP 協議
。
- 3. 應用廣泛和跨平臺
互聯網發展至今,HTTP 的應用范圍非常的廣泛,從臺式機的瀏覽器到手機上的各種 APP,從看新聞、刷貼吧到購物、理財、吃雞,HTTP 的應用遍地開花,同時天然具有跨平臺的優越性。
http1.1的缺點
缺點很明顯
- 無狀態雙刃劍
無狀態的好處,因為服務器不會去記憶 HTTP 的狀態,所以不需要額外的資源來記錄狀態信息,這能減輕服務器的負擔,能夠把更多的 CPU 和內存用來對外提供服務。
無狀態的壞處,既然服務器沒有記憶能力,它在完成有關聯性的操作時會非常麻煩。
對于無狀態的問題,解法方案有很多種,其中比較簡單的方式用 Cookie
技術和Session
技術,而Session又是基于Cookie實現的,這里不過多介紹倆張方案的實現。
- 明文傳輸
明文意味著在傳輸過程中的信息,是可方便閱讀的,比如 Wireshark 抓包都可以直接肉眼查看,為我們調試工作帶了極大的便利性。
這正是這樣,HTTP 的所有信息都暴露在了光天化日下,相當于信息裸奔
,也就是http傳輸非常不安全。
- 傳輸過程數據可能被篡改
由于數據沒有進行加密,傳輸過程都是明文,則很有可能被中間站攔截并篡改,所以http無法證明報文的完整性,且容易被篡改。
- 無法驗證通訊雙方身份
不驗證通信雙方的身份,因此有可能遭遇偽裝并劫持流量。
對于無狀態
這一點特點來說,不管是http1.1還是http2.0和http3.0都保持這一特點,因為這一特點并不是完全是一個缺點。
而對于明文傳輸
、保證不了報文的完整性
和無法驗證雙方身份
的這3個缺點,https
基本都解決啦,https是在應用層和傳輸層中間增加了一層SSL/TSL協議,https具體解決這3個問題的實現方式這里也不過多介紹。但是需要注意的一點是普通的https僅驗證了服務端的身份(通過CA證書),其實并沒有驗證客戶端的身份,所以有的https服務仍然可以通過抓包工具獲取。
你可能也發現了,有的https服務可以被抓包,有的卻抓不到,原因就是看https服務是否開啟的雙向驗證
,開始了雙向驗證之后不僅是在服務端需要按照證書,客戶端也需要安裝證書,也就是只有開啟了雙向驗證的https來可以驗證雙方身份,且無法被抓到工具獲取
。
http1.1的性能如何
HTTP 協議是基于 TCP/IP
,并且使用了「請求 - 應答」
的通信模式,所以性能的關鍵就在這兩點里。
「請求 - 應答」
的通信模式是在同一個TCP連接
里客戶端發出一個請求之后只能等待該請求被響應之后,客戶端才可以發生下一個請求,如果上個請求一直沒有被響應,那么就是會被阻塞,可以發現http的性能的關鍵就在于此。
- 長鏈接
早期 HTTP/1.0 性能上的一個很大的問題,那就是每發起一個請求,都要新建一次 TCP 連接(三次握手),請求完成后,都要斷開TCP鏈接(4次揮手),俗稱短鏈接,而且是串行請求,做了無謂的 TCP 連接建立和斷開,增加了很大的通信開銷。
為了解決上述 TCP 連接問題,HTTP/1.1 提出了長連接
的通信方式,也叫持久連接
,且是http1.1的默認方式了。這種方式的好處在于減少了 TCP 連接的重復建立
和斷開
所造成的額外開銷,減輕了服務器端的負載。
短連接的特點是:只有任意一方執行完代碼或者任意一方顯示的明確提出斷開連接時則會斷開鏈接。
長連接的特點是:雙方代碼執行完成后并不會斷開鏈接,而只要任意一端顯示的明確提出斷開連接,才會進行4次揮手斷開TCP連接。
- 管道傳輸
HTTP/1.1 默認采用了長連接
的方式,這使得管道
(pipeline)網絡傳輸成為了可能。
即可在同一個 TCP 連接里面,客戶端可以發起多個請求
,只要第一個請求發出去了,不必等其回來,就可以發第二個請求出去,可以減少整體的響應時間。
還記得開頭說的「請求 - 應答」
的通信模式嗎?該模式下同一個tcp客戶端只能同時發送一個請求,只有該請求被響應之后才可以發送下一個請求,這種模式對應http的性能影響很大,而管道傳輸
其實就是想解決這樣問題的,但是遺憾的是管道傳輸
并沒有本質上解決上述的問題,原因繼續往下看。
- 隊頭阻塞
舉例來說,客戶端需要請求兩個資源。以前的做法是,在同一個 TCP 連接里面,先發送 A 請求,然后等待服務器做出回應,收到后再發出 B 請求。那么,管道機制
則是允許瀏覽器同時發出 A 請求和 B 請求
,如下圖:
但是服務器必須按照接收請求的順序發送對這些管道化請求的響應。
如果服務端在處理 A 請求時耗時比較長,那么后續的請求的處理都會被阻塞住,這稱為「隊頭堵塞」。所以管道傳輸
只解決了請求隊頭阻塞
而沒有解決響應隊頭阻塞
。也就是說管道傳輸
其實很雞肋,沒什么卵用。
TIP:實際上 HTTP/1.1 管道化技術不是默認開啟
,而且瀏覽器基本都沒有支持。
有沒有想過為什么響應時必須按照請求的順序返回呢?如果沒有按照請求的順序返回會發生什么情況呢?我舉個??,如果沒有按照請求順序響應會發生什么結果:
如果有倆個請求a和b,請求a先到達服務器,但由于某種原因導致a的響應被阻塞,而請求b隨后到達服務器并得到了及時的響應,那么返回的響應的順序其實發生了顛倒
。
客戶端可能會錯誤地將屬于請求a和響應b關聯,此時數據就會發生了錯亂。這就導致客戶端難以確定哪個請求對應哪個響應,從而引發數據錯亂。
這就是為什么要求響應和請求的順序要一致,一致時就是可以順序匹配請求和響應
,將不會出現上述的數據錯亂問題。
本質原因就是請求和響應不能一一對應而只能按照順序來匹配,可以繼續往下看,http2.0是怎么解決這個對應的問題的。
http2.0
可以發現http1.1的性能其實很一般,http2.0對其做了很多改進,使性能發生了質的提升。廢話不多說,上圖。
那 HTTP/2 相比 HTTP/1.1 性能上的改進:
- 內置TLS協議。
- 頭部壓縮:使用靜態表、動態表和HPack實現。
- 二進制格式:Header+Body都使用二進制傳輸。
- 并發傳輸:使用Stream、fream實現,Stream是實現并發傳輸的關鍵。
- 服務器主動推送資源。
頭部壓縮
HTTP 協議的報文是由「Header + Body」
構成的,對于 Body 部分,HTTP/1.1
協議可以使用頭字段 「Content-Encoding」
指定 Body 的壓縮方式,比如用gzip
壓縮,這樣可以節約帶寬,但報文中的另外一部分 Header,是沒有針對它的優化手段。
HTTP/1.1 報文中 Header 部分存在的問題:
-
含很多固定的字段
,比如 Cookie、User Agent、Accept 等,這些字段加起來也高達幾百字節
甚至上千字節
,所以有必要壓縮
; -
大量的請求和響應的報文里有很多字段和字段值都是
重復
的,這樣會使得大量帶寬被這些冗余的數據占用了
,所以有必須要避免重復性
; - 字段是 ASCII 編碼的,雖然易于人類觀察,但
效率低
,所以有必要改成二進制編碼
;
HTTP/2 對 Header 部分做了大改造,把以上的問題都解決了。
HTTP/2 沒使用常見的 gzip
壓縮方式來壓縮頭部,而是開發了HPACK
算法,HPACK 算法主要包含三個組成部分:
靜態字典
動態字典
-
Huffman 編碼
(壓縮算法)
客戶端和服務器兩端都會建立和維護「字典」
,用長度較小的索引號
表示重復的字符串,再用Huffman 編碼壓縮數據
,可達到 50%~90% 的高壓縮率。
靜態字典
首先TCP連接建立后,客戶端和服務端都會有一張靜態字典
,它是寫入到 HTTP/2 框架里的,不會變化的,靜態表里共有61 組
,如下圖:
表中的
Index
表示索引(Key),Header Value
表示索引對應的 Value,Header Name
表示字段的名字,比如 Index 為 2 代表Header頭中method: GET,Index 為 8 代表Header頭中的狀態碼 Status :200。
你可能注意到,表中有的 Index 沒有對應的 Header Value,這是因為這些 Value 并不是固定
的而是變化的
,這些 Value 都會經過 Huffman 編碼
后,才會發送出去,具體是怎么實現的呢?繼續往下看:
我們來看個具體的例子,比如Header頭中下面這個 server 頭部字段,在 HTTP/1.1 的形式如下:
server: nghttpx\r\n
先給出結論:在http1.1中算上冒號空格和末尾的\r\n,共占用了
17 字節
,而使用了靜態表
和Huffman 編碼
,可以將它壓縮成8 字節
,壓縮率大概47%
。
根據 RFC7541 規范,如果頭部字段
屬于靜態表范圍
,并且Value
是變化
的,整個頭部格式如下圖:
我抓了個 HTTP/2 協議的網絡包,你可以從下圖看到,高亮部分就是 server: nghttx 頭部字段的二進制數據,只用了 8 個字節而已。
對照著頭部格式來一步一步分析:
如果頭部字段屬于靜態表范圍,并且Value 是變化時,
第一個字節
的前倆位固定為01
,后6位是頭部字段server
在靜態表中的索引值
,也就是54
,轉化為二進制為110110
,拼接起來之后第一個字節為01110110
。第二個字節中的第一個位
H
表示Value 是否經過 Huffman 編碼,1
表示經過 Huffman 編碼,0
則相反。后面的7個bit位表示Value 的長度
,也就是nghttx
經過huffman編碼后的長度為7
,也就是0111
,至于為什么是7,繼續看下面,這里寫暫且認為是7,拼接上首位1后,第二個字節表示為10000111
。-
接著計算
nghttx
的長度。value
的值是通過huffman編碼
算出來的,而Huffman 編碼的原理是將高頻出現的信息用「較短」的編碼表示,從而縮減字符串長度。
于是,在統計大量的 HTTP 頭部后,HTTP/2 根據
出現頻率
將 ASCII 碼編改為了Huffman 編碼表
,可以在RFC7541 文檔
找到這張靜態 Huffman 表
,我就不把表的全部內容列出來了,我只列出字符串 nghttpx 中每個字符對應的 Huffman 編碼,如下圖:
通過查表后,字符串nghttpx
的 Huffman 編碼在下圖看到,共6
個字節,每一個字符的 Huffman 編碼
,我用相同的顏色將他們對應起來了,最后的 7 位是補位的
。
最終,server 頭部
的二進制數據對應的靜態頭部格式如下:
動態字典
靜態表
只包含了61
種高頻
出現在頭部的字符串,不在靜態表范圍內的頭部字符串就要自行構建動態表
,它的 Index
從62
起步,會在編碼解碼的時候隨時更新,也就是靜態字典喝動態字典是結合使用的。
比如,第一次發送請求時的request頭部中的「Cookie」
字段數據有上百個字節,經過 Huffman 編碼
發送出去后,客戶端就會更新自己的動態字典
,添加一個Index 號 62
的數據。
當服務器收到請求之后會更新自己的動態表
,也添加一個新的 Index 號 62
。
那么在下一次請求的時候,就不用重復發這個字段的數據了,只用發 1
個字節的 Index 號就好了,因為雙方都可以根據自己的動態表獲取到字段的數據。
細心的人可能發現,如果客戶端請求發出后,由于網絡原因服務端并沒有收到請求,此時會出現的情況是客戶端已經更新Index為62的記錄,而服務并沒有更新。
如果此時客戶端再次請求時,攜帶的是62的Index,而服務端收到62之后不清楚是什么意思,因為服務端并沒有存儲62的數據,此時就會出現問題,
這是http2.0的一個潛在問題,http3.0對該問題進行了修復。
需要注意的是:新建的連接初始化時只有靜態表,只有在同一個連接上后續的請求時才會動態的增加動態字典,連接銷毀時對應的動態字典也就隨之消失。
如果消息字段在 1 個連接上只發送了 1 次,或者重復傳輸時,字段總是略有變化,動態表就無法被充分利用了。
因此,隨著在同一 HTTP/2 連接上發送的報文越來越多,客戶端和服務器雙方的「字典」積累的越來越多,理論上最終每個頭部字段都會變成 1 個字節的 Index,這樣便避免了大量的冗余數據的傳輸,大大節約了帶寬。
理想很美好,現實很骨感。動態表越大,占用的內存也就越大
,如果占用了太多內存,是會影響服務器性能的,因此 Web 服務器都會提供類似 http2_max_requests
的配置,用于限制一個連接上能夠傳輸的請求數量,避免動態表無限增大
,請求數量到達上限后,就會關閉 HTTP/2 連接來釋放內存。
綜上,HTTP/2 頭部的編碼通過「靜態表、動態表、Huffman 編碼」共同完成的。
二進制
HTTP/2 厲害的地方在于將 HTTP/1 的文本格式改成二進制格式傳輸數據,極大提高了 HTTP 傳輸效率,而且二進制數據使用位運算能高效解析。
二進制數據傳輸的基本單位是二進制幀
,即為fream
,下圖為fream
的結構
- 幀開頭的前 3 個字節表示幀數據(Frame Playload)的長度。
- 幀長度后面的一個字節是表示
幀的類型
,HTTP/2 總共定義了 10 種類型的幀,一般分為數據幀
和控制幀
兩類,如下表格:
我們主要關注數據幀
,我們知道http2.0中的·Header+Body·都是使用·二進制幀·來實現,如果幀的類型為HEADRERS
則表示該幀的數據為·Header數據·,如果幀的類型為DATA
則表示該幀的數據為Body
數據。
幀的類型
主要的作用是表明該數據是什么類型的數據。 - 幀類型后面的一個字節是
標志位
,可以保存 8 個標志位,用于攜帶簡單的控制信息,比如:-
END_HEADERS
表示頭數據結束標志,相當于 HTTP/1 里頭后的空行(“\r\n”); -
END_Stream
表示單方向數據發送結束,后續不會再有數據幀。 -
PRIORITY
表示流的優先級;
-
該
標志位
的作用是非常重要的,想象一個http請求被分成了多個幀數據
發生,服務端在接受到這么多幀數據
的時,可以根據數據幀的類別
區別出哪些幀是Header數據哪些是Body數據。但是
一個完整的Header數據或者Body數據是被分成多個幀數據發送
的,服務端是當收到幀類別為HEADRERS
且標志位為END_HEADERS
時表示該請求的header數據已經全部接受完成了,可以處理header請求了;同理當收到幀類別為
DATA
且標識位為END_Stream
時表示該請求的body數據全部發生完成了,可以處理body請求了。可以發現標志位的作用非常重要,但是有沒有發現一點,如果幀數據發生順序錯亂,會發生嚴重的問題,比如當收到了一個幀類別為
HEADRERS
且標志位為END_HEADERS
的幀數據時,就代表著header頭數據發生完畢了,但是由于網絡原因導致其中的一個header幀數據又發生過來了,這不出現很重的問題了嗎?我已經認為header數據發生完畢了,然后過一會,你發過來一個header數據,我該怎么處理啊,全亂了。
所以同一個stream里的幀數據是嚴格要求順序的發送的,不可以亂序發送的,時序問題由tcp的順序性保證(序列號)。
- 幀頭的最后 4 個字節是
流標識符
(Stream ID),最高位被保留不用,只有 31 位可以使用,因此流標識符的最大值是 2^31,大約是 21 億,它的作用是用來標識該Frame
屬于哪個Stream
,接收方可以根據這個信息從亂序的幀(這里的亂序是指所屬不同Stream的Fream是亂序的,但是同一個Stream內的Fream肯定是有序的)里找到相同 Stream ID 的幀,從而有序組裝信息。
如果你不懂Stream和Fream的關系,那么繼續往下看
并發傳輸
知道了 HTTP/2 的幀結構
后,我們再來看看它是如何實現并發傳輸的。
我們都知道 HTTP/1.1 的實現是基于請求-響應模型的。同一個TCP連接中
,HTTP 完成一個事務(請求與響應),才能處理下一個事務,也就是說在發出請求等待響應的過程中,是沒辦法做其他事情的,如果響應遲遲不來,那么后續的請求是無法發送的,也造成了隊頭阻塞
的問題,這也是http1.1的性能關鍵。
而 HTTP/2 就很牛逼了,引出了 Stream
概念,多個 Stream 可復用在一條 TCP 連接。實現了在同一個TCP連接上可以并發多個請求和響應。
為了理解 HTTP/2 的并發是怎樣實現的,我們先來理解 HTTP/2 中的 Stream、Message、Frame 這 3 個概念。
你可以從上圖中看到:
- 1 個 TCP 連接包含一個或者多個 Stream,Stream 是 HTTP/2 并發的關鍵技術。一個TCP連接由相同的四元組組成,即源ip、源端口、目標ip和目標端口,也就是客戶端并發請求,服務端也可以并發響應;
- Stream 里可以包含 1 個或多個 Message,Message 對應 HTTP/1 中的請求或響應,由 HTTP 頭部和包體構成;
- Message 里包含一條或者多個 Frame,Frame 是 HTTP/2 最小單位,以二進制壓縮格式存放 HTTP/1 中的內容(頭部和包體);
我估計你看到這里還是云里霧里的,為什么Stream能實現并發傳輸呢?Stream到底是個什么呢?
想知道這個問題的答案,先來回顧下http1.1中開啟管道傳輸之后為什么只支持客戶端并發請求而不支持服務端的并發響應呢?根本原因就是服務端的響應和客戶端的請求對應不上,只能根據請求和響應的順序來匹配。
而Stream
其實是一個唯一的ID標識
,在同一個tcp連接中的 Stream ID 是不能復用的,只能順序遞增
,Stream
可以理解為并不是一個真實存在的東西,就是一個唯一的自增ID。
我們知道幀數據Fream
是數據傳輸的基本單位,而Fream
結構中有個流標識符
是用來表示該Fream
所屬的Stream ID
。
客戶端的每次請求都會分配一個唯一的Stream ID
。比如客戶端發出一個請求,Stream ID為123
,而請求體中Header+Body
會被分割成多個Fream
,且這些Fream
的標識符都將是123
;當服務端響應數據時,也會將響應體中的Header+Body
分割成多個Fream
,且這些響應體的所有Fream
的標識符也同樣是123
。
看到這里,我想你應該明白了Stream是怎么實現并發傳輸的了吧。http1.1中由于不知道返回的響應是屬于哪個請求的,所以只能默認按照順序匹配。而http2.0中請求和響應共用一個Stream ID,這樣就可以將請求和響應進行精準匹配啦。
因此,我們可以得出個結論:多個 Stream 跑在一條 TCP 連接,同一個 HTTP 請求與響應是跑在同一個 Stream 中,HTTP 消息可以由多個 Frame 構成, 一個 Frame 可以由多個 TCP 報文構成。
在 HTTP/2 連接上,不同 Stream 的幀是可以亂序發送的(因此可以并發不同的 Stream )
,因為每個幀的頭部會攜帶 Stream ID 信息,所以接收端可以通過 Stream ID 有序組裝成 HTTP 消息,而同一 Stream 內部的幀必須是嚴格有序的
,在上面幀數據結構介紹標志位
的說明為什么必須保持有序。
比如下圖,服務端并行亂序
的地發送了兩個響應: Stream 1 和 Stream 3,這兩個 Stream 都是跑在一個 TCP 連接上,客戶端收到后,會根據相同的 Stream ID 有序組裝成 HTTP 消息,并將組裝完成的消息發送給相同Stream ID 的請求。
客戶端和服務器雙方都可以建立 Stream,因為
服務端可以主動推送資源給客戶端
。
客戶端建立的 Stream 必須是奇數號
,而服務器建立的 Stream 必須是偶數號
。
同一個連接
中的 Stream ID 是不能復用
的,只能順序遞增。從幀結構
中可以發現,流標識符
(Stream ID),只有 31 位可以使用,因此流標識符的最大值是 2^31,大約是 21 億
,所以當 Stream ID 耗盡時,需要發一個控制幀 GOAWAY
,用來優雅關閉 TCP 連接。
服務器主動推送資源
HTTP/1.1 不支持服務器主動推送資源給客戶端,都是由客戶端向服務器發起請求后,才能獲取到服務器響應的資源。
比如,客戶端通過 HTTP/1.1 請求從服務器那獲取到了 HTML 文件,而 HTML 可能還需要依賴 CSS 來渲染頁面,這時客戶端還要再發起獲取 CSS 文件的請求,需要兩次消息往返,如下圖左邊部分:
如上圖右邊部分,在 HTTP/2 中,客戶端在訪問 HTML 時,服務器可以直接主動推送 CSS 文件,減少了消息傳遞的次數,
減少了網絡耗時
。
在 Nginx 中,如果你希望客戶端訪問 /test.html 時,服務器直接推送 /test.css,那么可以這么配置:
location /test.html {
http2_push /test.css;
}
http2.0存在的缺點
http2.0相對于http1.1性能確實提升了很多,但是仍然還存在一些問題
- 對頭阻塞
- TCP和TLS的握手延時
- 網絡遷移需要重新建立連接
TCP 對頭阻塞
HTTP/2 多個請求是跑在一個 TCP 連接中的,那么當 TCP 丟包時,整個 TCP 都要等待重傳,那么就會阻塞該 TCP 連接中的所有請求。
比如下圖中,Stream 2 有一個 TCP 報文丟失了,那么即使收到了 Stream 3 和 Stream 4 的 TCP 報文,應用層也是無法讀取讀取的,相當于阻塞了 Stream 3 和 Stream 4 請求。
因為HTTP2.0是基于TCP 是字節流協議,TCP 層必須保證收到的字節數據是完整且有序的,如果序列號較低的 TCP 段在網絡傳輸中丟失了,即使序列號較高的 TCP 段已經被接收了,也會被阻塞在
傳輸層
,應用層
也無法從內核中讀取到這部分數據,只有當所有的TCP段都被接受成功之后,才會將數據組裝之后發送給應用層
,應用層
才會讀取數據。
可以發現出現阻塞的本質原因是:阻塞出現在了傳輸層,而導致應用層讀不到數據。
那么如果讓阻塞出現在應用層而不要出現在網絡層是不是就可以避免對頭阻塞了,可以看下http3.0的實現。
可以發現Http2.0出現對頭阻塞的場景只發送在Tcp丟包
的場景下。
可以發現http2.0不管怎么優化也解決不了TCP的對頭阻塞
問題,除非把TCP協議換了, 這是TCP 協議固有的問題。可以接續往下看http3.0是怎么解決TCP對頭阻塞的。
TCP和TLS的握手延時
發起 HTTP 請求時,需要經過 TCP 三次握手
和 TLS 四次握手
(TLS 1.2)的過程,因此共需要 3 個 RTT 的時延才能發出請求數據。
另外,TCP 由于具有「擁塞控制」
的特性,所以剛建立連接的 TCP 會有個「慢啟動」
的過程,它會對 TCP 連接產生“減速”效果,給人的感覺就是突然卡頓了一下。
網絡遷移需要重新建立連接
一個 TCP 連接
是由四元組
(源 IP 地址,源端口,目標 IP 地址,目標端口)確定的,這意味著如果 IP 地址或者端口變動了,就會導致需要 TCP 與 TLS 重新握手,這不利于移動設備切換網絡的場景,比如 4G 網絡環境切換成 WiFi。
這些問題都是 TCP 協議固有的問題,無論應用層的 HTTP/2 在怎么設計都無法逃脫。要解決這個問題,就必須把傳輸層協議替換成 UDP,這個大膽的決定,HTTP/3 做了!
http3.0
http2.0有3個缺陷,http3.0都給解決了,接下來看下是怎么一步一步解決的。
可以發現http3.0有幾個特點
- 傳輸層由TCP改用成UDP協議。我們深知,UDP 是一個簡單、不可靠的傳輸協議,而且是 UDP 包之間是無序的,也沒有依賴關系。而且,UDP 是不需要連接的,也就不需要握手和揮手的過程,所以天然的就比 TCP 快。
-
應用層
新增QUIC
協議: 雖然UDP是不具備可靠性等特性的,但是UDP的上層協議QUID
,它具有類似 TCP 的連接管理、擁塞窗口、流量控制的網絡特性,相當于將不可靠傳輸的 UDP 協議變成“可靠”的了,所以不用擔心數據包丟失的問題。 - TLS協議內置在了
QUIC
協議中。避免了TLS的4次握手。 - 由HTTP2.0中的
HPACK
編碼格式改為了QPACK
編碼格式。
無隊頭阻塞
QUIC 協議
也有類似 HTTP/2 Stream 與多路復用
的概念,也是可以在同一條連接上并發傳輸多個 Stream的,這個優點http3.0是繼承了的。
在來回憶下,為什么HTTP2.0會有TCP對頭阻塞?
主要原因是:當tcp發送丟包時,tcp所在的傳輸層會發生阻塞,進而應用層也發生阻塞。只有組裝完成所有tcp段之后,應用層才會讀取。
而上面的http3.0介紹中TCP改成了UDP,而UDP并不是對數據的連續性,時序性等問題進行驗證,所以傳輸層肯定是不會發生阻塞了。
因為UDP是不可靠的,所以在應用層
實現了QUIC
協議來保證消息的可靠性。http2.0和http3.0對比可發現一點:阻塞發生的層級由http2.0的傳輸層變更到了http3.0的應用層
。正是由于這一變更,如果http3.0請求時發生丟包則應用層只會阻塞本請求,后續的請求是不會阻塞的,應用層都是可以讀取到的;同理,如果ttp3.0響應時發生丟包則應用層也只會阻塞本響應,后續的響應是不會阻塞的,應用層都是可以讀取到的。
所以,QUIC 連接上的多個 Stream 之間并沒有依賴,都是獨立的,某個流發生丟包了,只會影響該流,其他流不受影響。
更快的連接建立
對于 HTTP/1 和 HTTP/2 協議,TCP 和 TLS 是分層的,分別屬于內核實現的傳輸層、OpenSSL 庫實現的表示層,因此它們難以合并在一起,需要分批次來握手,先 3次
TCP 握手,再 4次
TLS 握手。
HTTP/3 在傳輸數據前雖然需要 QUIC 協議握手,這個握手過程只需要 1 RTT
,握手的目的是為確認雙方的「連接 ID」,連接遷移就是基于連接 ID 實現的。
但是 HTTP/3 的 QUIC 協議并不是與 TLS 分層
,而是 QUIC 內部包含了 TLS
,它在自己的幀會攜帶 TLS 里的“記錄”,再加上 QUIC 使用的是 TLS 1.3,因此僅需 1 個 RTT
就可以「同時」完成建立連接與密鑰協商,甚至在第二次連接的時候,應用數據包可以和 QUIC 握手信息(連接信息 + TLS 信息)一起發送,達到0-RTT
的效果。
至于為什么第二次連接時為 0-RTT
,接著繼續看下面。
網絡遷移
在前面我們提到,基于 TCP 傳輸協議的 HTTP 協議,由于是通過四元組(源 IP、源端口、目的 IP、目的端口)確定一條 TCP 連接。
那么當移動設備的網絡從 4G 切換到 WiFi 時,意味著 IP 地址變化了,那么就必須要斷開連接,然后重新建立連接,而建立連接的過程包含 TCP 三次握手和 TLS 四次握手的時延,以及 TCP 慢啟動的減速過程,給用戶的感覺就是網絡突然卡頓了一下,因此連接的遷移成本是很高的。
而 QUIC 協議沒有用四元組
的方式來“綁定”連接
,而是通過連接 ID
來標記通信的兩個端點
,客戶端和服務器可以各自選擇一組 ID 來標記自己,因此即使移動設備的網絡變化后,導致 IP 地址變化了,只要仍保有上下文信息(比如連接 ID、TLS 密鑰等),就可以“無縫”地復用原連接,消除重連的成本,沒有絲毫卡頓感,達到了連接遷移的功能。