Skip to content

HTTP/2 & HTTP/3

HTTP 协议演进

HTTP/1.0 (1996)     HTTP/1.1 (1997)      HTTP/2 (2015)       HTTP/3 (2022)
─────────────       ─────────────        ──────────          ──────────
每个请求一个连接     Keep-Alive 复用      多路复用             QUIC (UDP)
无 Host 头          管线化(pipelining)    二进制分帧           无队头阻塞
无缓存控制          chunked 传输          头部压缩(HPACK)      0-RTT 连接
                    Cache-Control        Server Push          连接迁移
                    队头阻塞             流优先级              内建 TLS 1.3

HTTP/1.1 的核心问题

队头阻塞(Head-of-Line Blocking)

HTTP/1.1 在同一个 TCP 连接上必须串行处理请求——前一个响应完成后才能发送下一个:

HTTP/1.1 单连接:

请求1 ──→ 响应1(200ms)
                        请求2 ──→ 响应2(50ms)
                                              请求3 ──→ 响应3(100ms)
                                              
总时间:200 + 50 + 100 = 350ms

如果响应1很慢(500ms),所有后续请求都被阻塞:
请求1 ──→ ............响应1(500ms)
                                    请求2 ──→ 响应2
                                                    请求3 ──→ 响应3

浏览器的应对策略

Chrome 对同一域名最多开 6 个 TCP 连接:

连接1: 请求1 ──→ 响应1    请求7 ──→ 响应7
连接2: 请求2 ──→ 响应2    请求8 ──→ 响应8
连接3: 请求3 ──→ 响应3
连接4: 请求4 ──→ 响应4
连接5: 请求5 ──→ 响应5
连接6: 请求6 ──→ 响应6

问题:
  - 6 个连接仍然不够(一个页面可能有 50+ 个资源)
  - 每个连接都要 TCP + TLS 握手(延迟高)
  - 占用服务器连接资源
  
HTTP/1.1 时代的优化手段(现在大多已过时):
  - 域名分片(cdn1.example.com, cdn2.example.com)
  - 资源合并(CSS Sprites, Bundle 所有 JS)
  - 内联小资源(Base64 图片, 内联 CSS)

头部冗余

HTTP/1.1 的 Header 是纯文本不压缩,每次请求都重复发送:

GET /api/users HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Cookie: session_id=abc123; _ga=GA1.2.xxx; _gid=GA1.2.yyy; token=eyJhb...
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Connection: keep-alive

→ 每个请求 ~800 字节的 Header
→ 100 个请求 = ~80KB 纯 Header 流量(大部分是重复内容)

HTTP/2 核心特性

1. 二进制分帧层(Binary Framing Layer)

HTTP/2 最根本的变化——在应用层(HTTP)和传输层(TCP)之间增加了二进制分帧层:

HTTP/1.1:纯文本协议
  GET /index.html HTTP/1.1\r\n
  Host: example.com\r\n
  \r\n

HTTP/2:二进制帧(Frame)
  ┌───────────────────────────┐
  │ Length (24 bit)           │  帧长度
  │ Type (8 bit)             │  帧类型(HEADERS / DATA / ...)
  │ Flags (8 bit)            │  标志位
  │ Stream ID (31 bit)       │  流标识符
  ├───────────────────────────┤
  │ Payload                  │  帧负载
  └───────────────────────────┘

帧类型:
  HEADERS  — 请求/响应头
  DATA     — 请求/响应体
  PRIORITY — 流优先级
  RST_STREAM — 取消流
  SETTINGS — 连接设置
  PUSH_PROMISE — 服务器推送
  PING     — 心跳
  GOAWAY   — 关闭连接
  WINDOW_UPDATE — 流量控制

2. 多路复用(Multiplexing)

单个 TCP 连接上并行传输多个请求和响应——HTTP/2 最重要的特性:

HTTP/1.1(6 个连接,串行):
连接1: [请求A ─────────→][请求D ──→]
连接2: [请求B ──→][请求E ────→]
连接3: [请求C ───────→][请求F ──→]

HTTP/2(1 个连接,并行):
单连接: [A帧][B帧][C帧][A帧][B帧][C帧][D帧][A帧][E帧]...
         ↑    ↑    ↑
      流1-A  流2-B  流3-C  三个请求的数据帧交错传输

核心概念

连接(Connection):一个 TCP 连接
  └─ 流(Stream):双向的帧序列,代表一个请求/响应对
       └─ 帧(Frame):通信的最小单位

一个连接可以承载多个流(通常上限 100-256 个并发流)
每个流有唯一的 Stream ID
客户端发起的流用奇数 ID(1, 3, 5...)
服务器推送的流用偶数 ID(2, 4, 6...)
帧可以交错发送,接收方根据 Stream ID 重新组装
示例:3 个资源并行传输

   Stream 1 (index.html)    Stream 3 (app.js)     Stream 5 (style.css)
   ────────────────         ──────────────         ────────────────
   HEADERS 帧               HEADERS 帧             HEADERS 帧
   DATA 帧 #1               DATA 帧 #1             DATA 帧 #1
   DATA 帧 #2               DATA 帧 #2             DATA 帧 #2
   DATA 帧 #3(END)          DATA 帧 #3             DATA 帧 #3(END)
                             DATA 帧 #4(END)

TCP 连接上实际传输顺序:
[S1:H][S3:H][S5:H][S1:D1][S3:D1][S5:D1][S1:D2][S5:D2][S3:D2][S1:D3][S5:D3][S3:D3][S3:D4]

3. 头部压缩(HPACK)

HTTP/2 使用 HPACK 算法压缩 Header,大幅减少重复头部的传输:

HPACK 三大策略:

1. 静态表(Static Table):61 个常见 Header 预定义
   ┌─────┬──────────────────┬──────────────┐
   │ 索引 │ Header Name      │ Header Value │
   ├─────┼──────────────────┼──────────────┤
   │  1  │ :authority       │              │
   │  2  │ :method          │ GET          │
   │  3  │ :method          │ POST         │
   │  4  │ :path            │ /            │
   │  5  │ :path            │ /index.html  │
   │ ... │ ...              │ ...          │
   │ 61  │ www-authenticate │              │
   └─────┴──────────────────┴──────────────┘
   
   例:":method: GET" → 只需发送索引 2(1 字节)

2. 动态表(Dynamic Table):连接级别的 Header 缓存
   首次发送:
     Cookie: session_id=abc123  → 完整传输 + 加入动态表(索引 62)
   后续发送:
     Cookie: session_id=abc123  → 发送索引 62(1 字节)

3. Huffman 编码:对字面值进一步压缩
   "application/json" → Huffman 编码后更短
压缩效果示例:

第一个请求(~800 字节 → ~200 字节):
  :method: GET          → 索引 2
  :path: /api/users     → 索引 4 + 字面值 "/api/users"
  host: api.example.com → 字面值(加入动态表)
  cookie: session=abc   → 字面值(加入动态表)
  authorization: Bearer → 字面值(加入动态表)

第二个请求(~800 字节 → ~20 字节!):
  :method: GET          → 索引 2
  :path: /api/posts     → 索引 4 + 字面值 "/api/posts"
  host:                 → 动态表索引(1 字节)
  cookie:               → 动态表索引(1 字节)
  authorization:        → 动态表索引(1 字节)

4. 流优先级(Stream Prioritization)

客户端可以告诉服务器哪些资源更重要,让服务器优先发送:

优先级树(HTTP/2 原始方案):

       Stream 1 (HTML) — 权重 256
       /              \
Stream 3 (CSS)    Stream 5 (JS)
  权重 256           权重 256
                      |
               Stream 7 (image)
                  权重 128

含义:
  - HTML 最先传输
  - CSS 和 JS 平分 HTML 之后的带宽
  - 图片在 JS 之后传输,且只获得一半带宽

实际问题:
  - 浏览器实现不一致
  - 服务器可能忽略优先级
  - HTTP/2 的优先级方案过于复杂

