写在前面
上一篇讲了 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。
| |
TLS 严格说不是"传输层之上、应用层之下"的独立层(五层模型里它通常算应用层的一部分),但工程上它确实横在 HTTP 和 TCP 之间,对 HTTP 透明——HTTP 该怎么发还怎么发,只是字节流被加了一层加密。
HTTP 能一统 Web,靠的是无状态 + 请求-响应 + 文本可读 的极简模型。但这个模型在性能上踩过不少坑,每一次大版本升级都是在对这些坑打补丁。
二、HTTP/1.0:每个请求开一条连接
1996 年的 HTTP/1.0(RFC 1945)模型简单粗暴:
| |
默认每个请求单独开一条 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 连接,省掉了反复握手的开销:
| |
2. Host 头:虚拟主机的基石
请求行里加了 Host: www.example.com。这让一台服务器、一个 IP 上能托管成百上千个域名 —— 服务器靠 Host 头区分请求要给哪个站点。没有这一条,今天的云托管 / SaaS 多租户根本做不起来。
3. 管线化(pipelining):想法很美,落地很惨
1.1 允许客户端连发多个请求不等响应(流水线):
| |
听起来能省掉等待,但有两个致命问题:
- 应用层队头阻塞 —— 响应必须按请求顺序 返回,响应 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)。一个连接里可以混合传输属于不同请求的帧:
| |
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 上。
| |
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.0 | 1996 | TCP | 每请求一连接(默认) | 握手开销大、慢启动从头爬 |
| HTTP/1.1 | 1997 | TCP | keep-alive、Host 头、管线化、chunked | 应用层队头阻塞、管线化难落地 |
| HTTP/2 | 2015 | TCP | 二进制帧、多路复用、HPACK、推送 | TCP 层队头阻塞、推送已废弃 |
| HTTP/3 | 2022 | QUIC(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,明文握手主体
| |
握手干了四件事:
- 协商参数 —— 双方交换支持的版本、加密套件、两个随机数(Rc、Rs);
- 验证身份 —— 服务器发证书,客户端用本地信任库验证证书链(见下一节);
- 建立共享秘密并派生密钥 —— 以常见的 ECDHE 为例,双方各生成临时密钥对,用本端临时私钥和对端临时公钥算出相同的共享秘密,再结合 Client Random、Server Random 等握手上下文派生会话密钥。证书私钥和 ECDHE 临时私钥都只在本端使用、不会通过网络发送;“私钥永远不上线”并不准确,服务器通常在本机或 HSM 中使用证书私钥完成签名;
- 确认无误 —— 双方各发一个
Finished,包含整段握手的摘要,确保握手没被篡改。
之后用的对称密钥就是从这里来的。非对称(证书 + DH)只为这一步服务,因为它慢;后续大量数据用对称密钥加密。
TLS 1.3:1-RTT,握手主体被加密
TLS 1.3(RFC 8446,2018)做了大刀阔斧的精简:
| |
相比 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 信任:凭什么信这张证书
握手里客户端要"验证服务器证书",但这张证书凭什么可信?答案是信任链:
| |
验证过程是沿着链向上找:
- 服务器在握手里不只发自己的证书,而是发整条链(服务器证书 + 中间 CA 证书);
- 客户端用中间 CA 的公钥验证服务器证书的签名 → 验证通过,说明服务器证书确实由该中间 CA 签发;
- 再用根 CA 的公钥验证中间 CA 证书的签名 → 一路验到根 CA;
- 根 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 又是怎么自动分配地址的。
参考: