本章回答:为什么 HTTP/1.1 必须靠"多开连接"续命,而 HTTPS 又凭什么在 1~2 个 RTT 内谈完密钥。

学习目标

读完本章,你应当能够:

  1. 推导 HTTP/1.1 队头阻塞(head-of-line blocking, HOL)的成因,说明"浏览器 6 连接"与"域名分片"是同一原因的两种表现;
  2. 区分应用层 HOL 与 TCP 层 HOL,指出后者为什么是 HTTP/3 的唯一动机;
  3. 画出 TLS 1.2(2-RTT)与 TLS 1.3(1-RTT、0-RTT)握手时序,逐条说出 TLS 1.3 砍掉了什么、为什么能省一个 RTT;
  4. 判断 301/302/307/308、401/403、502/504 的语义差异,并指出"是否允许改变请求方法"这一判定轴;
  5. 解释 SNI、HSTS、OCSP Stapling 各自解决的具体问题,而不是背缩写。

前置知识检查

你需要知道在哪一节补齐
TCP 三次握手、TIME_WAIT、半连接队列见第 16 章 16.1~16.4 节
cwnd/ssthresh 演化、快速重传与 SACK见第 17 章 17.4~17.6 节
MTU/MSS 与分层封装解封装见第 15 章 15.3 节
TLS 记录层与密码套件命名读法本章 18.7 节给出最小必要背景

18.1 HTTP 的定位与无状态性

HTTP(HyperText Transfer Protocol)是应用层的请求-响应协议,只约定报文的语义与格式,不约定连接怎么建、怎么复用、怎么保证可靠——这些全部下推给传输层。本章的版本演进都发生在这条分界线的夹缝里。

**无状态(stateless)**要说得比课本精确:服务端处理一个请求时,不依赖此前任何请求留下的、协议层自带的状态,每个请求必须自带全部上下文。好处是工程性的:

好处机制上的原因
服务端可任意水平扩缩任一请求可落到任意实例,无需 sticky 路由
单点故障不影响会话实例崩溃不丢协议层状态,天然可重启
中间层可缓存、可重放、可重试请求自含语义,代理无需回放历史
协议实现简单无需在服务端维护每连接的会话状态机

代价是应用状态必须外置。Cookie(RFC 6265)是客户端存储加自动回带(按域、路径、Max-Age、Secure、HttpOnly、SameSite 约束),本身不含语义;Session 才是真正的状态,放在服务端内存/Redis/DB,Cookie 里只放一个不可猜测的 ID。于是无状态协议上重新引入了状态,代价立刻显现:会话粘滞(sticky session)、会话存储成为新单点、跨机房同步延迟、SameSite 与 CSRF 的耦合。

重要

面试标准落点:无状态是协议设计的正确取向,Cookie/Session 是应用层补偿,代价是"服务端又变成有状态了"。 只说"HTTP 无状态所以用 Cookie"是半句话,面试官会追问"Session 存哪里、跨机房怎么办"。

18.1.1 HTTP 报文格式

报文格式必须能默画,因为它解释了后面几乎所有演进:HTTP/1.1 的每个请求都要在同一条字节流上与前后请求首尾相接,格式本身携带了边界信息。

1
2
3
4
5
GET /api/v1/users?page=2 HTTP/1.1    ← 起始行:方法 + 目标 + 版本
Host: api.example.com                ← 头部字段(header fields),0..N 行
Accept: application/json
                          ← 空行:CRLF,格式上的强制分隔符
{"id":1}                             ← 正文(body),可缺省

四个部分:起始行(start-line)+ 头部字段 + 空行(CRLF)+ 正文(body)。三个会被追问的点:

  1. 空行是格式的一部分,不是排版。没有它接收方无法知道头部在哪结束;这也是头部能逐行解析、而正文只能用长度或分块定界的原因。
  2. 正文定界有三条路:Content-Length 定长、Transfer-Encoding: chunked 分块(长度未知时)、由连接关闭隐式定界。第三条正是流水线无法工作的直接原因之一。
  3. Host 是 HTTP/1.1 的强制头,它是"一个 IP 承载多域名"的基础;HTTPS 里同样的需求由 SNI 在 TLS 层完成(见 18.9.3)。

18.2 HTTP/1.1:持久连接、连接池与管线化

18.2.1 持久连接与连接池

HTTP/1.0 默认每请求一条 TCP 连接,一次页面几十个资源就要几十次"三次握手 + 慢启动"。成本是实打实的:同机房 RTT 约 0.10.5 ms,跨大洲 RTT 约 150 ms,而 cwnd 只从 110 个 MSS 起跳。

HTTP/1.1 默认持久连接(persistent connection)(HTTP/1.1 中 Connection: keep-alive 其实冗余,缺省即持久;Connection: close 才是否定)。工程上真正的重点是连接池:客户端按"每源 6 条"限流;服务端把建连成本摊销到每 N 个请求;而 NAT 网关、四层负载均衡通常有 60~900 s 的空闲回收,于是 keepalive_timeout 必须小于上游的回收时间,否则会出现"连接看着是活的、一写就 RST"的半开(half-open)现象——这是线上最常见的 502 来源之一。

提示

keepalive_timeout 的取值原则是"比谁都短":让服务端成为主动关闭方——只有主动关闭方才走完整 FIN 流程,而不是撞上对端已消失的 RST(TIME_WAIT 的归属见第 16 章 16.5 节)。

18.2.2 管线化为什么在实践中失败

