关于 HTTP 的一切你需要知道的内容

更新于 2026-01-27

cs.fyi 2021-02-16

HTTP 是每个 Web 开发者都必须了解的协议,因为它驱动着整个互联网。掌握 HTTP 肯定能帮助你开发出更优秀的应用程序。

在本文中,我将讨论什么是 HTTP、它的起源、当前的发展状况,以及我们是如何走到今天的。

什么是 HTTP?

首先,什么是 HTTP?HTTP 是一种基于 TCP/IP 的应用层通信协议,它标准化了客户端与服务器之间如何进行通信。它定义了内容如何在互联网上被请求和传输。所谓“应用层协议”,是指它只是一个抽象层,用于标准化主机(客户端和服务器)之间的通信方式。HTTP 本身依赖于 TCP/IP 来在客户端和服务器之间传输请求和响应。默认情况下使用 TCP 端口 80,但也可以使用其他端口。而 HTTPS 则使用端口 443。

HTTP/0.9 —— 单行协议(1991 年)

HTTP 的第一个有文档记录的版本是 1991 年提出的 HTTP/0.9。这是有史以来最简单的协议:仅包含一个名为 GET 的方法。如果客户端需要访问服务器上的某个网页,它会发出如下简单的请求:

GET /index.html

服务器的响应则如下所示:

(响应体)
(连接关闭)

也就是说,服务器收到请求后,会以 HTML 形式回复内容,一旦内容传输完毕,连接就会立即关闭。该版本具有以下特点:

  • 没有头部(headers)
  • 仅允许使用 GET 方法
  • 响应必须是 HTML

如你所见,这个协议除了为后续发展奠定基础之外,几乎没有任何功能。

HTTP/1.0 —— 1996 年

1996 年,HTTP 的下一个版本 HTTP/1.0 出现,相比原始版本有了巨大改进。

与仅支持 HTML 响应的 HTTP/0.9 不同,HTTP/1.0 现在可以处理其他响应格式,例如图像、视频文件、纯文本或任何其他内容类型。它增加了更多方法(如 POST 和 HEAD),请求/响应格式发生了变化,请求和响应中都加入了 HTTP 头部,引入了状态码以标识响应类型,还加入了字符集支持、多部分类型(multipart types)、身份验证、缓存、内容编码等特性。

下面是一个 HTTP/1.0 请求和响应的示例:

请求示例:

GET / HTTP/1.0
Host: cs.fyi
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

如你所见,除了请求本身,客户端还发送了其自身信息、所需响应类型等。而在 HTTP/0.9 中,由于没有头部,客户端无法发送此类信息。

对应的响应可能如下所示:

HTTP/1.0 200 OK 
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

(响应体)
(连接关闭)

响应开头是 HTTP/1.0(HTTP 后跟版本号),然后是状态码 200 及其原因短语(即状态码的描述)。

在这一新版本中,请求和响应头部仍采用 ASCII 编码,但响应体可以是任意类型,如图像、视频、HTML、纯文本或其他内容类型。因此,服务器现在可以向客户端发送任意内容类型;正因如此,在该版本推出不久后,“超文本”(Hyper Text)一词在 HTTP 中就变得名不副实了。也许 HMTP(Hypermedia Transfer Protocol,超媒体传输协议)会更贴切,但我想我们这辈子都只能沿用这个名字了。

HTTP/1.0 的一个主要缺点是:每个连接只能处理一个请求。也就是说,每当客户端需要从服务器获取某些内容时,就必须建立一个新的 TCP 连接;一旦该请求完成,连接就会关闭。对于下一次请求,又必须建立新的连接。为什么这很糟糕?假设你访问一个网页,该页面包含 10 张图片、5 个样式表和 5 个 JavaScript 文件,总共需要获取 20 项资源。由于服务器在每次请求完成后都会关闭连接,因此需要建立 20 个独立的连接,逐个获取这些资源。这种大量连接会导致严重的性能损失,因为每次新建 TCP 连接都需要进行三次握手(three-way handshake)并经历慢启动(slow-start)过程。