HTTP/3 改进(Extensible Priorities - RFC 9218):
  使用更简单的 urgency(0-7)+ incremental(是否增量)
  priority: u=0   → 最高优先级(HTML/CSS)
  priority: u=3   → 中等优先级(JS)
  priority: u=6   → 低优先级(图片)

5. Server Push(服务器推送)

服务器可以主动推送客户端可能需要的资源:

传统流程:
  客户端 → GET /index.html
  服务器 → 200 OK (HTML)
  客户端 → 解析 HTML,发现需要 style.css
  客户端 → GET /style.css
  服务器 → 200 OK (CSS)

Server Push 流程:
  客户端 → GET /index.html
  服务器 → PUSH_PROMISE (style.css)    ← 提前推送
  服务器 → 200 OK (HTML)
  服务器 → 200 OK (style.css)          ← 客户端不用再请求
  客户端 → 解析 HTML,发现 style.css 已在缓存中 ✅
实际现状(2025):

Server Push 已被大部分实践证明收益有限:
  - 推送的资源可能客户端已有缓存(浪费带宽)
  - 推送时机难以精确控制
  - 与 CDN 配合困难
  - Chrome 从 106 版本开始已移除 HTTP/2 Push 支持

替代方案:
  - 103 Early Hints:服务器在最终响应前先发提示
    HTTP/1.1 103 Early Hints
    Link: </style.css>; rel=preload; as=style
    Link: </app.js>; rel=preload; as=script

    → 浏览器提前开始下载,比 Push 更灵活
  
  - <link rel="preload">:在 HTML 中声明预加载

HTTP/2 仍存在的问题

TCP 层的队头阻塞

HTTP/2 解决了应用层的队头阻塞,但**传输层(TCP)**的队头阻塞仍然存在:

HTTP/2 多路复用:
  Stream 1: [帧A1][帧A2][帧A3]
  Stream 2: [帧B1][帧B2]
  Stream 3: [帧C1][帧C2][帧C3]

TCP 传输的实际包序列:
  [包1:A1][包2:B1][包3:C1][包4:A2][包5:B2][包6:C2][包7:A3][包8:C3]

如果包 2 丢失:
  ✅ 包1(A1) 正常
  ❌ 包2(B1) 丢失 → TCP 必须等待重传
  ⏸️ 包3(C1) 已收到但不能交付(TCP 保证顺序)
  ⏸️ 包4(A2) 已收到但不能交付
  ⏸️ 包5(B2) 已收到但不能交付
  ... 所有后续包都被阻塞!

TCP 不知道 HTTP/2 的"流"概念
  → 一个包丢失会阻塞所有流
  → 在高丢包率网络(如移动网络)下,HTTP/2 性能可能不如 HTTP/1.1

TCP 连接建立延迟

TCP + TLS 1.2:3-RTT
  TCP 三次握手(1 RTT)+ TLS 握手(2 RTT)

TCP + TLS 1.3:2-RTT
  TCP 三次握手(1 RTT)+ TLS 握手(1 RTT)

在高延迟网络(如卫星、跨洋)下:
  RTT = 200ms → 连接建立 = 400-600ms
  用户等了半秒还没开始传数据

连接迁移问题

TCP 连接由四元组标识:(源IP, 源端口, 目标IP, 目标端口)

场景:手机从 Wi-Fi 切换到 4G
  Wi-Fi IP: 192.168.1.100 → 4G IP: 10.0.0.50
  源 IP 变了 → TCP 连接断开 → 必须重新建连 + TLS 握手
  
  用户感知:页面短暂无响应 → 重新加载

HTTP/3 核心特性

HTTP/3 将传输层从 TCP 切换为 QUIC(Quick UDP Internet Connections)

协议栈对比:

HTTP/2:                HTTP/3:
┌──────────┐          ┌──────────┐
│  HTTP/2  │          │  HTTP/3  │
├──────────┤          ├──────────┤
│   TLS    │          │   QUIC   │  ← QUIC 内建了 TLS 1.3
├──────────┤          │          │     + 可靠传输
│   TCP    │          │          │     + 多路复用
├──────────┤          ├──────────┤
│    IP    │          │   UDP    │  ← 基于 UDP
└──────────┘          ├──────────┤
                      │    IP    │
                      └──────────┘

1. 无队头阻塞的多路复用

QUIC 在传输层原生支持**流(Stream)**的概念——每个流独立可靠传输:

HTTP/2 over TCP(一个包丢失阻塞所有流):

  TCP 字节流:[A1][B1][C1][A2][B2][C2]
                    ↑ 丢失
              所有后续数据被阻塞

HTTP/3 over QUIC(一个流丢包只影响该流):

  QUIC 流 A: [A1]     [A2] → A 正常接收
  QUIC 流 B: [B1] ❌        → B 等待重传,但不影响 A 和 C
  QUIC 流 C: [C1]     [C2] → C 正常接收

  关键区别:QUIC 的每个流有独立的丢包恢复
  → 一个流丢包不影响其他流
  → 高丢包率网络下优势明显

2. 更快的连接建立

TCP + TLS 1.3:2-RTT(TCP 1 RTT + TLS 1 RTT)

QUIC 首次连接:1-RTT
  ┌─────────────────────────────────────┐
  │ Client Hello + TLS ClientHello      │ ──→
  │ ←── Server Hello + TLS + 握手完成    │     1 个 RTT
  │ 加密数据传输开始                      │ ──→
  └─────────────────────────────────────┘
  
  QUIC 将传输握手和 TLS 握手合并为一步

QUIC 恢复连接:0-RTT
  ┌─────────────────────────────────────┐
  │ Client Hello + 缓存的密钥 + 数据     │ ──→  0 个 RTT!
  │ ←── Server 响应                      │      首个包就携带数据
  └─────────────────────────────────────┘
  
  条件:客户端和服务器之前建立过连接,缓存了密钥
  风险:0-RTT 数据可能被重放攻击(需要应用层防护)

3. 连接迁移(Connection Migration)

TCP:四元组标识连接 → IP 变化 = 连接断开

QUIC:Connection ID 标识连接 → IP 变化不影响

场景:Wi-Fi → 4G
  TCP:  Wi-Fi 连接断开 → 重新三次握手 + TLS → 恢复传输
  QUIC: Connection ID 不变 → 无缝切换 → 继续传输

  ┌──────────────────────────────────────┐
  │   Wi-Fi (IP: 192.168.1.100)         │
  │   QUIC Connection ID: 0xABCD1234    │
  │   正在传输数据...                     │
  │                                      │
  │   ── 切换到 4G (IP: 10.0.0.50) ──   │
  │                                      │
  │   QUIC Connection ID: 0xABCD1234    │ ← 同一个 Connection ID
  │   继续传输数据(无中断)              │
  └──────────────────────────────────────┘

4. QPACK 头部压缩

HTTP/3 使用 QPACK 替代 HPACK,解决了 HPACK 在无序传输下的问题:

HPACK 的问题:
  动态表的更新依赖帧的顺序
  HTTP/2 over TCP 保证了顺序 → 没问题
  但 QUIC 的流是独立传输的,可能乱序到达 → 动态表状态不一致

QPACK 的解决方案:
  使用单独的单向流来同步动态表状态
  
  ┌─ Encoder Stream(客户端 → 服务器):发送动态表更新指令
  ├─ Decoder Stream(服务器 → 客户端):确认收到的指令
  └─ 请求/响应流:引用静态表和已确认的动态表条目

  效果:保持了 HPACK 的压缩率,同时支持无序传输

三代协议对比

特性HTTP/1.1HTTP/2HTTP/3
传输层TCPTCPQUIC (UDP)
连接复用Keep-Alive(串行)多路复用(并行)多路复用(并行)
队头阻塞应用层 ✅ / TCP 层 ✅应用层 ❌ / TCP 层 ✅应用层 ❌ / 传输层 ❌
头部格式文本二进制二进制
头部压缩HPACKQPACK
加密可选(HTTPS)实际强制 HTTPS内建 TLS 1.3
连接建立1-3 RTT1-2 RTT1 RTT / 0-RTT
连接迁移✅ (Connection ID)
Server Push✅(已弃用)✅(同样不推荐)
流优先级复杂树模型简化 urgency + incremental
部署难度高(需 UDP 支持)

