HTTP/1.0
HTTP/1.0 的核心问题是每个请求通常都需要创建一条新的 TCP 连接。
一次 TCP 连接需要经历建立连接、发送请求、接收响应、关闭连接等过程。如果页面中有 HTML、CSS、JS、图片等多个资源,就会反复创建和销毁连接,带来两个明显问题:
- 连接成本高:TCP 握手和慢启动都会增加首包时间
- 并发能力弱:请求需要排队等待,资源越多,页面加载越慢
HTTP/1.0 也可以通过 Connection: keep-alive 复用连接,但这不是默认行为,兼容性和实现方式也不统一。
HTTP/1.1
HTTP/1.1 将持久连接作为默认能力。同一个 TCP 连接可以承载多次请求和响应,减少了频繁建连带来的开销。
Connection: keep-alive
持久连接
在 HTTP/1.1 中,客户端和服务器默认不会在一次响应结束后立即关闭连接。连接可以被后续请求继续复用,直到出现以下情况:
- 客户端或服务器显式发送
Connection: close - 连接长时间空闲,服务器主动关闭
- 网络异常或进程退出导致连接中断
管道化
HTTP/1.1 还设计了管道化(Pipelining):客户端可以在没有收到前一个响应时,继续发送后续请求。
问题在于,服务器仍然必须按照请求顺序返回响应。如果第一个响应很慢,后面的响应即使已经处理完,也不能先返回。这就是 HTTP/1.1 的队头阻塞。
请求: A -> B -> C
响应: A -> B -> C
由于这个限制,浏览器通常不会真正启用 HTTP/1.1 管道化,而是通过对同一个域名建立多个 TCP 连接来提升并发能力。
常见优化
HTTP/1.1 时代常见的前端优化,本质上都是在绕开连接数量和队头阻塞的限制:
- 合并文件,减少请求数量
- 使用雪碧图,减少图片请求
- 域名分片,让浏览器建立更多并发连接
- 开启缓存,避免重复请求
HTTP/2
HTTP/2 最大的变化是引入二进制分帧层。它不再按纯文本请求和响应直接传输,而是把消息拆成更小的帧(Frame),再在同一个 TCP 连接中交错发送。
帧和流
HTTP/2 中有两个重要概念:
- 帧(Frame):传输的最小单位
- 流(Stream):一次完整的请求/响应,由多个帧组成
每个帧都会标记自己属于哪个流。因此,同一个 TCP 连接上可以同时传输多个请求和响应。
Stream 1: A1 ---- A2 ---- A3
Stream 3: ---- B1 ---- B2
Stream 5: C1 -------- C2
这就是多路复用。它解决了 HTTP/1.1 在应用层必须按顺序返回响应的问题。
头部压缩
HTTP 请求头经常包含大量重复内容,例如 cookie、user-agent、accept 等。HTTP/2 使用 HPACK 压缩头部,减少重复传输:
- 静态表预置常见头部字段
- 动态表记录连接中出现过的头部
- 后续请求可以只传输索引和变化的部分
服务器推送
HTTP/2 还提供了服务器推送(Server Push):服务器可以在客户端请求 HTML 后,主动推送 CSS、JS 等资源。
不过服务器推送在实际工程中的收益并不稳定,容易推送客户端已经缓存的资源。 不过更经常使用 preload、prefetch
、缓存策略等方式来优化资源加载。
HTTP/2 仍然存在的问题
HTTP/2 解决的是 HTTP 应用层的队头阻塞,但它仍然运行在 TCP 之上。
如果同一个 TCP 连接中某个数据包丢失,TCP 必须等待这个包重传完成,后续数据即使已经到达也不能交给上层应用。这就是 TCP 层的队头阻塞。
HTTP/3
HTTP/3 的主要变化是把底层传输协议从 TCP 换成 QUIC。QUIC 基于 UDP 实现,并在协议内部提供可靠传输、拥塞控制、加密和多路复用。
HTTP/3 主要解决 HTTP/2 仍然受 TCP 限制的问题:
- 减少连接建立成本:QUIC 内置 TLS 1.3,连接建立更快
- 改善队头阻塞:不同流之间相互独立,单个流丢包不会阻塞其他流
- 支持连接迁移:网络从 Wi-Fi 切到移动网络时,可以更平滑地保持连接