三次握手(Three-way Handshake)

简单来说,所有 TCP 连接都以三次握手开始,客户端和服务器在开始传输应用数据前先交换一系列数据包:

  1. SYN:客户端选择一个随机数(例如 x),并将其发送给服务器。
  2. SYN-ACK:服务器通过向客户端发送 ACK 数据包来确认请求,其中包含服务器自己选择的随机数(例如 y)以及客户端发送的数字加 1(即 x+1)。
  3. ACK:客户端将收到的服务器随机数 y 加 1,并将 ACK 数据包(y+1)发回服务器。

三次握手完成后,客户端和服务器之间就可以开始传输数据了。需要注意的是,客户端在发送最后一个 ACK 数据包后即可开始发送应用数据,但服务器仍需等到收到该 ACK 数据包后才能处理请求。

然而,一些 HTTP/1.0 的实现尝试通过引入一个新的头部 Connection: keep-alive 来克服这个问题,该头部的作用是告诉服务器:“嘿,别关这个连接,我还要用!” 但这种机制并未被广泛支持,问题依然存在。

此外,HTTP 还是一种无状态协议(stateless protocol),即服务器不会保留关于客户端的信息。因此,每个请求都必须包含服务器完成该请求所需的全部信息,而不能依赖之前的请求。这进一步加剧了问题:除了需要建立大量连接外,客户端还必须在每次请求中重复发送冗余数据,从而增加了带宽消耗。

HTTP/1.1 —— 1997 年(实际发布于 1999 年)

在 HTTP/1.0 发布仅仅三年后,HTTP/1.1 于 1999 年发布,相比前一版本做出了大量改进。主要改进包括:

  • 新增 HTTP 方法:引入了 PUT、PATCH、OPTIONS、DELETE 等方法。
  • 主机名识别:在 HTTP/1.0 中,Host 头部不是必需的,但在 HTTP/1.1 中变为强制要求。
  • 持久连接(Persistent Connections):如前所述,HTTP/1.0 每个连接只能处理一个请求,完成后即关闭,导致严重的性能和延迟问题。HTTP/1.1 引入了持久连接,即连接默认保持打开状态,允许多个连续请求复用同一个连接。若要关闭连接,请求中必须包含 Connection: close 头部。客户端通常在最后一个请求中发送此头部以安全关闭连接。
  • 管道化(Pipelining):HTTP/1.1 还支持管道化,即客户端可以在同一连接上连续发送多个请求,而无需等待服务器的响应;服务器则必须按请求接收的顺序返回响应。你可能会问:客户端如何知道第一个响应在哪里结束、下一个响应从哪里开始?为解决此问题,响应中必须包含 Content-Length 头部,客户端可据此判断响应边界。

需要注意的是,要充分利用持久连接或管道化,响应中必须包含 Content-Length 头部,这样客户端才能知道传输何时完成,从而发送下一个请求(在普通顺序请求模式下)或开始等待下一个响应(在启用管道化时)。

但这种方法仍存在问题:如果数据是动态生成的,服务器在传输开始前无法预知内容长度怎么办? 在这种情况下,你就无法享受持久连接的优势了!为解决此问题,HTTP/1.1 引入了分块传输编码(Chunked Encoding)。在这种情况下,服务器可以省略 Content-Length,转而使用分块编码(稍后详述)。但如果两者都未提供,则必须在请求结束时关闭连接。

  • 分块传输(Chunked Transfers):对于动态内容,当服务器在传输开始时无法确定 Content-Length 时,它可以将内容分成多个块(chunk)逐个发送,并在每个块发送时附带该块的长度。当所有块发送完毕(即整个传输完成)后,服务器会发送一个空块(即 Content-Length 为零的块)来通知客户端传输已完成。为告知客户端使用分块传输,服务器会在响应中包含 Transfer-Encoding: chunked 头部。

  • 身份验证增强:HTTP/1.0 仅支持基本身份验证(Basic Authentication),而 HTTP/1.1 增加了摘要身份验证(Digest Authentication)和代理身份验证(Proxy Authentication)。

  • 其他改进:还包括缓存机制、字节范围请求(Byte Ranges)、字符集支持、语言协商、客户端 Cookie、增强的压缩支持、新状态码等。

我不会在本文中深入探讨 HTTP/1.1 的所有特性,因为这本身就是一个庞大的话题,而且你已经可以找到大量相关资料。我推荐你阅读的一份文档是《HTTP/1.0 与 HTTP/1.1 的关键区别》,而对于追求极致的读者,这里是原始 RFC 的链接。

HTTP/1.1 自 1999 年发布以来,长期作为标准使用。尽管它相比前代有了显著改进,但随着 Web 的飞速发展,其局限性也逐渐显现。如今加载一个网页比以往任何时候都更耗费资源。一个简单的网页现在可能需要建立超过 30 个连接。你可能会问:“HTTP/1.1 不是支持持久连接吗?为什么还需要这么多连接?” 原因在于,HTTP/1.1 在任意时刻只能有一个未完成的请求(outstanding request)。虽然 HTTP/1.1 通过引入管道化试图解决此问题,但由于队头阻塞(Head-of-Line Blocking) 问题,效果并不理想:一个缓慢或大型的请求会阻塞其后的所有请求,一旦某个请求在管道中卡住,后续请求就必须等待。为克服 HTTP/1.1 的这些缺陷,开发者们开始采用各种变通方案,例如使用精灵图(spritesheets)、在 CSS 中嵌入编码图像、合并成单个巨大的 CSS/JavaScript 文件、域名分片(domain sharding)等。

SPDY —— 2009 年

Google 开始尝试开发替代协议,以加快 Web 速度、提升 Web 安全性并降低网页延迟。2009 年,他们宣布了 SPDY。

SPDY 是 Google 的商标,并非缩写。

研究发现,当我们不断增加带宽时,网络性能起初会提升,但达到某个点后,性能增益就不再明显。然而,如果我们持续降低延迟,性能则会持续提升。这正是 SPDY 背后的核心理念:通过降低延迟来提升网络性能

对于不了解的人来说,延迟(latency)是指数据从源到目的地所需的时间(以毫秒为单位),而带宽(bandwidth)是指每秒可传输的数据量(以比特/秒为单位)。

SPDY 的特性包括多路复用(multiplexing)、压缩(compression)、优先级(prioritization)、安全性等。我不会深入讲解 SPDY 的细节,因为当你阅读下一节关于 HTTP/2 的内容时就会明白——正如我所说,HTTP/2 主要受 SPDY 启发。

SPDY 并未试图取代 HTTP;它是在 HTTP 之上的一层转换层,位于应用层,在数据发送到网络之前对请求进行修改。它逐渐成为事实标准,大多数浏览器都开始实现它。

2015 年,Google 不希望同时存在两个竞争标准,因此决定将其融入 HTTP,从而诞生了 HTTP/2,并弃用了 SPDY。

HTTP/2 —— 2015 年

到目前为止,你应该已经理解为什么我们需要再次修订 HTTP 协议了。HTTP/2 的设计目标是实现低延迟的内容传输。相比旧版 HTTP/1.1,其关键特性或差异包括:

  1. 二进制而非文本
  2. 多路复用(Multiplexing):在单个连接上支持多个异步 HTTP 请求
  3. 使用 HPACK 的头部压缩
  4. 服务器推送(Server Push):单个请求可触发多个响应
  5. 请求优先级(Request Prioritization)
  6. 安全性

1. 二进制协议

HTTP/2 通过将其变为二进制协议来解决 HTTP/1.x 中存在的高延迟问题。作为二进制协议,它更容易解析,但不像 HTTP/1.x 那样可被人眼直接阅读。HTTP/2 的主要构建模块是帧(Frames)流(Streams)

HTTP 消息现在由一个或多个帧组成。有用于元数据的 HEADERS 帧和用于有效载荷的 DATA 帧,还有其他几种帧类型(如 HEADERSDATARST_STREAMSETTINGSPRIORITY 等),你可以在 HTTP/2 规范中查阅。

