2023-03-02 11:12:31

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 请求头经常包含大量重复内容,例如 cookieuser-agentaccept 等。HTTP/2 使用 HPACK 压缩头部,减少重复传输:

  • 静态表预置常见头部字段
  • 动态表记录连接中出现过的头部
  • 后续请求可以只传输索引和变化的部分

服务器推送

HTTP/2 还提供了服务器推送(Server Push):服务器可以在客户端请求 HTML 后,主动推送 CSS、JS 等资源。

不过服务器推送在实际工程中的收益并不稳定,容易推送客户端已经缓存的资源。 不过更经常使用 preloadprefetch 、缓存策略等方式来优化资源加载。

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 切到移动网络时,可以更平滑地保持连接

Reference