TCP/IP(十一):HTTP、HTTPS 与 TLS

写在前面

上一篇讲了 DNS——怎么把 example.com 这样的域名变成 IP。拿到 IP 之后,浏览器要做的下一件事就是:建连、发请求、拿数据。承载这套交互的,是应用层里最成功的协议——HTTP。

但光有 HTTP 还不够:它是明文的,链路上任何人都能偷看甚至篡改。于是有了 HTTPS,也就是 HTTP 套上一层 TLS。这一篇把两件事讲透:HTTP 从 1.0 一路演进到 3 到底在解决什么痛点,以及 TLS 那次"握手"究竟握了什么、为什么需要证书链。

本文是应用层的"主菜"。如果你更关心"一次完整的 API 请求在工程上怎么跑",可以回看本站的 后端全链路解剖;想深入 .NET 里 HTTP 客户端的坑(DNS 不刷新、socket 耗尽、连接池),看 HttpClient 的前世今生


一、应用层概览:HTTP 站在哪儿

回忆第五层模型:应用层直接跑在传输层之上。HTTP 绝大多数时候跑在 TCP 上(可靠传输对网页、API 来说是刚需),默认端口 80;加密后的 HTTPS 默认端口 443。

1
2
3
4
5
应用层:    HTTP / HTTPS  (应用报文)
              │  (HTTPS 在这里多一层 TLS,把 HTTP 加密后再交给 TCP)
传输层:    TCP(端口 80 / 443)

TLS 严格说不是"传输层之上、应用层之下"的独立层(五层模型里它通常算应用层的一部分),但工程上它确实横在 HTTP 和 TCP 之间,对 HTTP 透明——HTTP 该怎么发还怎么发,只是字节流被加了一层加密。

HTTP 能一统 Web,靠的是无状态 + 请求-响应 + 文本可读 的极简模型。但这个模型在性能上踩过不少坑,每一次大版本升级都是在对这些坑打补丁。


二、HTTP/1.0:每个请求开一条连接

1996 年的 HTTP/1.0(RFC 1945)模型简单粗暴:

1
2
3
4
5
6
Client                          Server
  │ ── TCP 三次握手 ──────────────▶ │
  │ ── HTTP 请求 ─────────────────▶ │
  │ ◀── HTTP 响应 ───────────────── │
  │ ── TCP 四次挥手(连接关闭)────▶ │
  │ ── 下一个请求?再握手一遍 ─────▶ │

默认每个请求单独开一条 TCP 连接,响应完就关。痛点很明显:

  • 每个请求都要一次 TCP 握手(1 个 RTT)+ 一次 TLS 握手(如果走 HTTPS,又是 1~2 个 RTT);
  • TCP 慢启动 每条新连接都要从头爬坡,根本用不上已经调好的拥塞窗口;
  • 一个页面几十个资源,就几十次握手,延迟堆叠非常可怕。

严格说,HTTP/1.0 并非完全不能复用连接——它有个 Connection: Keep-Alive 扩展头可以勉强长连,但非默认、且语义不统一。真正把长连接变成默认行为的是 1.1。


三、HTTP/1.1:keep-alive、Host 头与管线化

1997 年的 HTTP/1.1(RFC 2068,后由 RFC 2616 / 7230 系列修订)是生命力最长的一版,今天很多服务还跑在它上面。三大关键改进:

1. 持久连接(keep-alive)成为默认

连接不再用完即弃,而是默认保持打开,后续请求复用同一条 TCP 连接,省掉了反复握手的开销:

1
2
── TCP 握手 ──▶  请求1 → 响应1  →  请求2 → 响应2  →  ……  →  请求N → 响应N  ── TCP 关闭 ──▶
                 └──────────── 一条连接复用 ────────────┘

2. Host 头:虚拟主机的基石

请求行里加了 Host: www.example.com。这让一台服务器、一个 IP 上能托管成百上千个域名 —— 服务器靠 Host 头区分请求要给哪个站点。没有这一条,今天的云托管 / SaaS 多租户根本做不起来。

3. 管线化(pipelining):想法很美,落地很惨

1.1 允许客户端连发多个请求不等响应(流水线):

1
2
Client:请求1, 请求2, 请求3 ─────▶  Server
Client ◀──── 响应1, 响应2, 响应3

听起来能省掉等待,但有两个致命问题:

  • 应用层队头阻塞 —— 响应必须按请求顺序 返回,响应 1 慢了,2 和 3 再快也得堵在后面;
  • 代理 / 服务器兼容性差 —— 不少中间件对管线化支持有 bug,容易串响应。

