HTTP 协议版本演进 (1.0, 1.1, & 2.0)
HTTP/1.0 (1996年)
HTTP/1.0 的设计非常简单,但也因此带来了严重的性能问题。
核心特点
- 无法复用连接 (Connectionless)
- 队头阻塞 (Head-of-Line Blocking)
无法复用连接详解
在 HTTP/1.0 中,每个请求/响应对都需要一个完整独立的 TCP 连接生命周期。
流程:建立TCP连接 (三次握手) -> 发起请求 -> 服务器响应 -> 断开TCP连接 (四次挥手)
就好比每说一句话就要重新拨打一次电话,说完就挂断。如果一个页面包含 HTML、CSS、JS 和多张图片,就需要重复“拨号-通话-挂断”这个过程数十次。
带来的问题:
- 资源浪费: 频繁地创建和销毁 TCP 连接会大量消耗客户端与服务器的内存和 CPU 资源。
- 时间浪费: TCP 的三次握手和四次挥手本身消耗时间,导致请求的响应延迟增大。
- 带宽利用率低: TCP 连接有一个“慢启动”的特性,即连接刚建立时数据传输速度较慢,然后逐渐提升。HTTP/1.0 这种“短命”的连接方式,使得连接刚刚“热身”就被关闭,远未达到带宽的峰值速率。
HTTP/1.1 (1997年 - 至今)
HTTP/1.1 是使用时间最长的版本,它主要解决了 1.0 版本中连接无法复用的问题。
核心特点
- 长连接 (Persistent Connections)
- 管道化 (Pipelining) 与 队头阻塞
长连接详解
HTTP/1.1 默认启用长连接,允许在同一个 TCP 连接上发送和接收多个请求/响应。
这相当于电话接通后,双方约定先不挂断,可以持续交流多个话题。
实现方式:
- 在 HTTP/1.1 中,长连接是默认行为。
- 在 HTTP/1.1 之前,可以通过非标准的请求头
Connection: keep-alive来请求服务器保持连接。为了兼容,现代浏览器在发起 HTTP/1.1 请求时通常仍会带上此请求头。
GET /style.css HTTP/1.1
Host: example.com
Connection: keep-alive优点:
- 节约资源: 避免了频繁创建和销毁连接的开销。
- 提升速度: 省略了多次建立连接的时间。
- 充分利用带宽: 连接可以保持在“热”状态,充分利用 TCP 的性能,更容易达到带宽上限。
连接关闭时机:
- 客户端在最后一个请求中发送
Connection: close。 - 服务器端有超时机制(如
Keep-Alive-Timeout),若连接长时间空闲则会主动关闭。
管道化与队头阻塞
为了进一步提高效率,HTTP/1.1 引入了管道化技术,允许客户端在收到上一个响应前,连续发送多个请求。
问题所在: 虽然请求可以一起发送,但服务器必须严格按照接收请求的顺序来返回响应。这就导致了新的队头阻塞 (HOL Blocking)。
假设你连续发送了请求 A (一个大文件) 和请求 B (一个小图标)。服务器很快处理完了请求 B,但由于请求 A 还没处理完,服务器必须等待 A 的响应发送完毕后,才能发送 B 的响应。在这期间,B 的响应就被 A “阻塞”了。
由于管道化带来的问题可能比收益更大,现代浏览器默认关闭了此功能。
1.1 时代缓解队头阻塞的策略
- 文件合并: 将多个小图片合并成一张雪碧图 (CSS Sprites),将 JS/CSS 文件合并,以减少请求总数。
- 域名分片 (Domain Sharding): 浏览器对单个域名下的并发 TCP 连接数有限制 (通常是6个)。通过将资源分布在不同的域名下(如
img1.a.com,img2.a.com),可以建立更多的 TCP 连接,变相提高并发度。
HTTP/2.0 (2015年)
HTTP/2.0 的核心目标就是解决 HTTP/1.1 中未能解决的性能瓶颈,尤其是队头阻塞问题。它通常基于 HTTPS 实现。
核心特点
- 二进制分帧 (Binary Framing)
- 多路复用 (Multiplexing)
- 头部压缩 (Header Compression - HPACK)
- 服务器推送 (Server Push)
二进制分帧与多路复用
这是 HTTP/2.0 最革命性的改进。它彻底改变了数据的传输方式。
- 帧 (Frame): HTTP/2 中最小的数据传输单位。一个完整的请求或响应报文被拆分为多个二进制格式的帧。
- 流 (Stream): 一个双向的、由多个帧组成的序列,对应一个独立的请求/响应。每个流都有一个唯一的 ID。
工作原理:
- 所有请求和响应都被拆分成带有流ID的帧。
- 这些来自不同流的帧可以在一个 TCP 连接中交错传输。
- 接收方根据帧头中的流 ID,将它们重新组装成完整的请求或响应。
这好比多个人同时在一个微信群里聊天。虽然消息是交错出现的,但你知道每一句话是谁说的,不会混淆。HTTP/1.1 则像两个人用私信,必须等对方回复完一个问题,你才能得到下一个问题的答案。
结果: 实现了真正的多路复用。一个连接上可以同时承载多个请求和响应,并且一个慢请求不会阻塞其他请求。这从根本上解决了应用层的队头阻塞问题。
头部压缩 (HPACK)
HTTP/1.x 中,每次请求都会发送大量重复的头部信息 (如 User-Agent, Accept),造成了不必要的开销。HTTP/2.0 使用 HPACK 算法来解决此问题。
原理:
- 维护动态表和静态表: 客户端和服务器共同维护一个头部字段表。静态表包含常用头部,动态表则记录了此次连接中出现过的头部。
- 发送索引: 对于表中已有的头部,只需发送其索引(一个整数)即可,而无需发送完整的字符串。
- 哈夫曼编码: 对新的或未在表中的头部值使用哈夫曼编码进行压缩。
这种方式可以极大地减少头部数据的大小,通常能压缩 80% 以上。
服务器推送 (Server Push)
服务器可以在客户端请求之前,主动将它认为客户端会需要的资源推送过去。
例如,当客户端请求
index.html时,服务器可以预见到该页面会需要style.css和main.js,于是在响应index.html的同时,主动将这两个文件也推送给客户端。当客户端解析 HTML 发现需要这两个文件时,它们已经存在于本地缓存中,无需再发起新的请求。
面试题汇总
Q1: 简述 HTTP/1.0, 1.1, 2.0 的主要区别。
- HTTP/1.0: 短连接,每次请求/响应都新建一个 TCP 连接;存在队头阻塞。
- HTTP/1.1: 默认长连接,可复用 TCP 连接;通过管道化试图解决队头阻塞,但效果不佳,仍存在问题;引入了缓存处理、断点续传等。
- HTTP/2.0: 二进制分帧和多路复用,彻底解决应用层队头阻塞;头部压缩减少了请求开销;服务器推送可提前发送资源,提升加载速度。
Q2: 为什么 HTTP/1.1 不能实现多路复用?
根本原因在于其传输单位是整个 HTTP 报文。这个报文是作为一个整体,不可分割的。服务器必须按顺序处理和响应请求,以确保客户端能正确地将响应与请求对应起来。如果前一个响应很大或处理很慢,就会阻塞后面的响应,这就是队头阻塞。
Q3: 为什么说 HTTP/2.0 解决了队头阻塞,但 HTTP/3.0 又提出来解决队头阻塞?
- HTTP/2.0 解决了应用层的队头阻塞:通过多路复用,某个流(请求/响应)的缓慢不会影响其他流。
- 但它没解决 TCP 层的队头阻塞:HTTP/2.0 运行在 TCP 之上。TCP 是一个可靠的、按顺序传输的协议。如果在一个 TCP 连接中,某个数据包丢失了,TCP 协议会暂停后续所有数据包的交付(即使这些数据包属于不同的 HTTP/2 流),直到丢失的数据包被重传并成功接收。这就造成了整个连接上所有流的阻塞。
- HTTP/3.0 的解决方案:它放弃了 TCP,改用基于 UDP 的 QUIC 协议。QUIC 协议自身实现了流的概念和可靠性传输。QUIC 中的一个流丢包,不会影响其他流的传输,从而彻底解决了传输层的队头阻塞问题。