**管线化(pipelining)**的设想很自然:既连接是持久的,客户端为什么不能一次把请求 A、B、C 全写出去,再按顺序读响应?三个原因必须分开讲:

  1. 响应必须严格按请求顺序返回(RFC 7230 要求)。服务端不能先回 B 再回 A,因为响应里没有请求 ID;于是 A 慢(比如一次重查询),B、C 早就处理完了也只能等——这就是应用层队头阻塞。
  2. 中间设备兼容性:早期代理对"一个连接里连续到达多个请求"处理不一致,有的直接关连接,有的串行化,有的缓存出错。协议允许不等于部署环境支持。
  3. 出错后无法确定应答归属:一旦服务端提前关连接或某个响应出错,客户端无法判断剩余字节属于哪个请求,只能整条连接作废重发。这是"缺少请求标识符"的直接后果,也解释了 HTTP/2 为什么必须引入 Stream ID。
1
2
3
4
5
HTTP/1.1 管线化的失败(应用层 HOL),时间 →
客户端 ──┬─ req A ─┬─ req B ─┬─ req C ───────────────────────►
服务端   │ 处理A(慢,200 ms) │ 处理B(快,5 ms)→ 完成但只能等待
         └─ resp A ─────────────────────► resp B ─► resp C
            └─── 这 200 ms 里 B、C 的响应被"堵"在 A 后面 ───┘

18.3 队头阻塞:应用层 vs 传输层

“队头阻塞"被过度使用,面试最容易被抓的是不区分层次。

HTTP/1.1 的 HOL 在应用层:同一连接上请求-响应严格串行,它甚至不需要管线化就已经存在——不开管线化时客户端必须等上一个响应读完才发下一个,串行度 100%。绕过的办法是开更多连接:浏览器对每个源(origin)默认限制 6 条并发连接(HTTP/1.1 时代的事实标准),并发度从 1 变成 6。这是绕过而非解决:第 7 个请求仍要排队。

**域名分片(domain sharding)**正是顺着这个限制做的工程妥协:把 static.example.com 拆成 s1..s2.example.com,因为限流单位是"源”,2 个域名就能拿到 2 × 6 = 12 条连接。代价同样具体:

代价机制
多出 DNS 解析每个新域名一次解析,冷启动额外 RTT(见第 19 章 19.2 节)
多出 TLS 握手每域名独立证书与握手,HTTPS 下代价翻倍
连接池碎片化每域名连接利用率下降,空闲连接仍占内存与 fd
破坏拥塞控制效率24 条连接各自从慢启动爬,总吞吐与丢包敏感度都更差
与 HTTP/2 直接冲突分片会破坏单连接多路复用前提,属负优化

小心

把"HTTP/1.1 的队头阻塞"和"HTTP/2 的队头阻塞"混为一谈,是这章最容易把面试答崩的地方。前者是应用层串行、可用多连接绕过;后者是TCP 字节流的有序交付、无法用多连接绕过(HTTP/2 的本意就是不许多开连接)。层次说错,HTTP/3 的动机就全错了。

18.4 HTTP/2:二进制分帧与多路复用

HTTP/2(RFC 7540)没有改语义(方法、状态码、头部字段含义全部沿用 HTTP/1.1),只改了两件事:报文怎么编码、报文怎么在连接上复用。

18.4.1 二进制分帧层

HTTP/2 把报文切成帧(frame),每帧带 9 字节固定头;Length 24 位决定单帧载荷上限,默认 SETTINGS_MAX_FRAME_SIZE 是 16 384(2¹⁴)字节;Stream Identifier 31 位是复用的全部秘密——接收方靠它把交错的帧归位到各自的响应上,这正是 HTTP/1.1 缺的那个"请求标识符"。

1
2
HTTP/2 帧头(9 字节): | Length(24) | Type(8) | Flags(8) |R| StreamID(31) |
Payload 长度 0 .. 2²⁴−1 字节;Type 决定这是 HEADERS / DATA / SETTINGS / ...

二进制而非文本的收益有两块:解析不必再处理文本边界与大小写变体;帧可以在同一连接上交错(interleave),因为边界判定不再依赖"行"和"顺序"。

18.4.2 流、消息与帧的三层关系