每个 HTTP/2 请求和响应都被分配一个唯一的流 ID,并被划分为多个帧。帧本质上是二进制数据片段。一组帧构成一个流(Stream)。每个帧都有一个流 ID,用于标识其所属的流,且每个帧都有一个公共头部。此外,值得注意的是,由客户端发起的请求使用奇数流 ID,而服务器的响应使用偶数流 ID

除了 HEADERSDATA 帧外,我认为值得一提的另一种帧类型是 RST_STREAM,这是一种特殊帧,用于中止某个流。例如,客户端可以发送此帧告知服务器:“我不再需要这个流了。” 在 HTTP/1.1 中,唯一让服务器停止发送响应的方法是关闭整个连接,这会导致后续请求必须重新建立连接,从而增加延迟。而在 HTTP/2 中,客户端可以使用 RST_STREAM 停止接收特定流,而连接仍然保持打开,其他流仍可继续使用。

2. 多路复用(Multiplexing)

由于 HTTP/2 现在是二进制协议,并且如上所述使用帧和流来处理请求和响应,因此一旦 TCP 连接建立,所有流都可以通过同一个连接异步发送,无需建立额外连接。服务器也以同样的异步方式响应,即响应没有固定顺序,客户端通过分配的流 ID 来识别每个数据包所属的流。这也解决了 HTTP/1.x 中存在的队头阻塞问题:客户端不必等待耗时较长的请求,其他请求仍可继续处理。

3. 头部压缩

头部压缩是单独一份 RFC 的核心内容,专门用于优化发送的头部。其核心思想是:当我们从同一客户端反复访问服务器时,头部中会包含大量重复数据,有时 Cookie 还会进一步增大头部体积,从而增加带宽消耗和延迟。为解决此问题,HTTP/2 引入了头部压缩。

与请求和响应不同,头部并非使用 gzip 或 compress 等格式进行压缩,而是采用了一种不同的机制:字面值使用霍夫曼编码(Huffman coding)进行编码,同时客户端和服务器各自维护一个头部表。在后续请求中,双方会省略重复的头部(如 User-Agent 等),而是通过维护的头部表进行引用。

顺便提一下,虽然我们谈到了头部,但 HTTP/2 的头部与 HTTP/1.1 基本相同,只是增加了一些伪头部(pseudo headers),即 :method:scheme:host:path

4. 服务器推送(Server Push)

服务器推送是 HTTP/2 的另一项强大功能:服务器可以预判客户端即将请求某些资源,并主动将其推送给客户端,而无需客户端显式请求。例如,当浏览器加载一个网页时,它需要先解析整个页面,找出需要从服务器加载的远程资源,然后依次发送请求。

服务器推送允许服务器通过提前推送这些资源来减少往返次数。具体实现方式是:服务器发送一个名为 PUSH_PROMISE 的特殊帧,通知客户端:“嘿,我即将把这个资源发给你!别再问我了。” PUSH_PROMISE 帧与触发推送的流相关联,并包含一个“承诺的流 ID”(promised stream ID),即服务器将在此流上发送要推送的资源。

5. 请求优先级(Request Prioritization)

客户端可以通过在打开流的 HEADERS 帧中包含优先级信息来为流分配优先级。在任何时候,客户端也可以发送 PRIORITY 帧来更改流的优先级。

如果没有优先级信息,服务器会异步处理请求(即无序处理)。如果为流分配了优先级,服务器则会根据该优先级信息决定为哪个请求分配多少资源。

6. 安全性

关于是否应强制要求 HTTP/2 使用 TLS(即加密)曾有过广泛讨论。最终决定不强制要求。然而,大多数厂商表示,他们只会在使用 TLS 时才支持 HTTP/2。因此,尽管 HTTP/2 规范本身不要求加密,但实际上它已默认成为强制要求

此外,当 HTTP/2 在 TLS 上实现时,还施加了一些要求:必须使用 TLS 1.2 或更高版本,密钥长度必须达到最低要求,必须使用临时密钥(ephemeral keys)等。