结果:主流浏览器默认关闭管线化,实际生产里基本没人用,HTTP/2 的多路复用才是它的"正版替代品"。

1.1 还顺手加了 chunked transfer encoding(分块传输) —— 边生成边发,不用提前知道 Content-Length,对动态页面和流式响应很关键。


四、HTTP/2:二进制帧、多路复用与首部压缩

2015 年的 HTTP/2(RFC 7540,来自 Google 的 SPDY)做了一次大手术,但仍然跑在 TCP 上。四个核心特性:

1. 二进制分帧

抛弃 1.1 的文本格式,把每个请求/响应拆成一个个二进制帧(frame)。一个连接里可以混合传输属于不同请求的帧:

1
2
3
4
5
一条 TCP 连接
├── Stream 1:  帧 │ 帧 │     │ 帧 │
├── Stream 3:      │ 帧 │ 帧 │     │ 帧
├── Stream 5:  帧 │     │ 帧 │     │
                   ↑ 同一条连接里交织发帧

2. 多路复用(multiplexing)

这是 HTTP/2 的杀手锏。一条 TCP 连接上同时跑无数个并发请求/响应,每个请求是一个独立的 stream,互不阻塞。1.1 的应用层队头阻塞被干掉了——再也不用为了"并发"去开 6~8 条连接。

3. 首部压缩(HPACK)

1.1 每个请求都带一坨重复的 header(Cookie、User-Agent、Accept …)。HTTP/2 用 HPACK 算法压缩这些头部,还维护一张静态表 + 动态表,重复字段只传索引,能省下可观字节。

4. 服务端推送(Server Push)—— 已废弃

服务器可以在客户端请求 A 时,主动把 B、C 一起推过去(“你待会儿肯定要这俩”)。但实践中收益不稳、难调优、还浪费带宽,Chrome 从 106 版(2022 年)起默认禁用、随后彻底移除。今天它事实上已经死了,替代方案是 103 Early Hints

没解决的痛:TCP 层队头阻塞

HTTP/2 解决了应用层 的队头阻塞,但底层还是一条 TCP 连接。一旦某个 TCP 包丢了,TCP 会卡住整条连接 等重传——所有 stream 上的帧(哪怕本身已经到了接收端)都得等着。并发越多,这个"被一个丢哥拖垮全军"的问题越明显。这正是 HTTP/3 要解决的。


五、HTTP/3:扔掉 TCP,跑在 QUIC 上

2022 年正式发布的 HTTP/3(RFC 9114)做了一个激进决定:不再跑 TCP,改跑 QUIC,而 QUIC 跑在 UDP 上

1
2
HTTP/2:   HTTP  →  TLS  →  TCP  →  IP
HTTP/3:   HTTP  →  QUIC(内建 TLS 1.3,跑在 UDP)  →  IP

QUIC(RFC 9000)把传输层和加密层揉在了一起 重新设计,带来几个关键收益:

特性解决了什么
独立 stream,无 TCP 队头阻塞一个 stream 丢包只卡它自己,其他 stream 照跑
集成 TLS 1.3握手和传输合并,1-RTT 建连,复用时 0-RTT
连接迁移手机从 Wi-Fi 切 4G,IP 变了连接不断(靠 Connection ID,不靠四元组)
0-RTT 数据对访问过的服务器,首个请求的数据能和握手一起发出去

代价是部署上:QUIC 跑 UDP,而不少企业防火墙 / 中间盒对 UDP 不友好(限速、直接丢),所以实践中 HTTP/3 的升级往往需要先 HTTP/2 协商 Alt-Svc 再尝试切换。

各版本一句话对比:1.0 一请求一连接,1.1 默认长连 + Host,HTTP/2 多路复用但卡在 TCP,HTTP/3 干脆换传输层彻底解决队头阻塞

版本年份传输层关键特性主要痛点
HTTP/1.01996TCP每请求一连接(默认)握手开销大、慢启动从头爬
HTTP/1.11997TCPkeep-alive、Host 头、管线化、chunked应用层队头阻塞、管线化难落地
HTTP/22015TCP二进制帧、多路复用、HPACK、推送TCP 层队头阻塞、推送已废弃
HTTP/32022QUIC(UDP)独立流、集成 TLS 1.3、0-RTT、连接迁移UDP 在部分网络 / 防火墙受限