必考的结构题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
一条 HTTP/2 连接(TCP)= 多个流;Stream 1 (ID=1, 客户端发起) ← 流:请求-响应的
逻辑通道,双向、有状态;Message(请求)= HEADERS + DATA 帧 ← 消息:一次完整
请求或响应;Message(响应)= HEADERS + 2×DATA 帧(END_STREAM)
├── Stream 3 (ID=3) └── Message: HEADERS / HEADERS + DATA 帧
└── Stream 5 (ID=5) └── Message: HEADERS + DATA 帧(END_STREAM)
线上字节流交错到达,接收方按 Stream ID 重组:[H1][H3][H1][D3][H5][D1]...
```规则:**流是逻辑通道**(并发数由 `SETTINGS_MAX_CONCURRENT_STREAMS` 约束,实践常为 100+);**消息是完整语义单元**;**帧是最小传输单位**。客户端发起的流 ID 为奇数,服务端发起(推送)的为偶数,ID 单调递增且不能重用,耗尽后需 `GOAWAY` 或新建连接。

### 18.4.3 头部压缩 HPACK

HTTP/1.1 的头部是纯文本且大量重复(Cookie、`User-Agent` 每请求带一遍)。HTTP/2 用 **HPACK**(RFC 7541)压缩,三个机制协同:

| 机制 | 做法 | 省字节的原理 |
|---|---|---|
| 静态表(static table) | 61 条预定义的字段名+常见值(`:method: GET`、`:status: 200`) | 常见头部压成 1 个索引字节,`:method: GET` 从约 14 字节降到 1 字节 |
| 动态表(dynamic table) | 连接级 FIFO 表,本次出现的字段名/值可被后续索引引用 | 第二个请求的 `user-agent` 变成 1 个索引,**跨请求去重**是大头 |
| 哈夫曼编码(Huffman coding) | 对未命中表的字面量按 RFC 7541 静态哈夫曼表编码 | 字面量再省约 30% |

动态表是**连接级、有状态的**,两个约束随之而来:压缩上下文不能跨连接复用;表容量需由 `SETTINGS_HEADER_TABLE_SIZE` 与 `dynamic table size update` 协商同步。也正因为有状态,HPACK 需要用表大小上限与重置机制限制解码侧信道的影响面。

### 18.4.4 服务端推送、优先级与它们的结局

**服务端推送(server push)**允许服务端用偶数 ID 的流主动推资源(典型是 `index.html` 的 CSS/JS),省掉"解析 HTML 再发请求"的一次往返。它在实践中基本失败了:服务端很难知道客户端缓存里已有什么,容易推重复白耗带宽;推送与 `Cache-Control` 交互复杂;且与浏览器自身的预加载与优先级逻辑冲突。**结果是 HTTP/3(RFC 9114)不再有 `PUSH_PROMISE`**,改用 `103 Early Hints` 让服务端"提示"客户端去请求。

**流优先级与依赖树**允许客户端给流指定权重(1~256)与依赖关系,构成一棵依赖树;这套复杂树模型难以被正确实现,后来被 RFC 9218 的**可扩展优先级方案**取代,简化为 `urgency`(0~7)与 `incremental` 两个参数。

> [!NOTE]
> HTTP/2 是"语义不变、传输层重写"的一次升级,且明确要求客户端**不要**再为绕开并发限制而开多条连接,否则多路复用与 HPACK 的收益互相抵消。记住这条"不许多开连接",18.5 节才讲得通。

## 18.5 HTTP/2 仍存在 TCP 层队头阻塞

这是 HTTP/2 最容易被考到的缺点,也是 HTTP/3 存在的唯一原因:**多路复用解决了应用层串行,却把并发放进了同一条 TCP 连接,而 TCP 的交付语义是"字节流按序交付"。**

```text
TCP 层 HOL:一个包丢了,全部流一起等
Stream 1   ██████████████████  ← 帧早已到达
Stream 3   ██████████████████  ← 帧早已到达
Stream 5   ██████░░░░░░░░░░░░  ← 第 3 个包丢了
                     ↑
TCP 接收缓冲区:[ ..... gap ..... ][后续字节全部就绪,但应用层读不到]
                     ↓ 内核不把 gap 之后的字节交给应用层(按序交付),HPACK 解码器也无法推进

关键在于这个等待是 RTT 量级的:同机房 RTT 约 0.1~0.5 ms 尚可忍,跨大洲 RTT 约 150 ms 时一次丢包就让所有流各停 150 ms 以上。丢包率越高、RTT 越大,HTTP/2 相对 HTTP/1.1 越吃亏——HTTP/1.1 时代一次丢包只影响其中一条连接上的一个请求,另外 5 条照常前进。

重要

一句话标准答案:HTTP/2 用一条 TCP 连接承载所有流,TCP 的按序交付使任一报文段丢失阻塞全部流;多开连接救不了,因为多路复用与 HPACK 的收益都建立在"单连接"上。 要解决它,必须把"流的独立重传"下沉到传输层——这就是 QUIC。

18.6 HTTP/3 与 QUIC

QUIC(RFC 9000)是跑在 UDP 之上的用户态传输协议,HTTP/3(RFC 9114)就是把 HTTP 语义映射到 QUIC 上。三个问题必须答清。

18.6.1 为什么基于 UDP

不是因为 UDP 更好,而是因为中间设备僵化(ossification):大量 NAT、防火墙、负载均衡只认 TCP/UDP 的既有行为,新传输协议若占用新的 IP 协议号(如 SCTP)会被直接丢弃;封装成 UDP 载荷则能以很高概率穿过这些设备。既然用 UDP,TCP 提供过的一切都得在用户态重建,而重建带来两个副产品:可快速迭代(不必等内核升级与操作系统普及,随浏览器/服务更新即可上线新特性),以及连接内的流独立可控。

18.6.2 QUIC 的流与拥塞控制

能力TCP + TLS(HTTP/2 栈)QUIC(HTTP/3 栈)
丢包影响范围阻塞整条连接的所有流只阻塞该流,其他流照常交付
握手TCP 三次握手 + TLS 握手串行叠加传输握手与 TLS 握手合并
加密范围TLS 只加密应用数据,TCP 头明文除少数字段外连传输层头也加密
连接标识四元组 (源IP, 源端口, 目的IP, 目的端口)Connection ID
拥塞控制位置内核用户态,可插拔
队头阻塞层次应用层已解决,传输层仍在跨流 HOL 消除

QUIC 内部有独立的流级流控与连接级流控,流的字节偏移独立。但要说准一个点:同一流内部仍然有序交付,所以 QUIC 消除的是"跨流 HOL",不是"流内 HOL"。头部压缩在 HTTP/3 中换成 QPACK(RFC 9204),专门为"跨流乱序到达"重新设计——这正是 HPACK 那种"一个头部帧丢了就卡住所有流"的机制不能在 QUIC 上直接复用的原因。

