一文讀懂 HTTP/1HTTP/2HTTP/3
作者:charryhuang,騰訊 CSIG 前端開發工程師
從 1989 年全球資訊網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式釋出。2020 年 HTTP3 或能正式使用。以下將會簡單介紹。
HTTP1.1 與 HTTP2
HTTP1.1 的缺陷
高延遲 — 隊頭阻塞(Head-Of-Line Blocking)
無狀態特性 — 阻礙互動
明文傳輸 — 不安全性
不支援服務端推送
隊頭阻塞
隊頭阻塞是指當順序傳送的請求序列中的一個請求因為某種原因被阻塞時,在後面排隊的所有請求也一併被阻塞,會導致客戶端遲遲收不到資料。
針對隊頭阻塞:
1.將同一頁面的資源分散到不同域名下,提升連線上限。雖然能公用一個 TCP 管道,但是在一個管道中同一時刻只能處理一個請求,在當前的請求沒有結束之前,其他的請求只能處於阻塞狀態。
2.減少請求數量
3.內聯一些資源:css、base64 圖片等
4.合併小檔案減少資源數
無狀態特性
無狀態是指協議對於連線狀態沒有記憶能力。純淨的 HTTP 是沒有 cookie 等機制的,每一個連線都是一個新的連線。上一次請求驗證了使用者名稱密碼,而下一次請求伺服器並不知道它與上一條請求有何關聯,換句話說就是掉登入態。
不安全性
傳輸內容沒有加密,中途可能被篡改和劫持。
SPDY 協議
SPDY 是由 google 推行的改進版本的 HTTP1.1 (那時候還沒有 HTTP2)。
特性:
多路複用 — 解決隊頭阻塞
頭部壓縮 — 解決巨大的 HTTP 頭部
請求優先順序 — 先獲取重要資料
服務端推送 — 填補空缺
提高安全性
多路複用
SPDY 允許在一個連線上無限制併發流。因為請求在一個通道上,TCP 效率更高(參考 TCP 擁塞控制 中的慢啟動)。更少的網路連線,發出更密集的包。
頭部壓縮
使用專門的 HPACK 演算法,每次請求和響應只傳送差異頭部,一般可以達到 50% ~90% 的高壓縮率。
請求優先順序
雖然無限的併發流解決了隊頭阻塞的問題,但如果頻寬受限,客戶端可能會因防止堵塞通道而阻止請求。在網路通道被非關鍵資源堵塞時,高優先順序的請求會被優先處理。
服務端推送
服務端推送(ServerPush),可以讓服務端主動把資原始檔推送給客戶端。當然客戶端也有權利選擇是否接收。
提高安全性
支援使用 HTTPS 進行加密傳輸。
HTTP2
HTTP2 基於 SPDY,專注於效能,最大的一個目標是在使用者和網站間只用一個連線。
新增特性:
二進位制分幀 - HTTP2 效能增強的核心
多路複用 - 解決序列的檔案傳輸和連線數過多
二進位制分幀
首先,HTTP2 沒有改變 HTTP1 的語義,只是在應用層使用二進位制分幀方式傳輸。因此,也引入了新的通訊單位:幀、訊息、流。
分幀有什麼好處?伺服器單位時間接收到的請求數變多,可以提高併發數。最重要的是,為多路複用提供了底層支援。
多路複用
一個域名對應一個連線,一個流代表了一個完整的請求-響應過程。幀是最小的資料單位,每個幀會標識出該幀屬於哪個流,流也就是多個幀組成的資料流。多路複用,就是在一個 TCP 連線中可以存在多個流。演示
HTTP2 的缺陷
TCP 以及 TCP+TLS 建立連線的延時
TCP 的隊頭阻塞並沒有徹底解決
多路複用導致伺服器壓力上升
多路複用容易 Timeout
建連延時
TCP 連線需要和伺服器進行三次握手,即消耗完 1.5 個 RTT 之後才能進行資料傳輸。
TLS 連線有兩個版本—— TLS1.2 和 TLS1.3,每個版本建立連線所花的時間不同,大致需要 1~2 個 RTT。
RTT(Round-Trip Time):往返時延。表示從傳送端傳送資料開始,到傳送端收到來自接收端的確認(接收端收到資料後便立即傳送確認),總共經歷的時延。
隊頭阻塞沒有徹底解決
TCP 為了保證可靠傳輸,有一個“超時重傳”機制,丟失的包必須等待重傳確認。HTTP2 出現丟包時,整個 TCP 都要等待重傳,那麼就會阻塞該 TCP 連線中的所有請求。
RTO:英文全稱是 Retransmission TimeOut,即重傳超時時間;RTO 是一個動態值,會根據網路的改變而改變。RTO 是根據給定連線的往返時間 RTT 計算出來的。接收方返回的 ack 是希望收到的下一組包的序列號。
多路複用導致伺服器壓力上升
多路複用沒有限制同時請求數。請求的平均數量與往常相同,但實際會有許多請求的短暫爆發,導致瞬時 QPS 暴增。
多路複用容易 Timeout
大批次的請求同時傳送,由於 HTTP2 連線記憶體在多個並行的流,而網路頻寬和伺服器資源有限,每個流的資源會被稀釋,雖然它們開始時間相差更短,但卻都可能超時。
即使是使用 Nginx 這樣的負載均衡器,想正確進行節流也可能很棘手。其次,就算你嚮應用程式引入或調整排隊機制,但一次能處理的連線也是有限的。如果對請求進行排隊,還要注意在響應超時後丟棄請求,以避免浪費不必要的資源。引用
QUIC
簡介
Google
在推 SPDY 的時候就已經意識到了這些問題,於是就另起爐灶搞了一個基於 UDP 協議的 QUIC 協議。而這個就是 HTTP3。它真正“完美”地解決了“隊頭阻塞”問題。
主要特點
改進的擁塞控制、可靠傳輸
快速握手
整合了 TLS 1.3 加密
多路複用
連線遷移
改進的擁塞控制、可靠傳輸
從擁塞演算法和可靠傳輸本身來看,QUIC 只是按照 TCP 協議重新實現了一遍,那麼 QUIC 協議到底改進在哪些方面呢?主要有如下幾點:
1. 可插拔 — 應用程式層面就能實現不同的擁塞控制演算法。
一個應用程式的不同連線也能支援配置不同的擁塞控制。應用程式不需要停機和升級就能實現擁塞控制的變更,可以針對不同業務,不同網路制式,甚至不同的 RTT,使用不同的擁塞控制演算法。
關於應用層的可插拔擁塞控制模擬,可以對 socket 上的流為物件進行實驗。
2. 單調遞增的 Packet Number — 使用 Packet Number 代替了 TCP 的 seq。
每個 Packet Number 都嚴格遞增,也就是說就算 Packet N 丟失了,重傳的 Packet N 的 Packet Number 已經不是 N,而是一個比 N 大的值。而 TCP 重傳策略存在二義性,比如客戶端傳送了一個請求,一個 RTO 後發起重傳,而實際上伺服器收到了第一次請求,並且響應已經在路上了,當客戶端收到響應後,得出的 RTT 將會比真實 RTT 要小。當 Packet N 唯一之後,就可以計算出正確的 RTT。
3. 不允許 Reneging — 一個 Packet 只要被 Ack,就認為它一定被正確接收。
Reneging 的意思是,接收方有權把已經報給傳送端 SACK(Selective Acknowledgment) 裡的資料給丟了(如接收視窗不夠而丟棄亂序的包)。
QUIC 中的 ACK 包含了與 TCP 中 SACK 等價的資訊,但 QUIC 不允許任何(包括被確認接受的)資料包被丟棄。這樣不僅可以簡化傳送端與接收端的實現難度,還可以減少傳送端的記憶體壓力。
4. 前向糾錯(FEC)
早期的 QUIC 版本存在一個丟包恢復機制,但後來由於增加頻寬消耗和效果一般而廢棄。FEC 中,QUIC 資料幀的資料混合原始資料和冗餘資料,來確保無論到達接收端的 n 次傳輸內容是什麼,接收端都能夠恢復所有 n 個原始資料包。FEC 的實質就是異或。示意圖:
5. 更多的 Ack 塊和增加 Ack Delay 時間。
QUIC 可以同時提供 256 個 Ack Block,因此在重排序時,QUIC 相對於 TCP(使用 SACK)更有彈性,這也使得在重排序或丟失出現時,QUIC 可以在網路上保留更多的在途位元組。在丟包率比較高的網路下,可以提升網路的恢復速度,減少重傳量。
TCP 的 Timestamp 選項存在一個問題:傳送方在傳送報文時設定傳送時間戳,接收方在確認該報文段時把時間戳欄位值複製到確認報文時間戳,但是沒有計算接收端接收到包到傳送 Ack 的時間。這個時間可以簡稱為 Ack Delay,會導致 RTT 計算誤差。現在就是把這個東西加進去計算 RTT 了。
6. 基於 stream 和 connection 級別的流量控制。
為什麼需要兩類流量控制呢?主要是因為 QUIC 支援多路複用。Stream 可以認為就是一條 HTTP 請求。Connection 可以類比一條 TCP 連線。多路複用意味著在一條 Connetion 上會同時存在多條 Stream。
QUIC 接收者會通告每個流中最多想要接收到的資料的絕對位元組偏移。隨著資料在特定流中的傳送,接收和傳送,接收者傳送 WINDOW_UPDATE 幀,該幀增加該流的通告偏移量限制,允許對端在該流上傳送更多的資料。
除了每個流的流控制外,QUIC 還實現連線級的流控制,以限制 QUIC 接收者願意為連線分配的總緩衝區。連線的流控制工作方式與流的流控制一樣,但傳送的位元組和最大的接收偏移是所有流的總和。
最重要的是,我們可以在記憶體不足或者上游處理效能出現問題時,透過流量控制來限制傳輸速率,保障服務可用性。
快速握手
由於 QUIC 是基於 UDP 的,所以 QUIC 可以實現 0-RTT 或者 1-RTT 來建立連線,可以大大提升首次開啟頁面的速度。
整合了 TLS 1.3 加密
TLS 1.3 支援 3 種基本金鑰交換模式:
(EC)DHE (基於有限域或橢圓曲線的 Diffie-Hellman)
PSK - only
PSK with (EC)DHE
在完全握手情況下,需要 1-RTT 建立連線。TLS1.3 恢復會話可以直接傳送加密後的應用資料,不需要額外的 TLS 握手,也就是 0-RTT。
TLS 1.3 0-RTT 簡單原理示意(基於 DHE):
但是 TLS1.3 也並不完美。TLS 1.3 的 0-RTT 無法保證前向安全性(Forward secrecy)。簡單講就是,如果當攻擊者透過某種手段獲取到了 Session Ticket Key,那麼該攻擊者可以解密以前的加密資料。
要緩解該問題可以透過設定使得與 Session Ticket Key 相關的 DH 靜態引數在短時間內過期(一般幾個小時)。
多路複用
QUIC 是為多路複用從頭設計的,攜帶個別流的的資料的包丟失時,通常隻影響該流。QUIC 連線上的多個 stream 之間並沒有依賴,也不會有底層協議限制。假如 stream2 丟了一個包,也只會影響 stream2 的處理。
連線遷移
TCP 是按照 4 要素(客戶端 IP、埠, 伺服器 IP、埠)確定一個連線的。而 QUIC 則是讓客戶端生成一個 Connection ID (64 位)來區別不同連線。只要 Connection ID 不變,連線就不需要重新建立,即便是客戶端的網路發生變化。由於遷移客戶端繼續使用相同的會話金鑰來加密和解密資料包,QUIC 還提供了遷移客戶端的自動加密驗證。
挑戰
NAT 問題
NAT 概念
為了解決 IP 地址不足的問題,NAT 給一個區域網路只分配一個 IP 地址,這個網路內的主機,則分配私有地址,這些私有地址對外是不可見的,他們對外的通訊都要藉助那個唯一分配的 IP 地址。所有離開本地網路去往 Internet 的資料包的源 IP 地址需替換為相同的 NAT,區別僅在於埠號不同。
原因
TCP 和 UDP 的報文頭部不同導致 NAT 問題的出現。
NAT 裝置的埠記憶問題
對於基於 TCP 的 HTTP、HTTPS 傳輸,NAT 裝置可以根據 TCP 報文頭的 SYN/FIN 狀態位,知道通訊什麼時候開始,什麼時候結束,對應記憶 NAT 對映的開始和結束。
但是基於 UDP 傳輸的 HTTP3 ,不存在 SYN/FIN 狀態位。NAT 裝置的記憶如果短於使用者會話時間,則使用者會話會中斷。NAT 裝置的記憶時間如果長於使用者會話時間,則意味著 NAT 裝置的埠資源會被白白佔用。
最直接的解決方案是,在 QUIC 的頭部模仿 TCP 的 SYN/FIN 狀態,讓沿途的 NAT 裝置知道會話什麼時候開始、什麼時候結束。但這需要升級全球所有的 NAT 裝置的軟體。
另外一個可行的方案是,讓 QUIC 週期性地傳送 Keepalive 訊息,重新整理 NAT 裝置的記憶,避免 NAT 裝置自動釋放。
NAT 裝置禁用 UDP
在一些 NAT 網路環境下(如某些校園網),UDP 協議會被路由器等中間網路裝置禁止,這時客戶端會直接降級,選擇 HTTPS 等備選通道,保證正常業務請求。
NGINX 負載均衡問題概念
QUIC 客戶端存在網路制式切換,就算是同一個移動機房,可能第一次業務請求時會落到 A 這臺伺服器,後續再次連線,就會落到 B 例項上,重複走 1-RTT 的完整握手流程。
全域性握手快取
為所有 QUIC 伺服器例項建立一個全域性握手快取。當使用者網路發生切換時,下一次的業務請求無論是落到哪一個機房或哪一臺例項上,握手建連都會是 0-RTT。
歷代 HTTP 速度測試
結尾
從古至今實時資料傳輸(音訊、影片、遊戲等)都面臨卡頓、延遲等問題,而 QUIC 基於 UDP 的架構和改進的重傳等特性,能夠有效的提升使用者體驗。目前
B 站 也已經接入 QUIC。
如果想要自己體驗 QUIC,可以使用 Libquic、Caddy 等。另外 github 上面也有 C++版本的 QUIC 實現,利用 Nodejs 的 C++ 模組,前端工程師也可以快速實現一個 node-quic。
參考資料
http2.0 原理詳細分析
HPACK: HTTP/2 裡的沉默殺手
QPACK:HTTP /3 的頭壓縮
DH 演算法
前向安全(ForwardSecrecy)
TLS 1.3 VS TLS 1.2,讓你明白 TLS 1.3 的強大
CaddyWeb 伺服器 QUIC 部署
關於 QUIC 的各種嘗試
使用 QUIC 協議實現實時影片直播 0 卡頓
解密 HTTP/2 與 HTTP/3 的新特性
Web通訊協議,你還需要知道:SPDY 和 QUIC
如何看待 HTTP/3 ?
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559354/viewspace-2673354/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 一文讀懂HTTP/2及HTTP/3特性HTTP
- 一文讀懂HTTP/2 及 HTTP/3特性HTTP
- 一文讀懂Go Http Server原理GoHTTPServer
- 快速讀懂 HTTP/3 協議HTTP協議
- 一文讀懂常見HTTP狀態碼HTTP
- 一文讀懂所有HTTP狀態碼含義HTTP
- 一文讀懂mavenMaven
- 一文讀懂ServletServlet
- 一文讀懂 NPM 版本NPM
- 一文讀懂 Data Mesh
- 一文讀懂Ka/Ks
- 一文讀懂微核心
- 一文讀懂 Apache PulsarApache
- 一文讀懂eBPF/XDPeBPF
- 一文讀懂特徵工程特徵工程
- 從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路HTTP協議
- 一文讀懂“負載均衡”負載
- 一文讀懂野指標指標
- 一文讀懂web組態Web
- 一文讀懂:GBDT梯度提升梯度
- 一文讀懂「雲託管」
- 一文讀懂Lua元表
- 一文讀懂 Kubernetes APIServer 原理APIServer
- 一文讀懂Spring整合RedisSpringRedis
- 一文讀懂擁塞控制
- 一文讀懂支付系統
- 一文讀懂前端快取前端快取
- 【Flutter】一文讀懂混入類MixinFlutter
- 一文讀懂元宇宙的特徵元宇宙特徵
- 一文讀懂git核心工作原理Git
- 一文讀懂Kafka副本機制Kafka
- 一文讀懂鏈路追蹤
- 一文讀懂Apache Flink技術Apache
- JVM(2)--一文讀懂垃圾回收JVM
- 一文讀懂系列-JVM垃圾收集JVM
- 解讀HTTP/3HTTP
- 真正“搞”懂HTTP協議14之HTTP3HTTP協議
- 選擇 K3s 還是 RKE2?一文讀懂!