六、HTTPS = HTTP + TLS:为什么需要这层壳

HTTP 本身不加密,明文跑在网络上意味着:

  • 能被窃听 —— 链路上的任何一跳都能看到你传了什么(密码、token、业务数据);
  • 能被篡改 —— 中间人可以改响应、插广告、劫持跳转;
  • 无法验证身份 —— 你连的 bank.com 到底是真银行还是钓鱼站,HTTP 自己说了不算。

TLS(Transport Layer Security,前身 SSL)补三件事,正好对应信息安全的三大目标:

TLS 提供对应威胁怎么做到
机密性(加密)窃听协商出对称密钥,加密应用数据
身份认证钓鱼 / 中间人冒充服务器出示证书,由受信任的 CA 签名
完整性篡改每条记录带 MAC / AEAD 标签,改一个字节都验不过

所以 HTTPS 不是"一种新协议",而是 HTTP 的字节流先经 TLS 加密,再交给 TCP。对 HTTP 来说它完全透明。


七、TLS 握手:到底"握"了什么

TLS 握手要解决一个核心难题:双方在一条不安全的信道上,协商出一个只有他们俩知道的对称密钥,同时让客户端确认服务器的身份。下面分别看 1.2 和 1.3。

TLS 1.2:2-RTT,明文握手主体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Client                                              Server
  │                                                  │
  │ ── ClientHello (支持的 TLS 版本、加密套件、随机数 Rc) ─▶ │
  │                                                  │
  │ ◀── ServerHello (选定套件、随机数 Rs) ───────────────── │
  │ ◀── Certificate (服务器证书) ────────────────────────── │
  │ ◀── ServerKeyExchange (DH 参数,视套件而定) ─────────── │
  │ ◀── ServerHelloDone ─────────────────────────────────── │   ← 第 1 个 RTT 结束
  │                                                  │
  │ ── ClientKeyExchange (DH 公开值) ──────────────────▶ │
  │ ── ChangeCipherSpec ──────────────────────────────────▶ │
  │ ── Finished (已加密,含握手摘要) ──────────────────────▶ │
  │                                                  │
  │ ◀── ChangeCipherSpec ────────────────────────────────── │
  │ ◀── Finished (已加密) ────────────────────────────────── │   ← 第 2 个 RTT 结束
  │                                                  │
  │ ◀═══════ 加密的应用数据(HTTP) ═════════════════════▶ │

握手干了四件事:

  1. 协商参数 —— 双方交换支持的版本、加密套件、两个随机数(Rc、Rs);
  2. 验证身份 —— 服务器发证书,客户端用本地信任库验证证书链(见下一节);
  3. 建立共享秘密并派生密钥 —— 以常见的 ECDHE 为例,双方各生成临时密钥对,用本端临时私钥和对端临时公钥算出相同的共享秘密,再结合 Client Random、Server Random 等握手上下文派生会话密钥。证书私钥和 ECDHE 临时私钥都只在本端使用、不会通过网络发送;“私钥永远不上线”并不准确,服务器通常在本机或 HSM 中使用证书私钥完成签名;
  4. 确认无误 —— 双方各发一个 Finished,包含整段握手的摘要,确保握手没被篡改。

之后用的对称密钥就是从这里来的。非对称(证书 + DH)只为这一步服务,因为它慢;后续大量数据用对称密钥加密。

TLS 1.3:1-RTT,握手主体被加密

TLS 1.3(RFC 8446,2018)做了大刀阔斧的精简:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Client                                              Server
  │                                                  │
  │ ── ClientHello (套件、随机数、密钥共享 KeyShare) ────▶ │
  │     (附上客户端的 DH 公开值,不再等)                  │
  │                                                  │
  │ ◀── ServerHello (套件、随机数、KeyShare) ────────────── │
  │     ▼ 从这条消息之后,所有握手记录全部加密 ▼            │
  │ ◀── {EncryptedExtensions} ─────────────────────────── │
  │ ◀── {Certificate} ──────────────────────────────────── │
  │ ◀── {CertificateVerify} (用证书私钥签名) ───────────── │
  │ ◀── {Finished} ─────────────────────────────────────── │   ← 第 1 个 RTT 结束
  │                                                  │
  │ ── {Finished} ──────────────────────────────────────▶ │
  │                                                  │
  │ ◀═══════ 加密的应用数据(HTTP) ═════════════════════▶ │