18.6.3 0-RTT、重放风险与连接迁移

  • 1-RTT 建连:首次连接时 QUIC 的传输握手(含 TLS 1.3 密钥协商)在一个往返内完成;而 TCP + TLS 1.2 通常要 3 个往返(1 建连 + 2 握手)。

  • 0-RTT 与会话复用:客户端用上次会话的票据(session ticket)直接加密并发送应用数据,做到"第一个包就带业务数据"。代价是 0-RTT 数据没有前向保密且可被重放——攻击者抓到这段密文就能重复发送。因此 0-RTT 只适用于幂等且无副作用的请求(如 GET 静态资源),绝不能用于支付、提交订单;服务端还必须自行做重放防护(一次性票据、时间窗、请求去重)。

  • 连接迁移(connection migration):Connection ID 取代四元组作为连接身份,所以手机从 Wi-Fi 切到 4G、或 NAT 重新映射端口时连接不中断,也不必重新握手。

  • 弱网优势的量化直觉:设 RTT = 150 ms、丢包率 2%。HTTP/2 下每次丢包让所有并发流各等一个 RTT 量级;HTTP/3 下只有丢包的那个流等。这就是"弱网下 HTTP/3 的 P99 明显好于 HTTP/2"的来源——不是带宽更高,而是尾延迟抖动更小。

警告

常见错误回答:“QUIC 用 UDP 所以更快,因为 UDP 不握手、不重传。“QUIC 自己实现了可靠性、重传、拥塞控制与流控,比 TCP 更复杂,不是退回不可靠。另一个错误是把 0-RTT 当纯收益——0-RTT 有重放风险,必须限制在幂等请求。

18.7 HTTPS 与 TLS 的位置:四种密码学原语的分工

TLS(Transport Layer Security)位于传输层之上、应用层之下,给应用层留下一个"看起来像可靠字节流、但已加密并校验过完整性"的接口,因此 HTTP 本身完全不必改就能被保护——这也是 HTTPS 只是 “HTTP over TLS” 的原因。四种原语解决四个不同问题,最容易混:

原语解决什么在 TLS 中的用途性能量级(相对)
对称加密(symmetric)大批量数据的机密性AES-GCM/ChaCha20-Poly1305 加密应用数据极快,有硬件加速时可达 GB/s
非对称加密(asymmetric)密钥在不安全信道上的分发、身份鉴别ECDHE 密钥交换、RSA/ECDSA 签名慢 2~3 个数量级
消息认证码(MAC/AEAD)完整性:数据被改过没有、来源是否持有密钥GCM 的认证标签、TLS 1.2 CBC 套件的 HMAC快,通常与加密一起
数字签名(digital signature)不可否认的身份:证明消息确由持私钥者发出证书签名、CertificateVerify、ServerKeyExchange 签名慢,但只用几次

为什么非对称只用来协商密钥、数据用对称传:非对称是模幂/椭圆曲线点乘这类大数运算,比 AES 慢 2~3 个数量级,一次响应几百 KB 用 RSA 逐块加密不可行。所以握手阶段用非对称(或基于 DH 的密钥交换)只协商出一个小秘密,之后全部用对称密钥。

注意

面试官会追问的下一层:“对称密钥不能直接用非对称加密传过去吗?” TLS 1.2 的 RSA 密钥交换套件确实如此——客户端生成预主密钥,用服务端 RSA 公钥加密后发过去。TLS 1.3 删掉了静态 RSA 密钥交换,全部改为 (EC)DHE 的前向保密(forward secrecy):即使服务端私钥日后泄露,也无法解密此前录下的流量,因为会话密钥由双方临时私钥共同决定、用完即弃。

18.8 TLS 1.2 与 TLS 1.3 握手对比

18.8.1 完整握手时序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
TLS 1.2 = 2 RTT                       TLS 1.3 = 1 RTT
──────────────────────────────        ─────────────────────────────────
C ─ ClientHello ───────────► S        C ─ ClientHello ──────────────► S
   client_random(32B), cipher_suites,    client_random, cipher_suites,
   extensions                            key_share(猜定的 ECDHE 公钥)
C ◄ ServerHello/Certificate/ ──►      C ◄ ServerHello ─────────────── S
   ServerKeyExchange ──────── S           server_random, key_share
   (server_random 32B、选定套件、          ── 双方已能算出握手密钥 ──
    ECDHE 参数 + 私钥签名)            C ◄ {EncryptedExtensions}, {Certificate},
C ◄ ServerHelloDone ─────── S             {CertificateVerify}, {Finished} ── S
 ══ 第 1 RTT:拿到随机数、             C ─ {Finished} + {HTTP 请求} ──► S
    套件、证书、DH 参数 ══              C ◄ {HTTP 响应} ─────────────── S
C ─ ClientKeyExchange ────► S            ══ 1 RTT 后即可发数据 ══
C ─ ChangeCipherSpec, Finished ► S
C ◄ ChangeCipherSpec, Finished S
 ══ 第 2 RTT 后才发数据 ══

为什么 TLS 1.3 能少一个 RTT:TLS 1.2 的客户端必须先看到服务端的 ServerHello 才知道用哪个组,DH 公钥只能放到第 2 个往返;TLS 1.3 让客户端在 ClientHello 里先猜一个组并带上 key_share(通常是 X25519),服务端若接受该组立刻就能算出共享秘密,于是第 1 个往返里服务端就能把证书与 Finished 一起加密发回。猜错时服务端回 HelloRetryRequest,退回 2-RTT——这是 TLS 1.3 唯一的额外往返。

TLS 1.3 砍掉了什么:

