HTTP 消息

HTTP 消息是 HTTP 协议中用于在服务器和客户端之间交换数据的机制。消息有两种类型:客户端发送的请求,用于触发服务器上的操作;以及服务器为响应请求而发送的响应

开发者很少(如果不是从不)从零开始构建 HTTP 消息。浏览器、代理或 Web 服务器等应用程序使用专门设计的软件以可靠且高效的方式创建 HTTP 消息。消息的创建或转换通过浏览器中的 API、代理或服务器的配置文件或其他接口进行控制。

在 HTTP/2 及更早的 HTTP 协议版本中,消息是基于文本的,熟悉格式后相对容易阅读和理解。在 HTTP/2 中,消息被封装在二进制帧中,这使得它们稍微难以阅读。然而,协议的底层语义是相同的,因此您可以根据 HTTP/1.x 消息的文本格式来学习 HTTP 消息的结构和含义,并将这种理解应用于 HTTP/2 及更高版本。

本指南使用 HTTP/1.1 消息以增强可读性,并使用 HTTP/1.1 格式解释 HTTP 消息的结构。我们将在最后一节重点介绍您可能需要描述 HTTP/2 的一些差异。

注意:您可以在浏览器的开发者工具的网络选项卡中查看 HTTP 消息,或者在使用诸如 curl 等 CLI 工具将 HTTP 消息打印到控制台时查看。

HTTP 消息的组成

为了理解 HTTP 消息如何工作,我们将研究 HTTP/1.1 消息并检查其结构。以下插图显示了 HTTP/1.1 中的消息是什么样的

Requests and responses share a common structure in HTTP

请求和响应都具有相似的结构

  1. 一条起始行是一行,描述了 HTTP 版本以及请求方法或请求结果。
  2. 一组可选的HTTP 标头,包含描述消息的元数据。例如,对资源的请求可能包含该资源允许的格式,而响应可能包含指示实际返回格式的标头。
  3. 一个空行,表示消息的元数据已完成。
  4. 一个可选的正文,包含与消息关联的数据。这可能是请求中发送到服务器的 POST 数据,或者是响应中返回给客户端的某些资源。消息是否包含正文由起始行和 HTTP 标头决定。

HTTP 消息的起始行和标头统称为请求的头部,而其后包含其内容的部分称为正文

HTTP 请求

让我们看一个用户提交网页表单后发送的以下 HTTP POST 请求示例

http
POST /users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 49

name=FirstName+LastName&email=bsmth%40example.com

HTTP/1.x 请求中的起始行(上例中的POST /users HTTP/1.1)称为“请求行”,由三部分组成

http
<method> <request-target> <protocol>
<方法>

HTTP 方法(也称为HTTP 动词)是一组定义的单词之一,描述了请求的含义和期望的结果。例如,GET 表示客户端希望接收一个资源作为返回,而 POST 表示客户端正在向服务器发送数据。

<请求目标>

请求目标通常是绝对或相对的 URL,并由请求的上下文来描述。请求目标的格式取决于所使用的 HTTP 方法和请求上下文。下面 请求目标 部分对此进行了更详细的描述。

<协议>

HTTP 版本,它定义了剩余消息的结构,作为响应预期使用的版本的指示器。这几乎总是 HTTP/1.1,因为 HTTP/0.9HTTP/1.0 已过时。在 HTTP/2 及更高版本中,协议版本不包含在消息中,因为它从连接设置中即可得知。

请求目标

描述请求目标有几种方法,但最常见的是“源形式”。以下是目标类型及其使用场景的列表

  1. 源形式中,接收方将绝对路径与 Host 标头中的信息结合起来。查询字符串可以附加到路径中以获取额外信息(通常采用 key=value 格式)。这与 GETPOSTHEADOPTIONS 方法一起使用

    http
    GET /en-US/docs/Web/HTTP/Guides/Messages HTTP/1.1
    
  2. 绝对形式是一个完整的 URL,包括权限,与 GET 一起使用,当连接到代理时

    http
    GET https://mdn.org.cn/en-US/docs/Web/HTTP/Guides/Messages HTTP/1.1
    
  3. 权威形式是权威和端口用冒号 (:) 分隔。它仅与 CONNECT 方法一起使用,用于建立 HTTP 隧道时

    http
    CONNECT developer.mozilla.org:443 HTTP/1.1
    
  4. 星号形式仅与 OPTIONS 一起使用,当您想要表示整个服务器 (*) 而不是命名资源时

    http
    OPTIONS * HTTP/1.1
    

请求头

头是与请求一起发送的元数据,位于起始行之后和正文之前。在上面的表单提交示例中,它们是消息的以下几行

http
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 49

