2 min read
HTTP 协议的演进

HTTP/1 -> HTTP/1.1

短连接 & 长连接

HTTP/1.0 默认使用的是短连接,每一次请求都会发起一次 TCP 连接,请求响应后关闭 TCP 连接。

发起 TCP 连接 -> HTTP 请求 -> HTTP 响应 -> 关闭 TCP 连接

打开每一个 TCP 连接都是相当耗费资源的操作,HTTP/1.1 的关键改进点就是 TCP 连接的复用

在 HTTP/1.1 中,Connection: Keep-Alive 默认开启,使 TCP 连接保持一段时间(服务器可以使用 Keep-Alive 协议头设置一个最小的连接保持时间),可以用于重复发送 HTTP 请求,节省新建 TCP 连接握手的时间。

http/1.x连接

虽然在理论上 HTTP Pipeline 无需等待响应,但现实中由于代理兼容性差,队头阻塞严重等原因,现代浏览器没有默认启用 HTTP 流水线。

HTTP/1.1 存在的问题

TCP 长连接解决了 HTTP/1.0 中每次 HTTP 请求都需要发起 TCP 连接的问题,但是在一个 TCP 长连接中发起的多个 HTTP 请求存在 队头阻塞 问题。 即一个 HTTP 请求的响应太慢时,长连接内的所有 HTTP 请求需要排队等待(Application-level Head-of-Line Blocking)。 浏览器只能多开几个 TCP 连接来缓解这个问题,这也是早期雪碧图域名分片等技术产生的原因。

HTTP/1.1 -> HTTP/2

Binary Frame & Multiplexing

与 HTTP/1.x 在连接中使用明文请求不同,HTTP/2 将一个 TCP 连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。

Frame -> Stream -> Connection
Stream1 ┐
Stream2 ├→ 同一 TCP
Stream3 ┘

HPACK 算法

PACK算法是新引入HTTP/2的一个算法,用于对HTTP头部做压缩。其原理在于:

  • 客户端与服务端根据RFC 7541的附录A,维护一份共同的静态字典(Static Table),其中包含了常见头部名及常见头部名称与值的组合的代码;
  • 客户端和服务端根据先入先出的原则,维护一份可动态添加内容的共同动态字典(Dynamic Table);
  • 客户端和服务端根据RFC 7541的附录B,支持基于该静态哈夫曼码表的哈夫曼编码(Huffman Coding)。

HTTP/2 存在的问题

通过二进制分帧以及多路复用,HTTP/2 解决了 HTTP 层队头组测问题,但 TCP 层队头阻塞问题仍然存在。 原因在于 TCP 必须保证按顺序发送数据包,其中一个数据包发生丢包,则后续的数据包全部等待,即使这些数据包属于不同的 Stream。

HTTP/2 -> HTTP/3

与 HTTP/1.x、HTTP/2 不同,HTTP/3 不依赖 TCP 协议,而是使用基于 UDP 协议的 QUIC 协议实现。

HTTP/3

QUIC

UDP

QUIC 协议

简单来说,QUIC = 在用户态重写 TCP + TLS + HTTP/2 能力

  • 支持传输层级的多路复用:丢包只会影响数据真正丢失的 Stream,其他 Stream 不受影响,真正消除了队头阻塞
  • 内置 TLS1.3:QUIC 使协商密钥和支持的协议成为初始握手过程的一部分,客户端打开连接时,服务器响应的数据包包含了将来的数据包加密所需的数据。
  • 连接迁移:在网络切换场景(例如移动设备从 WiFi 切换到移动网络),QUIC 包含一个连接标识符,该标识符唯一地标识客户端与服务器之间的连接,因此无论源 IP 地址是什么,只需发送一个包含此 ID 的数据包即可重新建立连接。

HTTP 1/2/3 协议栈对比

总结

  • HTTP/1.1 通过持久连接减少 TCP 建立开销,但是存在应用层的队头阻塞。
  • HTTP/2 通过二进制分帧与多路复用解决 HTTP 层队头阻塞,但是受限于 TCP 的数据包顺序传输,仍存在传输层队头阻塞问题。
  • HTTP/3 基于 QUIC 协议,在 UDP 上实现可靠传输与多流控制,从传输层消除队头阻塞。
时代解决问题
HTTP/1.1减少 TCP 建立成本
HTTP/2提交连接利用率
HTTP/3解决 TCP 结构性限制