被砍掉的东西砍掉的原因
静态 RSA 密钥交换无前向保密:服务端私钥泄露即可解密历史流量
重协商(renegotiation)历史漏洞多(重协商前缀攻击),且与 PSK/0-RTT 模型冲突
压缩(compression)CRIME 类攻击利用压缩率泄露明文
CBC 模式套件填充预言(padding oracle)类攻击反复出现,只保留 AEAD
自定义 DH 参数小群/非安全群导致降级;只允许几个标准命名组
ChangeCipherSpec握手状态可隐式推导,少一个可被利用的消息
出口级弱套件、RC4、SHA-1已知不安全

TLS 1.3 还把证书本身也加密(放在 EncryptedExtensions 之后),顺带修掉了"证书明文暴露访问域名"的隐私问题。

18.8.2 会话复用与 0-RTT

模式往返数机制风险
TLS 1.2 session ID / ticket1-RTT服务端缓存或客户端持票据,复用主密钥票据泄露即可解密;无前向保密
TLS 1.3 PSK 复用(pre_shared_key)1-RTT票据承载 PSK,跳过证书与密钥交换依赖票据保密性
TLS 1.3 0-RTT(early data)0-RTT第一个包就用 PSK 加密并携带应用数据可重放;0-RTT 数据无前向保密
1
2
3
4
一次全新 HTTPS 请求,发 HTTP 数据前花掉多少往返?RTT 口径:同机房 0.1~0.5 ms、跨大洲约 150 ms
  DNS 解析(无缓存) 1~2 RTT → TCP 三次握手 1 RTT → TLS 1.2 握手 2 RTT → 合计约 4~5 RTT
  改用 TLS 1.3:1 + 1 = 3~4 RTT(省 1 RTT);用 0-RTT(仅幂等请求)再省 1 RTT
跨大洲每省 1 RTT ≈ 省 150 ms,这才是 TLS 1.3 与 0-RTT 的真正价值(HTTP 分层排查见第 19 章 19.2 节)。

0-RTT 的适用边界:只允许幂等、无副作用、可重放的请求(服务端应拒绝非 GET/HEAD 的 0-RTT,或只用于静态资源,并做一次性票据校验)。把"0-RTT 快"和"0-RTT 可被重放"一起说,才完整。

18.9 证书与验证链

18.9.1 证书里有什么

X.509 证书的关键字段:使用者(Subject,含 CN 与 SAN)、颁发者(Issuer)、有效期、公钥、签名算法、签名值及若干扩展。两个必考点:SAN(Subject Alternative Name)已取代 CN,现代实现只认 SAN,多域名靠 SAN 列表或通配符 *.example.com(只匹配一级子域);另外证书本身不加密通信,它只绑定"公钥 ↔ 域名/组织”,并用 CA 签名让任何人可验证这一绑定,真正的加密在握手后由对称密钥完成。

18.9.2 CA 层级与链式验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
根证书(Root CA,自签名,预装于操作系统/浏览器信任库,私钥离线)
  └── 中间 CA(Intermediate CA,由根 CA 签名;日常签发者,泄露可吊销而不伤根)
        └── 叶子证书(Leaf,服务端部署)
              Subject: CN=api.example.com  SAN=[api.example.com, *.example.com]

客户端本地验证(不需要联网):
  1. 用【中间 CA 公钥】验证【叶子证书】签名 → 叶子未被篡改
  2. 用【根 CA 公钥】验证【中间 CA 证书】签名 → 中间 CA 可信
  3. 根 CA 证书在本地信任库 → 信任锚,链条闭合
  4. 逐层检查有效期、SAN 是否匹配目标域名、用途扩展(Key Usage / EKU)
  5. 检查吊销状态(CRL / OCSP / Stapling)

服务端必须发送完整链(叶子 + 中间 CA),但不发根证书(根在客户端本地已有);“链不完整(missing intermediate)“是线上最常见的 TLS 报错之一,浏览器可能靠 AIA 扩展自动补链,但移动 SDK、Java 客户端、curl 等往往不会,于是直接失败。

18.9.3 SNI、吊销与 HSTS

SNI(Server Name Indication)是 ClientHello 中的扩展,明文携带客户端想访问的域名,解决一个 IP 上部署多个 HTTPS 域名的问题:没有 SNI,服务端在握手时必须先决定出示哪张证书,而此时它还不知道客户端要访问哪个域名(HTTP 的 Host 还没发过来,且整个 HTTP 层都在加密之后)。SNI 的隐私缺陷也很实际:它是明文,暴露你要访问的域名;ECH(Encrypted Client Hello)就是在补这个洞。

吊销机制做法问题
CRL定期下载 CA 的全量吊销列表列表可能很大、更新有延迟,客户端可能干脆不检查(fail-open)
OCSP客户端向 CA 实时查询单张证书状态多一次网络往返;CA 可见用户访问行为;CA 不可用时可用性下降
OCSP Stapling服务端定期取回带时间戳的 OCSP 响应,握手时"钉"给客户端省掉客户端往返并保护隐私;缺点是响应有有效期,服务端必须能成功刷新

HSTS(HTTP Strict Transport Security)用 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload 告诉浏览器"此后一段时间内对我只用 HTTPS”。它解决降级攻击:用户输入 example.com 或点某个 http:// 链接时首次请求是明文的,中间人可劫持并阻止跳转(SSL Strip);HSTS 让浏览器在本地就把该域名的 http:// 改写成 https://,不发明文请求。preload 进一步把域名硬编码进浏览器,补上"第一次访问还没收到 HSTS 头"的窗口。