在 HTTP/1.x 中,每个标头都是一个不区分大小写的字符串,后跟一个冒号 (:) 和一个值,其格式取决于标头。整个标头,包括值,由一行组成。在某些情况下,例如 Cookie 标头,此行可能非常长。

Example of headers in an HTTP request

有些标头专门用于请求,而其他标头可以在请求和响应中发送,或者可能具有更具体的分类

  • 请求头为请求提供额外的上下文,或向服务器添加额外的逻辑来处理请求(例如,条件请求)。
  • 如果消息有正文,则会发送表示头,它们描述消息数据的原始形式以及应用的任何编码。这允许接收方理解如何在资源通过网络传输之前重新构建它。

请求体

请求正文是请求中向服务器传递信息的部分。只有 PATCHPOSTPUT 请求才具有正文。在表单提交示例中,这部分是正文

http
name=FirstName+LastName&email=bsmth%40example.com

表单提交请求中的正文包含相对少量的信息,以 key=value 对的形式,但请求正文可能包含服务器期望的其他类型的数据

json
{
  "firstName": "Brian",
  "lastName": "Smith",
  "email": "bsmth@example.com",
  "more": "data"
}

或多部分数据

http
--delimiter123
Content-Disposition: form-data; name="field1"

value1
--delimiter123
Content-Disposition: form-data; name="field2"; filename="example.txt"

Text file contents
--delimiter123--

HTTP 响应

响应是服务器为回复请求而发送回的 HTTP 消息。响应让客户端知道请求的结果是什么。这是一个 HTTP/1.1 响应 POST 请求的示例,该请求创建了一个新用户

http
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://example.com/users/123

{
  "message": "New user created",
  "user": {
    "id": 123,
    "firstName": "Example",
    "lastName": "Person",
    "email": "bsmth@example.com"
  }
}

起始行(上面是 HTTP/1.1 201 Created)在响应中称为“状态行”,包含三部分

http
<protocol> <status-code> <reason-phrase>
<协议>

消息的HTTP 版本

<状态码>

一个数字状态码,指示请求是成功还是失败。常见的状态码有200404302

<原因短语> 可选

状态码后面的可选文本是对状态的简短、纯粹信息性的文本描述,以帮助人们理解请求的结果。原因短语偶尔会用括号括起来(例如,“201 (Created)”),表示它是可选的。

响应头

响应头是与响应一起发送的元数据。在 HTTP/1.x 中,每个头都是一个不区分大小写的字符串,后跟一个冒号 (:) 和一个值,其格式取决于所使用的头。

Example of headers in an HTTP response

与请求头一样,响应中可能出现许多不同的头,它们被分类为

  • 响应头提供有关消息的额外上下文或添加额外的逻辑来指导客户端如何进行后续请求。例如,Server 等头包含有关服务器软件的信息,而 Date 则包含响应生成的时间。还有关于返回资源的信息,例如其内容类型 (Content-Type) 或如何缓存 (Cache-Control)。
  • 如果消息有正文,表示头描述消息数据的形式以及应用的任何编码。例如,同一个资源可能以特定媒体类型(如 XML 或 JSON)格式化,本地化为特定书面语言或地理区域,和/或压缩或以其他方式编码以进行传输。这允许接收方理解如何在资源通过网络传输之前重新构建它。

响应体

在响应客户端时,大多数消息都包含响应体。在成功的请求中,响应体包含客户端在 GET 请求中请求的数据。如果客户端请求有问题,响应体通常会描述请求失败的原因,并暗示是永久性还是暂时性。

响应体可以是

状态码回答请求但无需包含消息内容的响应,例如201 Created204 No Content,不包含正文。

HTTP/2 消息

HTTP/1.x 使用基于文本的消息,易于阅读和构建,但也因此有一些缺点。您可以使用 gzip 或其他压缩算法压缩消息体,但不能压缩标头。在客户端-服务器交互中,标头通常相似或相同,但在连接中的后续消息中重复。有许多已知的压缩重复文本的有效方法,这导致大量带宽节省未被利用。

HTTP/1.x 还有一个问题叫做队头阻塞(HOL),即客户端必须等待服务器的响应才能发送下一个请求。HTTP 管道试图解决这个问题,但由于支持不佳和复杂性,它很少被使用,而且很难正确实现。需要打开多个连接才能并发发送请求;并且由于 TCP 慢启动,已建立的繁忙连接比新的连接更高效。

在 HTTP/1.1 中,如果您想并行发出两个请求,则必须打开两个连接

Making two HTTP requests to a server in parallel

这意味着浏览器在可以同时下载和渲染的资源数量上受到限制,通常限制为 6 个并行连接。