实际部署

HTTP/2 部署

nginx
# Nginx 配置
server {
    listen 443 ssl http2;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # HTTP/2 特有配置
    http2_max_concurrent_streams 128;
    http2_initial_window_size 65535;
}

HTTP/3 部署

nginx
# Nginx(需要 1.25.0+)
server {
    listen 443 ssl;
    listen 443 quic;          # 启用 QUIC/HTTP/3
    http2 on;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # 告诉浏览器支持 HTTP/3
    add_header Alt-Svc 'h3=":443"; ma=86400';
}
HTTP/3 协商流程:

1. 首次访问:浏览器用 HTTP/2 (TCP) 连接
2. 服务器响应头:Alt-Svc: h3=":443"; ma=86400
3. 浏览器后台尝试 QUIC 连接
4. 如果 QUIC 连接成功 → 后续请求使用 HTTP/3
5. 如果 QUIC 失败(UDP 被防火墙阻断)→ 继续用 HTTP/2

注意:
  - 企业网络/某些运营商可能屏蔽 UDP 443 端口
  - 所以 HTTP/3 总是需要 HTTP/2 作为 fallback
  - 浏览器会缓存 Alt-Svc 信息,下次直接尝试 HTTP/3

CDN 支持情况

主流 CDN 的 HTTP/3 支持(2025):

Cloudflare  — ✅ 默认启用(最早支持者之一)
Fastly      — ✅ 支持
AWS CloudFront — ✅ 支持
Google Cloud CDN — ✅ 支持
Akamai      — ✅ 支持
阿里云 CDN   — ✅ 支持
腾讯云 CDN   — ✅ 支持

推荐:直接在 CDN 层启用 HTTP/3,比自己配置 Nginx 简单得多

HTTP/2 时代的优化策略变化

HTTP/1.1 时代的优化         HTTP/2 时代是否仍需要
──────────────────         ───────────────────
域名分片                    ❌ 不需要(多路复用,一个连接够了)
CSS Sprites                ❌ 不需要(并行请求无代价)
资源合并(bundle all)       ⚠️ 视情况(代码分割更灵活,但有取舍)
内联小资源(Base64)          ⚠️ 视情况(小资源并行请求也很快)
资源压缩(gzip/brotli)      ✅ 仍然需要(减少传输体积)
CDN 加速                   ✅ 仍然需要(减少 RTT)
缓存策略                    ✅ 仍然需要(减少请求数)
代码分割(Code Splitting)    ✅ 更重要了(细粒度分割 + 并行加载)
<link rel="preload">       ✅ 仍然重要(提前发现关键资源)

Bundle 策略的变化

HTTP/1.1 时代:尽量合并,减少请求数
  app.js (500KB) = 所有代码打在一起
  缺点:改一行代码 → 整个 500KB 缓存失效

HTTP/2 时代:细粒度分割,利用并行加载 + 缓存
  vendor.js (200KB)      → 第三方库,很少变化
  framework.js (80KB)    → 框架代码,偶尔更新
  app.js (50KB)          → 业务公共代码
  page-home.js (30KB)    → 首页代码(按需加载)
  page-about.js (20KB)   → 关于页代码(按需加载)
  
  优势:
    - 改业务代码只失效 app.js(50KB),vendor 继续用缓存
    - 页面间共享的 vendor/framework 只需加载一次
    - 首屏只加载必要代码

面试高频题

1. HTTP/2 的多路复用是如何解决 HTTP/1.1 的队头阻塞的?

HTTP/1.1 在同一个 TCP 连接上请求必须串行——前一个响应完成后才能发下一个,导致慢响应阻塞后续请求。HTTP/2 引入二进制分帧层,将请求/响应分割为帧(Frame),每个请求/响应对应一个流(Stream),多个流的帧可以在同一个 TCP 连接上交错传输。接收方根据帧的 Stream ID 将它们重新组装为完整的请求/响应。这样一个连接就能并行传输多个请求,不再需要排队等待。但 HTTP/2 只解决了应用层的队头阻塞,TCP 层的队头阻塞仍然存在——一个 TCP 包丢失会导致所有流的数据被阻塞。

2. HTTP/2 的 HPACK 头部压缩是怎么工作的?

HPACK 使用三种策略:①静态表——预定义 61 个常见 Header(如 :method: GET),只需发送 1 字节索引;②动态表——连接级别的 Header 缓存,首次发送完整值并加入动态表,后续只发送索引号;③Huffman 编码——对字面值做进一步压缩。效果:首次请求 Header 可从 ~800 字节压缩到 ~200 字节,后续同类请求可压缩到 ~20 字节(大部分 Header 都能用索引替代)。

3. HTTP/3 为什么使用 UDP 而不是 TCP?

本质原因是 TCP 的设计无法满足 HTTP/3 的需求:①TCP 的队头阻塞无法解决——TCP 保证字节流顺序,一个包丢失会阻塞所有后续数据,即使它们属于不同的 HTTP 流;②TCP 握手延迟无法减少——TCP 三次握手是协议规范,无法跳过;③TCP 不支持连接迁移——TCP 用四元组标识连接,IP 变化就断开。HTTP/3 选择在 UDP 之上构建 QUIC 协议,自己实现可靠传输、拥塞控制和多路复用,从而获得了流级别的独立丢包恢复、1-RTT/0-RTT 连接建立、和基于 Connection ID 的连接迁移能力。

4. QUIC 的 0-RTT 是怎么实现的?有什么安全风险?

0-RTT 适用于客户端与服务器之前已建立过连接的场景。首次连接时,双方通过 1-RTT 握手协商密钥,客户端缓存会话密钥和传输参数。下次连接时,客户端用缓存的密钥直接加密第一个数据包,与 ClientHello 一起发送——服务器如果能验证密钥有效,就直接解密处理。安全风险:0-RTT 数据可能被重放攻击(攻击者截获并重发)——因为在握手完成前无法确认客户端是否真的发起了请求。防御措施:①服务器端对 0-RTT 数据做幂等性检查;②只对安全的操作(GET 请求)使用 0-RTT;③使用 ticket 失效机制限制重放窗口。

5. HTTP/2 时代还需要做哪些前端优化?哪些传统优化不再必要?

不再需要:①域名分片(多路复用一个连接够了);②CSS Sprites/图标合并(并行请求无额外代价);③内联 Base64 小资源(直接请求也很快)。仍然需要:①代码分割更重要了——细粒度分割利用并行加载和独立缓存(vendor/framework/page 分开);②资源压缩(Brotli/Gzip)减少传输体积;③缓存策略(长期强缓存 + hash 文件名);④<link rel="preload"> 提前发现关键资源;⑤CDN 加速减少 RTT。核心思维转变:从"减少请求数"转向"合理分割 + 并行加载 + 精细化缓存"。


追问思考

  1. 为什么 HTTP/2 要求使用 HTTPS?纯 HTTP 不能用 HTTP/2 吗?规范和实际实现有什么区别?
  2. QUIC 基于 UDP,如果企业防火墙或运营商屏蔽了 UDP 443 端口怎么办?浏览器如何处理 HTTP/3 降级?
  3. HTTP/2 的 Server Push 为什么失败了?Chrome 为什么在 106 版本移除了它?103 Early Hints 有什么优势?
  4. 从抓包工具(如 Wireshark)的角度,HTTP/2 和 HTTP/3 的数据分别长什么样?为什么 HTTP/3 更难调试?
  5. 假设一个页面需要加载 50 个小资源(平均每个 2KB),分别在 HTTP/1.1、HTTP/2、HTTP/3 下的加载行为有什么区别?

用心学习,用代码说话 💻