中间人攻击(MITM)与自签证书:MITM 的本质是"攻击者用自己的证书冒充服务端”。若客户端信任库里有攻击者的根证书(企业内网代理、被装过根证书的设备),验证就会通过——TLS 无法防御"客户端自己信任了错误根”。自签证书靠同一机制失效:验证链找不到本地信任锚,于是浏览器报警;用 curl -k 跳过校验"解决",在生产环境等于放弃身份鉴别。

小心

不要说"HSTS 加密了请求"或"OCSP Stapling 帮客户端省了握手"。HSTS 只做强制跳转与降级防护(前提是已有 HTTPS);OCSP Stapling 省的是吊销状态查询的那次往返,与握手的密码学无关。

18.10 常见状态码的语义与陷阱

状态码分 5 类(1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错),本科都讲过;这里只写面试会追问语义差异的部分。

18.10.1 重定向:永久/临时 × 是否允许改方法

状态码语义方法是否可变缓存典型用途
301 Moved Permanently永久移动历史实现允许改成 GET可缓存(浏览器常长期缓存)换域名、http→https
302 Found临时移动历史实现允许改成 GET不缓存登录后跳首页、临时活动页
303 See Other临时,且明确要求用 GET 取必须改为 GET不缓存POST 提交后跳转(PRG 模式)
307 Temporary Redirect临时移动必须保持原方法不缓存需要保方法的临时跳转
308 Permanent Redirect永久移动必须保持原方法可缓存需要保方法的永久跳转

记忆骨架:301/302 是老规范留下的模糊地带,307/308 是把 302/301 的语义说清楚的版本,判定轴只有一个——是否允许把 POST 改成 GET;303 则反过来,明确要求改成 GET。

警告

陷阱在 301/302 的历史行为:RFC 7231 允许(但不强制)客户端收到 301/302 后把非 GET 方法改写为 GET。所以 POST 到 301 的接口很可能被浏览器以 GET 重放,而 GET 没有 body——参数丢失、语义错误,且极难排查。要保方法就用 307/308。

18.10.2 认证与授权、网关错误的归属

  • 401 Unauthorized(名字起错了,语义是 Unauthenticated):你没有证明你是谁,响应必须带 WWW-Authenticate 头告诉客户端如何认证。
  • 403 Forbidden(语义是 Unauthorized):我们知道你是谁,但你无权访问;再认证一次也没用。一句话区分:401 是"去登录",403 是"登录了也没用"。
  • 502 Bad Gateway:网关从上游拿到了无效响应(连上了但响应不合法、上游拒绝、连接被 RST),典型如后端进程崩溃、协议错乱、keepalive 半开连接。
  • 504 Gateway Timeout:网关没等到上游响应就超时,典型如后端慢查询、下游级联超时。一句话区分:502 是"上游的回答不对",504 是"上游根本没回答",排查方向分别是后端进程/协议 与 超时链路/下游耗时。

18.10.3 200/204/206 与 Connection: close

