跳至主要內容
Skip to content

HTTP/2 核心:二進位分幀與多路複用

HTTP/2 是 HTTP 協定的一次重大革新。它從根本上改變了資料傳輸的方式,從文字協定變成二進位協定,從串行變成真正的並行。本篇將深入解析這些核心變革。


一、 從文字到二進位

1.1 HTTP/1.1 的文字協定

HTTP/1.1 使用純文字格式,人類可讀:

http
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: Mozilla/5.0...\r\n
Accept: text/html\r\n
\r\n

問題

問題說明
解析複雜需要逐字元掃描尋找分隔符
效率低文字比二進位佔用更多空間
歧義可能空格、換行處理可能不一致

1.2 HTTP/2 的二進位協定

HTTP/2 將所有資料封裝成幀(Frame)

HTTP/2 Frame:
┌─────────────────────────────────────┐
│ Length (24 bits)                    │
├─────────────────────────────────────┤
│ Type (8 bits) │ Flags (8 bits)      │
├─────────────────────────────────────┤
│ R │ Stream Identifier (31 bits)    │
├─────────────────────────────────────┤
│ Frame Payload (variable length)    │
└─────────────────────────────────────┘

優點

優點說明
解析簡單固定格式,直接讀取位元組
更緊湊二進位編碼更節省空間
無歧義明確的欄位長度
可擴展新增幀類型不破壞相容性

二、 幀(Frame)結構

2.1 幀標頭(9 bytes)

每個幀都有 9 位元組的標頭:

欄位大小說明
Length24 bitsPayload 長度(最大 16MB)
Type8 bits幀類型
Flags8 bits類型相關的標誌
R1 bit保留位(必須為 0)
Stream ID31 bits串流識別碼

2.2 幀類型

HTTP/2 定義了 10 種幀類型:

Type名稱用途
0x00DATA傳輸請求/回應 Body
0x01HEADERS傳輸標頭
0x02PRIORITY設定串流優先級
0x03RST_STREAM終止串流
0x04SETTINGS連線設定
0x05PUSH_PROMISE伺服器推送預告
0x06PING連線健康檢查
0x07GOAWAY優雅關閉連線
0x08WINDOW_UPDATE流量控制
0x09CONTINUATION延續 HEADERS

2.3 常見幀類型詳解

DATA 幀

傳輸實際的請求或回應內容:

DATA Frame:
┌─────────────────────────────────────┐
│ Pad Length? (8 bits)               │
├─────────────────────────────────────┤
│ Data (*)                           │
├─────────────────────────────────────┤
│ Padding (*)                        │
└─────────────────────────────────────┘

Flags:
- END_STREAM (0x1): 這是最後一個幀
- PADDED (0x8): 有填充資料

HEADERS 幀

傳輸壓縮後的 HTTP 標頭:

HEADERS Frame:
┌─────────────────────────────────────┐
│ Pad Length? (8 bits)               │
├─────────────────────────────────────┤
│ E │ Stream Dependency (31 bits)    │
├─────────────────────────────────────┤
│ Weight (8 bits)                    │
├─────────────────────────────────────┤
│ Header Block Fragment (*)          │
├─────────────────────────────────────┤
│ Padding (*)                        │
└─────────────────────────────────────┘

Flags:
- END_STREAM (0x1): 沒有 DATA 幀
- END_HEADERS (0x4): 標頭完整
- PADDED (0x8): 有填充
- PRIORITY (0x20): 有優先級資訊

三、 串流(Stream)概念

3.1 什麼是串流?

串流是 HTTP/2 中的虛擬通道,每個請求/回應配對使用一個獨立的串流:

3.2 串流 ID 規則

類型ID 規則範例
客戶端發起奇數1, 3, 5, 7...
伺服器發起偶數2, 4, 6, 8...
控制訊息0SETTINGS, PING

3.3 串流狀態機

3.4 串流生命週期範例


四、 多路複用(Multiplexing)

4.1 HTTP/1.1 的問題回顧

HTTP/1.1 即使有持久連線,一次也只能處理一個請求:

4.2 HTTP/2 的多路複用

HTTP/2 在單一連線上並行處理多個串流:

關鍵:回應可以亂序返回,快的先到!

4.3 幀交錯傳輸

實際上,不同串流的幀會交錯傳輸:

傳輸序列:
[HEADERS S1] [HEADERS S3] [DATA S3] [HEADERS S5]
[DATA S1 part1] [DATA S5] [DATA S1 part2] [DATA S1 END]

藍色 = Stream 1, 綠色 = Stream 3, 橘色 = Stream 5


五、 解決隊頭阻塞

5.1 HTTP/1.1 的隊頭阻塞

5.2 HTTP/2 解決方案

快的請求不用等慢的!

5.3 但仍有 TCP 層阻塞

WARNING

HTTP/2 解決了應用層的隊頭阻塞,但傳輸層(TCP)的阻塞仍然存在。

如果一個 TCP 封包丟失,整個連線都要等待重傳。這是 HTTP/3 改用 QUIC 的原因。


六、 連線建立

6.1 升級方式

HTTP/2 有兩種建立方式:

1. TLS + ALPN(主流)

2. 明文升級(較少見)

http
GET / HTTP/1.1
Host: example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64 設定>

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

<HTTP/2 連線開始>

6.2 連線前言

客戶端必須發送魔術字串作為連線開始:

PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

接著發送 SETTINGS 幀。


七、 設定協商

7.1 SETTINGS 幀

連線開始時交換設定:

設定預設值說明
HEADER_TABLE_SIZE4096HPACK 表大小
ENABLE_PUSH1允許 Server Push
MAX_CONCURRENT_STREAMS無限最大並行串流數
INITIAL_WINDOW_SIZE65535初始流控視窗
MAX_FRAME_SIZE16384最大幀大小
MAX_HEADER_LIST_SIZE無限最大標頭大小

7.2 設定交換流程


八、 實際封包觀察

使用 Chrome DevTools 或 Wireshark 可以觀察 HTTP/2 通訊:

Chrome DevTools

Protocol 欄位顯示 h2
可看到所有請求使用同一個連線 ID

curl 測試

bash
# 觀察 HTTP/2 通訊
curl -v --http2 https://example.com

# 輸出包含:
# * Using HTTP2, server supports multiplexing
# * Connection state changed (HTTP/2 confirmed)
# * ALPN, negotiated h2

總結

概念HTTP/1.1HTTP/2
格式文字二進位
傳輸單位請求/回應幀(Frame)
並行處理多連線多路複用
隊頭阻塞應用層解決
連線數每域名 6 個1 個

> **記住 HTTP/2 的三大核心**:

  1. 二進位分幀:高效解析
  2. 串流:請求/回應的虛擬通道
  3. 多路複用:單連線並行處理

進階挑戰

  1. 使用 Chrome DevTools 的 Network 面板,觀察一個 HTTP/2 網站的請求,確認所有請求使用同一個連線。
  2. 使用 nghttp 命令列工具發送 HTTP/2 請求,觀察幀的交換過程。
  3. 思考:為什麼 HTTP/2 推薦只使用一個 TCP 連線,而不是像 HTTP/1.1 那樣開多個?

延伸閱讀與資源