相比 1.2,1.3 有三个显著改进:

  • 少一个 RTT —— 客户端在 ClientHello 里就带上密钥共享(KeyShare),服务器第一个回包就能算出密钥、开始加密,握手压到 1-RTT
  • 握手加密 —— ServerHello 之后的所有消息都加密了,连证书都不再明文暴露(防被动监听窥探服务器证书);
  • 0-RTT 恢复 —— 如果是复用之前的会话(PSK),客户端可以在 ClientHello直接捎带应用数据,连那 1 个 RTT 都省了。代价是 0-RTT 数据不具备完整的前向保密保证,而且可能跨连接被重放,因此只能发送应用明确判定为可安全重放的数据。幂等请求常是候选,但“幂等”不自动等于“重放安全”,仍要结合计费、审计、限额等业务副作用判断。

1-RTT / 0-RTT 看似只是省了一个往返,但在移动互联网和高延迟链路上,每省一个 RTT 都直接转化成更快的首字节时间(TTFB)。HTTP/3 把 QUIC 和 TLS 1.3 揉在一起,很大程度就是为了把这几个 RTT 彻底榨干。


八、证书链与 CA 信任:凭什么信这张证书

握手里客户端要"验证服务器证书",但这张证书凭什么可信?答案是信任链

1
2
3
4
5
你的服务器证书            中间 CA 证书             根 CA 证书
example.com           ──签发──▶  Let's Encrypt R3  ──签发──▶  ISRG Root X1
                                          (根证书自签名,预装在
                                           操作系统 / 浏览器信任库里)

验证过程是沿着链向上找

  1. 服务器在握手里不只发自己的证书,而是发整条链(服务器证书 + 中间 CA 证书);
  2. 客户端用中间 CA 的公钥验证服务器证书的签名 → 验证通过,说明服务器证书确实由该中间 CA 签发;
  3. 再用根 CA 的公钥验证中间 CA 证书的签名 → 一路验到根 CA
  4. 根 CA 是自签名 的,它的证书预装在操作系统 / 浏览器的信任库里(Windows、macOS、Firefox、Android 各自维护一份)。只要根在信任库里,这条链就算可信。

信任的根基是"你的设备信任哪些根 CA"。这套体系(PKI,Public Key Infrastructure)把"信任某个网站"简化成"信任几百个根 CA",而根 CA 再通过中间 CA 把签名能力分级下发。

证书可能出问题:

  • 过期 —— 证书有有效期,过期了浏览器会拦截。线上事故里"证书忘续期"是高频低级错误,运维必须配监控自动续期(Let’s Encrypt + certbot / cert-manager)。
  • 吊销 —— 私钥泄露或站点停业,CA 要把证书提前作废。机制有 CRL(吊销列表,已少用)和 OCSP(在线查状态);但都偏慢,现代浏览器倾向用 OCSP Stapling(服务器主动把 OCSP 响应钉在握手里)。

为什么要有中间 CA,不让根直接签?因为根私钥是整个体系的命根子,必须离线存放、极少使用。日常签发全交给中间 CA,根只在签发/轮换中间 CA 时才出动——一旦根私钥泄露,整个信任体系崩塌。


小结

  • HTTP/1.0 一请求一连接、握手开销大;HTTP/1.1 默认 keep-alive、加 Host 头、引入管线化(但几乎没人用);
  • HTTP/2 用二进制帧 + 多路复用 + HPACK 干掉了应用层队头阻塞,服务端推送已废弃;但底层仍是 TCP,丢包会卡住整条连接
  • HTTP/3 换到 QUIC(UDP),用独立 stream 彻底解决 TCP 队头阻塞,集成 TLS 1.3 实现 1-RTT / 0-RTT 和连接迁移;
  • HTTPS = HTTP + TLS,TLS 提供加密、身份认证、完整性,对应机密性、真实性、防篡改三个目标;
  • TLS 1.2 要 2-RTT、握手明文;TLS 1.3 精简到 1-RTT、握手加密、支持 0-RTT(注意重放风险);
  • 证书靠 CA 信任链 取信——服务器证书 ← 中间 CA ← 根 CA(自签名、预装在设备信任库),信任的根基是"你的设备信哪些根 CA"。

下一篇:TCP/IP(十二):NAT、DHCP 与地址机制 —— 私网 IP 怎么靠 NAT 出网、为什么家用路由器一个公网 IP 能带几十台设备、DHCP 又是怎么自动分配地址的。


参考:

Licensed under CC BY-NC-SA 4.0