状态码语义要点陷阱
200 OK有正文用 200 + 业务码表达错误(“HTTP 200 但 code != 0")会让缓存、监控、重试全部失真
204 No Content成功但无正文若真带了 body,部分客户端/代理行为不一致;REST 中 DELETE/PUT 常用
206 Partial Content响应范围请求必须配 Content-Range;断点续传、视频拖动、多线程下载靠它

206 的前提:客户端发 Range: bytes=0-1023,服务端回 206 + Content-Range: bytes 0-1023/5000;若服务端不支持范围请求,应回 200 + 全量正文而不是 206,否则客户端会拼出错误文件。

Connection: close 在请求/响应结束后关闭连接:连接池复用失效,每请求重付一次握手与慢启动;更关键的是正文定界方式改变——close 语义下的响应可以不带 Content-Length,由"连接关闭"暗示正文结束。这也解释了为什么它与连接复用不能共存、为什么流水线在 close 下彻底不可行。

1xx 的现代用法:100 Continue(先发头、服务端同意后再发大 body)与 101 Switching Protocols(WebSocket 升级);103 Early Hints 在最终响应前提示客户端预加载资源,也是 HTTP/3 里替代服务端推送的主要手段。

注意

这里的追问通常是”502 和 504 你都怎么排查?“标准答法要落到可观测面:502 先确认后端进程是否存活、是否在重启、代理与后端的协议是否一致(HTTP/1.1 vs h2c、keepalive 超时不匹配导致的半开);504 先看网关超时阈值与后端 P99,再顺调用链看是哪一跳变慢。只答"重启一下"就是放弃这题。

本章小结

关键结论一句话表述常见错误
HTTP/1.1 的队头阻塞在应用层请求-响应同连接串行,浏览器用 6 连接绕过,域名分片是这一限制的产物说成"TCP 层丢包阻塞”
管线化失败的根本原因是缺少请求标识响应必须有序且无 ID,一错就整条连接作废只说"因为队头阻塞",说不出为什么队头阻塞
HTTP/2 的复用靠 Stream ID帧头 31 位流标识让帧可交错可重组;HPACK 靠静态表+动态表+哈夫曼省字节以为 HTTP/2 改了 HTTP 语义
HTTP/2 仍有 TCP 层 HOL一个包丢失阻塞同连接所有流,等待时间是一个 RTT 量级认为多开连接能解决,或认为 HOL 已彻底消除
TLS 1.3 省一个 RTT 的关键是 key_share 前置客户端在 ClientHello 里先猜 DH 组,服务端即可在同一往返算出共享秘密说"因为算法更快所以省 RTT"
307/308 保方法,301/302 可变方法判定轴是"是否允许把 POST 改成 GET"认为 301 和 308 只差永久/临时

自测题

Q1(推导)一个页面需要加载 30 个同源静态资源。说明 HTTP/1.1 下最多能并发几个,域名分片为什么能提高这个数字、代价是什么。

答案与解析

HTTP/1.1 下浏览器按**源(origin = scheme + host + port)**限流,事实标准是每源 6 条并发连接,因此最多 6 个资源在传;域名分片把资源拆到 s1..s4.example.com,因为限流按源计算,4 个域名 → 4 × 6 = 24 条连接。

代价:每新增域名一次 DNS 解析与一次 TLS 握手(HTTPS 下尤其贵);连接池碎片化、空闲连接占用 fd 与内存;多条连接各自从慢启动开始,拥塞控制效率下降;HTTP/2 下分片会让资源分散到多条连接,破坏单连接多路复用与 HPACK 跨请求压缩收益,是负优化。

错误回答长什么样:答"因为浏览器限制 6 个连接,分片能开更多连接",但说不出"限流单位是源而不是 IP",也说不出 HTTP/2 下分片有害。面试官会追问"HTTP/2 时代还需要分片吗"——不需要,且应把 CDN 的 s1/s2 合并回主域。

Q2(辨析)请分别说明 HTTP/1.1 与 HTTP/2 的队头阻塞在哪一层、由什么造成,以及 HTTP/2 为什么不能靠"多开连接"缓解。

答案与解析
  • HTTP/1.1 的 HOL 在应用层:同一连接上请求-响应必须严格串行(响应按请求顺序返回,且响应里没有请求 ID 可用于重排),一个慢响应阻塞其后所有响应;启用管线化仍然存在,管线化甚至因为缺少请求标识而无法在出错时正确恢复。
  • HTTP/2 的 HOL 在传输层(TCP):所有流共享一条 TCP 连接,TCP 按序交付,因此一个报文段丢失后,即使后续字节已到达接收缓冲区,内核也不会交给应用层,所有流一起等待重传(RTT/RTO 量级);HPACK 解码是有序的,头部帧丢失还会连带阻塞后续头部解析。多开连接也救不了:HTTP/2 的设计前提就是单连接多路复用,多开连接会把 HPACK 的连接级动态表拆成多份(跨请求去重失效),流控与优先级也无法统一调度,等于退回 HTTP/1.1 的思路。

错误回答:“HTTP/2 也是队头阻塞,因为请求要排队。"——把两者混为一谈,层次说错后 HTTP/3 的动机就完全推不出来。面试官会追问"HTTP/3 彻底消除 HOL 了吗”——QUIC 消除的是跨流 HOL,同一流内部仍有序交付,流内丢包照样阻塞该流。

Q3(画图与解释)画出 TLS 1.2 与 TLS 1.3 完整握手的主要消息,标出各自往返数,并解释 TLS 1.3 为什么能少一个 RTT。

答案与解析

TLS 1.2(2-RTT):第 1 个往返 ClientHello → ServerHello + Certificate + ServerKeyExchange + ServerHelloDone;客户端此时才拿到服务端选定的组与参数,于是第 2 个往返发 ClientKeyExchange + ChangeCipherSpec + Finished,服务端回 ChangeCipherSpec + Finished。 TLS 1.3(1-RTT):ClientHello 里就带 key_share(猜定的 DH 组公钥);服务端 ServerHello 带回自己的 key_share,双方即可算出握手密钥,随后的 EncryptedExtensions + Certificate + CertificateVerify + Finished 全部加密发出;客户端紧接着就能发应用数据与自己的 Finished。

少一个 RTT 的原因:TLS 1.2 的密钥交换材料只能放在第 2 个往返,因为客户端必须先知道服务端选了哪个组;TLS 1.3 让客户端先猜一个组并把 key_share 放进 ClientHello,只要服务端接受该组,同一个往返就能确定共享秘密。猜错时服务端回 HelloRetryRequest,退回 2-RTT。 TLS 1.3 砍掉的东西:静态 RSA 密钥交换(改为 (EC)DHE,获得前向保密)、重协商、压缩、CBC 模式、自定义 DH 参数、ChangeCipherSpec。 错误回答:“TLS 1.3 用的加密算法更快,所以少一个 RTT。"——省 RTT 是协议消息编排的改变,不是算力问题。面试官会追问”Certificate 为什么加密"——服务端在 ServerHello 之后已能算出握手密钥,把证书放在密钥之后既修掉了"证书明文暴露访问域名"的隐私问题,也减少了被中间设备干预的面。

Q4(计算)链路跨大洲,RTT 约 150 ms;DNS 已缓存;服务端支持 TLS 1.3。计算从 TCP 建连到收到 HTTP 响应首字节的最少往返数(忽略丢包与慢启动),并说明改用 0-RTT 能省多少。

答案与解析

逐段计:TCP 三次握手 1 RTT(约 150 ms)→ TLS 1.3 完整握手 1 RTT(约 150 ms)→ HTTP 请求到首字节 1 RTT(约 150 ms),共 3 RTT ≈ 450 ms。 若已持有有效会话票据并用 0-RTT:TLS 握手不占额外往返,客户端可在 ClientHello 后立刻附带应用数据,变成 TCP 1 RTT + 应用数据 1 RTT = 2 RTT ≈ 300 ms,省约 150 ms。

对比 TLS 1.2(2-RTT 握手):总计 1 + 2 + 1 = 4 RTT ≈ 600 ms,即 TLS 1.3 相对 1.2 省约 150 ms,0-RTT 相对 1.2 省约 300 ms。

错误回答:把 DNS 也算进去(题目已说明已缓存),或把 TCP 建连与 TLS 握手合并成 1 个 RTT。另一个常见错误是认为 0-RTT 可用于任何请求——它可被重放且对 0-RTT 数据无前向保密,只能用于幂等请求。面试官会追问"PSK 复用(1-RTT)和 0-RTT 差在哪"——1-RTT 复用仍要等一个往返才发数据,但票据不承担"提前发送"的风险;0-RTT 把数据提前到第一个包,代价是可重放。

Q5(辨析)四个场景分别选哪个状态码并说明理由:① POST /orders 提交后跳转订单详情页;② 迁到新域名,以后都访问新域名,且该接口只接受 POST;③ 用户带合法 token 但无权访问该资源;④ 网关等待后端 3 s 无响应,自身超时阈值 2 s。

答案与解析

① 303 See Other。典型 PRG(Post/Redirect/Get)模式:POST 成功后要让浏览器用 GET 取结果页,避免刷新重复提交表单;303 明确要求改成 GET,语义最贴切。

② 308 Permanent Redirect。要求"永久"且"保持方法";301 虽永久但历史实现允许把 POST 改成 GET,会丢 body;307 保方法但只是临时。

③ 403 Forbidden。身份已由 token 证明,服务端知道"你是谁",只是该身份无权访问,再认证也无济于事。若返回 401 会误导客户端去刷新 token 或重新登录,形成无意义循环。

④ 504 Gateway Timeout。网关在 2 s 就超时放弃等待,属于"上游没有(及时)回答";与 502(上游的回答无效)区分——这里根本还没拿到响应。

错误回答:① 答 302——虽能工作,但 302 的方法语义是模糊的,规范不保证改成 GET;② 答 301——最典型的踩坑,POST 会被改写成 GET,参数全丢;③ 答 401——混淆"未认证"与"未授权";④ 答 502——混淆"回答不对"与"没回答"。面试官会追问"401 响应里必须有哪个头"——WWW-Authenticate,它告诉客户端应该用哪种认证方案;缺少它,401 的语义是不完整的。

面试高频问题与陷阱

面试问法考查角度陷阱 / 加分点对应本章小节
HTTP/1.1 的队头阻塞到底是什么?为什么浏览器要开 6 个连接?应用层串行的成因错误回答:“因为 TCP 丢包要重传,所以后面的请求被堵住。"——把传输层机制当成应用层问题。加分点:指出响应无请求 ID,无法重排18.3
HTTP/1.1 的管线化看起来能提升性能,为什么实际没人用?协议约束与部署现实错误回答:“因为服务端不支持。"——太笼统。要答出响应必须按序返回、中间设备兼容性差、缺少请求标识导致出错无法恢复18.2.2
域名分片能提升并发,为什么 HTTP/2 下反而要避免?限流单位与版本协同错误回答:“HTTP/2 也会分片,只是不需要了。"——没说出分片会让 HPACK 动态表与多路复用失效。加分点:答出应把域名收敛回主域18.3
HTTP/2 的多路复用是怎么实现的?帧、消息、流是什么关系?分帧层的数据结构错误回答:“就是把多个请求打包一起发。"——没有 Stream ID 这个核心。要答出 9 字节帧头、31 位流标识、帧属于流、消息由帧组成18.4.1、18.4.2
HTTP/2 已经多路复用了,为什么还需要 HTTP/3?TCP 层 HOL错误回答:“因为 HTTP/2 兼容性不好。"——完全答偏。核心是单条 TCP 连接上一个包丢失会阻塞所有流,且多开连接会破坏多路复用前提18.5
QUIC 为什么要基于 UDP?它不怕不可靠吗?中间设备僵化与用户态实现错误回答:“UDP 更快、不用握手。"——QUIC 自己实现了重传、拥塞控制与流控,比 TCP 更复杂。加分点:答出协议号被中间设备丢弃的僵化问题18.6.1
0-RTT 有什么风险?什么请求可以用?重放攻击与前向保密错误回答:“0-RTT 没有风险,直接用就行。“要答出可重放、对 0-RTT 数据无前向保密,只能用于幂等无副作用请求18.6.3、18.8.2
SNI 是干什么的?它有什么隐私问题?TLS 扩展与虚拟主机错误回答:“就是告诉服务端用哪张证书。"——方向对但没说到"HTTP 的 Host 在加密之后,握手时看不到域名”,也没提 SNI 明文泄露访问域名、需要 ECH18.9.3

延伸阅读

  • RFC 7230 / 7231 / 7232 / 7233 / 7234 / 7235:HTTP/1.1 报文语法、语义、条件请求、范围请求、缓存与认证(301/302/307/308、206、401 的权威定义)。
  • RFC 7540(HTTP/2)与 RFC 7541(HPACK):二进制分帧、流与优先级、头部压缩。
  • RFC 9114(HTTP/3)、RFC 9000(QUIC)、RFC 9002(QUIC 丢包检测与拥塞控制)、RFC 9204(QPACK)。
  • RFC 8446(TLS 1.3)、RFC 5246(TLS 1.2)、RFC 6066(SNI)、RFC 6797(HSTS)、RFC 6960(OCSP)。
  • RFC 6265(Cookie)、RFC 9218(HTTP 可扩展优先级)、RFC 8297(103 Early Hints);《HTTP/2 in ACTION》与《Bulletproof SSL and TLS》分别是帧与流、证书链与吊销机制的实务参考。
  • 第 16 章 16.116.5 节(TCP 建连与关闭)、第 17 章 17.417.6 节(重传与拥塞控制)、第 19 章 19.2 节(DNS 递归与迭代、curl -w 分层排查)。