HTTP/2 允许您使用单个 TCP 连接同时处理多个请求和响应。这是通过将消息封装到二进制帧中,并在连接上的编号中发送请求和响应来实现的。数据帧和头帧分开处理,这允许通过 HPACK 算法压缩头。使用同一个 TCP 连接同时处理多个请求称为多路复用

Multiplexing requests and responses in HTTP/2 using a single TCP connection.

请求不一定是顺序的:例如,流 9 不必等待流 7 完成。来自多个流的数据通常在连接上交错,因此客户端可以同时接收流 9 和流 7 的数据。协议有一种机制可以为每个流或资源设置优先级。当低优先级资源通过不同的流发送时,它们占用的带宽比高优先级资源少,或者如果存在应首先处理的关键资源,它们可以有效地在同一连接上按顺序发送。

总的来说,尽管 HTTP/2 在 HTTP/1.x 的基础上进行了所有改进和抽象,但开发者用于利用 HTTP/2 而非 HTTP/1.x 的 API 几乎不需要更改。当浏览器和服务器都支持 HTTP/2 时,它会自动启用并使用。

伪头

HTTP/2 消息的一个显著变化是使用了伪头。HTTP/1.x 使用消息起始行,而 HTTP/2 使用以 : 开头的特殊伪头字段。在请求中,有以下伪头

  • :method - HTTP 方法。
  • :scheme - 目标 URI 的方案部分,通常是 HTTP(S)。
  • :authority - 目标 URI 的授权部分。
  • :path - 目标 URI 的路径和查询部分。

在响应中,只有一个伪头,那就是 :status,它提供响应的状态码。

我们可以使用 nghttp 发送 HTTP/2 请求来获取 example.com,它将以更具可读性的形式打印出请求。您可以使用此命令发送请求,其中 -n 选项会丢弃下载的数据,-v 用于“详细”输出,显示帧的接收和传输

bash
nghttp -nv https://www.example.com

如果您向下浏览输出,您将看到每个传输和接收帧的时间

[  0.123] <send|recv> <frame-type> <frame-details>

我们不必深入研究此输出的太多细节,但请注意格式为 [ 0.123] send HEADERS frame ...HEADERS 帧。在标头传输后的几行中,您将看到以下行

http
[  0.447] send HEADERS frame ...
          ...
          :method: GET
          :path: /
          :scheme: https
          :authority: www.example.com
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.61.0

如果您已经熟悉使用 HTTP/1.x,并且本指南前面部分介绍的概念仍然适用,这应该看起来很熟悉。这是二进制帧,包含对 example.comGET 请求,由 nghttp 转换为可读形式。如果您进一步向下查看命令的输出,您将看到服务器接收到的一个流中的 :status 伪头

http
[  0.433] recv (stream_id=13) :status: 200
[  0.433] recv (stream_id=13) content-encoding: gzip
[  0.433] recv (stream_id=13) age: 112721
[  0.433] recv (stream_id=13) cache-control: max-age=604800
[  0.433] recv (stream_id=13) content-type: text/html; charset=UTF-8
[  0.433] recv (stream_id=13) date: Fri, 13 Sep 2024 12:56:07 GMT
[  0.433] recv (stream_id=13) etag: "3147526947+gzip"
...

如果您从此消息中删除时间和流 ID,它应该会更熟悉

http
:status: 200
content-encoding: gzip
age: 112721

深入研究消息帧、流 ID 以及连接的管理超出了本指南的范围,但为了理解和调试 HTTP/2 消息,您应该能够很好地使用本文中的知识和工具。

总结

本指南概述了 HTTP 消息的结构,并使用 HTTP/1.1 格式进行说明。我们还探讨了 HTTP/2 消息帧,它在 HTTP/1.x 语法和底层传输协议之间引入了一个层,而没有从根本上修改 HTTP 的语义。引入 HTTP/2 是为了通过启用请求的多路复用来解决 HTTP/1.x 中存在的队头阻塞问题。

HTTP/2 中仍然存在一个问题,那就是尽管协议层解决了队头阻塞问题,但由于 TCP 内部(传输层)的队头阻塞,仍然存在性能瓶颈。HTTP/3 通过使用 QUIC(一种基于 UDP 的协议)而不是 TCP 来解决此限制。这一改变提高了性能,减少了连接建立时间,并增强了在降级或不可靠网络上的稳定性。HTTP/3 保留了相同的核心 HTTP 语义,因此请求方法、状态码和标头等功能在所有三个主要 HTTP 版本中保持一致。

如果您理解 HTTP/1.1 的语义,那么您已经为掌握 HTTP/2 和 HTTP/3 奠定了坚实的基础。主要区别在于这些语义在传输层是如何实现的。通过遵循本指南中的示例和概念,您现在应该能够熟练地使用 HTTP 并理解消息的含义,以及应用程序如何使用 HTTP 发送和接收数据。

另见