HTTP 请求的性质

许多开发人员发现 HTTP 请求被抽象化了;事实上,许多 PHP 开发人员永远不需要了解 HTTP 请求在引擎盖下的实际工作原理。

PHP 开发人员在开发时经常使用 HTTP 网络。事实上,PHP 包含的一些核心函数在处理 HTTP 通信时非常有用。

让我们使用一个名为 curl 的工具来看看高层次的 HTTP 请求。curl 本质上是一个允许我们模拟网络请求的命令行工具。它允许你使用各种协议模拟数据传输。

cURL 最初的名称是 see URL。

curl 项目包含 libcurlcurl 命令行工具。Libcurl 是 PHP 支持的一个库,只要安装了它,就可以在 PHP 中通过一系列协议进行连接和通信。

不过在本例中,我们将使用命令行工具来模拟请求。

让我们先向给定的网站发出一个简单的 curl 请求,如下所示:

curl https://junade.com

根据您在命令中查询的网站,您会发现终端输出是空白的:

image 2023 10 31 17 02 48 641

这是怎么回事?为了弄清这个问题,我们需要再深入调查一下。

你可以在 curl 命令中使用 -v 参数,这样我们就能看到发生了什么的详细输出:

curl -v http://junade.com

这个输出有很大不同:

image 2023 10 31 17 04 03 946

通过该输出,我们可以看到发送的报头和接收的报头。

以星号 * 开头的区块表示正在建立连接。我们可以看到 curl 是如何重建 URL 使其正确的(在末尾包含一个正斜线),然后解析服务器的 IP 地址(在我的例子中是一个 IPv6 地址),最后建立与网络服务器的连接:

* Rebuilt URL to: http://junade.com/
* Trying 2400:cb00:2048:1::6810:f005...
* Connected to junade.com (::1) port 80 (#0)

通过查询 DNS 服务器,将主机名转换为 IP 地址;稍后我们将对此进行详细介绍。但在这一点上,重要的是要记住,在此之后,与服务器的连接是通过 IP 地址建立的。

如果去掉末尾的斜线,我们就可以在第一行中看到,重建 URL 的过程将不复存在,因为在我们发出请求之前,URL 就已经是正确的格式了:

image 2023 10 31 17 06 10 228

接下来让我们看看星号后面的行。 我们在大于号 > 中看到出站标头。

这些标头如下所示:

> GET / HTTP/1.1
> Host: junade.com
> User-Agent: curl/7.43.0
> Accept: */*
>

因此,我们看到的第一条信息是请求方法 GET,然后是端点 / 和协议 HTTP/1.1

接下来,我们会看到 Host 头信息,它告诉我们服务器的域名,也可以包含服务器正在监听的 TCP 端口号,但如果端口是所请求服务的标准端口,则通常会进行修改。为什么需要这样做呢?假设一个服务器包含多个虚拟主机;这实际上是服务器使用标头来确定不同虚拟主机的原因。VirtualHosting 本质上允许服务器托管多个域名。为了做到这一点,我们需要这个标头;当服务器看到一个 HTTP 请求进来时,他们不会看到这个标头。

还记得我说过连接是通过 IP 地址建立的吗?这个 Host 头信息让我们可以通过主机名变量来发送 IP 地址。

接下来,我们会看到 User-Agent 头,表示客户端使用的是什么浏览器;这个请求中的 User-Agent 头表示我们使用 curl 命令发送 HTTP 请求。切记不要相信来自客户端的任何 HTTP 标头,因为它们可能被恶意对手篡改,以包含他们想放入的任何数据。从伪造的浏览器标识符到 SQL 注入,它们都可能包含其中。

最后,"接受"(Accept) 标头指明了响应可接受的 Content-Type 标头。在这里,我们看到的是通配符接受,表示我们乐意接收服务器发送给我们的任何内容。在其他情况下,我们可以使用 Accept: text/plain 表示我们希望看到纯文本,或使用 Accept:application/json 表示 JSON。我们甚至可以使用 Accept: image/png 来指定是否要接收 PNG 图像。

我们还可以通过 Accept 标头发送各种参数;例如,我们可以使用 Accept: text/html; charset=UTF-8 来请求使用 UTF-8 字符集的 HTML。

该标头允许使用的基本语法如下:

top-level type name / subtype name [ ; parameters ]

服务器可以使用响应中的 Content-Type 标头来指示返回给用户的内容类型。因此,服务器可以向终端用户发送如下标头:

Content-Type: text/html; charset=utf-8

转到响应的主题,让我们看一下响应。 它们以 < 为前缀:

< HTTP/1.1 301 Moved Permanently
< Date: Sun, 10 Jul 2016 18:23:22 GMT
< Transfer-Encoding: chunked
< Connection: keep-alive
< Set-Cookie: __cfduid=d45c9e013b12286fe4e443702f3ec15f31468175002;
expires=Mon, 10-Jul-17 18:23:22 GMT; path=/; domain=.junade.com; HttpOnly
< Location: https://junade.com/
< Server: cloudflare-nginx
< CF-RAY: 2c060be42065346a-LHR
<

因此,我们首先会收到格式和状态代码的响应。HTTP/1.1 表示我们收到的是 HTTP/1.1 响应,而 301 Moved Permanently 消息则表示永久重定向。相应地,我们还会收到一个 Location: https://junade.com/ 头信息,告诉我们下一步该去哪里。

服务器"(Server) 头信息表示提供请求的网络服务器的签名。它可以是 Apache 或 Nginx;在本例中,它是 CloudFlare 用于其网络的 Nginx 的修改版。

Set-Cookie 标头用于指示浏览器应设置哪些 cookies;其标准载于一份名为 RFC 6265 的文件中。

RFC 是 Request for Comments 的缩写,有多种类型。标准跟踪 RFC 是那些打算成为互联网标准(STD)的 RFC,而信息 RFC 则可以是任何内容。还有一些其他类型的 RFC,如实验性 RFC、当前最佳实践 RFC、历史性 RFC,甚至还有一种未知 RFC 类型,用于那些不清楚今天是否会发布的 RFC。

传输编码(Transfer-Encoding)"标头表示向用户传输实体时使用的编码,可以是分块编码,也可以是压缩实体的 gzip 编码。

有趣的是,2015 年 5 月在 RFC 7540 中发布的 HTTP/2 协议实际上允许对头部进行压缩。如今,我们发送的报头数据比最初创建 HTTP/1 协议时传输的数据还要多(最初的 HTTP 协议甚至不包含 Host 报头!)。

连接(Connection)头为连接提供了控制选项。它允许发送方指定当前连接所需的选项。最后,"日期"(Date) 标头表示发送信息的日期和时间。

考虑一下:HTTP 请求/响应可以包含多个相同名称的标头吗?可以,这在某些标头中尤其有用,比如链接标头。该标头用于执行 HTTP/2 服务器推送;服务器推送允许服务器在请求之前向客户端推送请求。每个标头可指定一个资产;因此,推送多个资产需要多个标头。

这一点我们可以在 PHP 中实现。下面是 PHP 中的头函数调用:

header("Link: <{$uri}>; rel=preload; as=image", false);

第一个参数是我们要发送的实际头信息的字符串,第二个参数(false)表示我们不想替换之前的相同头信息,而是也要发送这个头信息,但不是替换它。将该标志设置为 true,就表示我们要覆盖之前的头信息;如果没有指定该标志,这是默认选项。

最后,当请求关闭时,您将看到最后一个星号,表明连接已关闭:

* Connection #0 to host junade.com left intact

通常情况下,如果有正文的话,正文下方会出现这个链接。在这个请求中,由于只是一个重定向,所以并没有。

现在,我使用以下命令向 Location 头指向的地方发出 curl 请求:

curl -v https://junade.com/

您现在会注意到连接关闭消息是在 HTML 正文结束之后出现的:

image 2023 10 31 17 20 25 730

现在让我们试着探索几种 HTTP 方法。在 REST API 中,你会经常用到 GETPOSTPUTDELETE;但我们首先要探索另外两种方法,即 HEADOPTIONS

HTTP OPTIONS 请求详细说明了可以在给定端点上使用的请求方法。它提供了有关该特定端点可用通信选项的信息。

让我来演示一下。我将使用一个名为 HTTPBin 的服务,它允许我通过 curl 发出请求,从真正的服务器上获取一些响应。

下面是我使用 curl 发出的 OPTIONS 请求:

curl -v -X OPTIONS https://httpbin.org/get

-X 选项允许我们指定特定的 HTTP 请求类型,而不仅仅是默认为curl。

让我们看看执行后会是什么样子:

image 2023 10 31 17 22 29 486

首先,你会注意到,由于请求是通过 HTTP 发出的,你会在星号中看到一些额外的信息;这些信息包含用于加密连接的 TLS 证书信息。

请看下面一行:

TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

TLS 1.2 表示我们正在处理的传输层安全版本;第二部分指出 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,表示连接的密码套件。

密码套件首先详细说明我们使用的是 TLSECDHE_RSA 表示使用椭圆曲线 Diffie-Hellman 进行密钥交换。密钥交换的本质是确保加密密钥的安全传输。通过使用椭圆曲线加密技术,可以共享特定的密钥,然后再使用该密钥加密数据。ECDHE_RSA 表示我们使用椭圆曲线 Diffie-Hellman 共享基于服务器获得的 RSA 密钥的密钥。还有其他一些密钥交换算法,例如,ECDH_ECDSA 使用的是带有 ECDSA 签名证书的固定 ECDH。

访问控制前缀头用于一种称为 CORS 的机制,它主要允许 JavaScript 进行跨源 API 请求;这里我们不担心这个问题。

对于 OPTIONS 请求,我们需要关注的头信息是 Allow 头信息。它详细说明了允许我们向特定端点提交哪些请求方法。

因此,这就是我们在查询 /get 端点时收到的请求:

< Allow: HEAD, OPTIONS, GET

请注意,我在这里使用的是 /get 端点。相反,让我们使用下面的 curl 请求,向 /post 端点发出另一个 OPTIONS 请求:

curl -v -X OPTIONS https://httpbin.org/post

这是我们得到的回复:

image 2023 10 31 17 26 56 999

你会发现 Allow 头信息现在包含 POSTOPTIONS。还请注意,HEAD 选项已经消失。

你很快就会发现,HEAD 请求与 GET 请求非常相似,只是没有信息体。它只返回 HTTP 请求的标题,而不返回请求的正文。因此,它允许你获取实体的元信息,而不需要获取完整的响应。

让我们向 /get 端点发出 HEAD 请求:

curl -I -X HEAD https://httpbin.org/get

在这个请求中,我没有使用 -v(verbose)选项,而是使用了 -I 选项,它将只获取 HTTP 头信息。这非常适合使用 HEAD 选项进行 HTTP 请求:

image 2023 10 31 17 28 51 392

正如您所看到的,我们在 Content-Type 标头中获得了响应的类型。同时,你还会在 Content-Length 头信息中看到请求的长度。长度以八位字节(8 位)为单位;你可能会认为这与字节相同,但在所有架构上,字节并不一定都是 8 位。

还可以发送一些其他标头来表达元信息。这可能包括标准标头或非标准标头,以表达 RFC 支持的标准化标头无法表达的其他信息。

HTTP ETags(实体标记)是一种提供缓存验证的机制。您可以在 RESTful API 中使用它们来进行乐观并发控制;这基本上允许多个请求在无需相互干扰的情况下完成。这是一个相当高级的 API 概念,所以我不会在这里说得太详细。

请注意,在 HTTP HEADOPTIONS 请求中,我们都得到了 200 OK 头信息。200 状态代码表示 HTTP 请求成功。

状态代码有很多种。它们的分类如下:

  • 1xx messages: Informational

  • 2xx messages: Success

  • 3xx messages: Redirect

  • 4xx messages: Client Error

  • 5xx messages: Server Error

信息头可能是 101 响应,表示客户端正在切换协议,而服务器已同意这样做。如果您开发的是 RESTful 应用程序接口,您可能不会遇到信息头信息;这些信息很可能是由网络服务器发送的,而作为开发人员,这些信息是抽象出来的。

正确使用其他 HTTP 状态代码对于正确开发 API(尤其是 RESTful API)至关重要。

成功状态代码不仅限于 200 OK 消息;201 创建表示请求已满足,并创建了新资源。这在使用 PUT 请求创建新资源或使用 POST 创建附属资源时特别有用。202 Accepted(已接受)表示请求已被接受处理,但处理尚未完成,这在分布式系统中非常有用。204 No Content 表示服务器已处理请求,但未返回任何信息;205 Reset Content 标头的作用与此相同,但要求请求者重置其文档视图。这些只是一些 200 信息,显然还有更多。

重定向信息包括我们在第一个 curl 示例中展示的 301 Moved Permanently(永久移动),而 302 Found 则可用于临时重定向。同样,还有其他信息代码。

客户端错误代码包括臭名昭著的 404 Not Found(找不到资源)。除此之外,当需要但未提供身份验证时,我们还有 401 Unauthorized403 Forbidden 表示服务器拒绝响应请求(例如,权限不正确)。405 Method Not Allowed 允许我们拒绝使用无效请求方法提交的请求,这对于 RESTful API 也非常有用。405 Not Acceptable 是指服务器无法根据发送给它的 Accept 头生成响应。同样,还有许多其他 4xx HTTP 代码。

HTTP 代码 451 表示由于法律原因请求不可用。该代码是根据《华氏 451》(Fahrenheit 451)命名的,这部小说的作者声称华氏 451 是纸张的自燃温度。

最后,"服务器错误(Server Errors)" 允许服务器表明他们未能满足显然有效的请求。这些信息包括 500 内部服务器错误(500 Internal Server Error),这是遇到意外情况时给出的通用错误信息。

现在我们来看看如何发送 GET 请求。如果我们没有指定要发送的数据或特定方法,curl 默认会发出 GET 请求:

curl -v https://httpbin.org/get

我们还可以指定要进行 GET 请求:

curl -v -X GET https://httpbin.org/get

输出结果如下:

image 2023 10 31 17 38 59 809

在这里,你可以看到我们得到的头信息与 HEAD 请求中的头信息相同,但增加了一个主体;即我们试图访问的资源的一些 JSON 数据。

在这里,我们会得到一条 200 Success 消息,但让我们向一个不存在的端点发出 HTTP 请求,这样就能触发一条 404 消息:

image 2023 10 31 17 40 02 518

正如您所看到的,我们收到一个标头,指出 404 NOT FOUND 而不是我们通常的 200 OK 消息。

HTTP 404 响应也可以没有正文:

image 2023 10 31 17 40 52 490

GET 请求只是显示现有资源,而 POST 请求允许我们修改和更新资源。而 PUT 请求允许我们创建一个新资源或覆盖一个资源,但特别是在给定的端点。

有什么区别呢?PUT 是幂等的,而 POST 不是幂等的。PUT 就像设置一个变量,即 $x = 3,你可以反复设置,但输出结果是一样的,即 $x3

相反,POST 很像运行 $x++;它引起的变化不是可幂等的,就像 $x++ 不能反复使用以产生相同的变量一样。POST 可以更新资源、添加附属资源或引起变化。而 PUT 则在知道要创建的 URL 时使用。

当你知道为你创建资源的工厂的 URL 时,可以使用 POST 来创建。

因此,举例来说,如果终端/用户想要生成一个具有唯一 ID 的用户账户,我们可以使用下面的方法:

POST /user

但如果我们想在某个端点创建一个用户账户,就需要使用 PUT

PUT /user/tom

同样,如果我们想在给定端点覆盖 tom,我们可以在那里放置另一个 PUT 请求:

PUT /user/tom

但假设我们不知道汤姆的端点;相反,我们只想 PUT 到一个带有用户 ID 参数的端点,然后更新一些信息:

POST /user

希望这是有道理的!

现在让我们看一下给定的 HTTP POST 请求。

我们可以使用 URL 编码数据创建请求:

curl --data "user=tom&manager=bob" https://httpbin.org/post

请注意,如果我们在 curl 中指定数据但未指定请求类型,它将默认为 POST

如果我们执行此命令,您可以看到 Content-Typex-www-form-urlencoded

image 2023 10 31 17 47 02 193

但是,如果 API 允许并接受该格式,我们也可以将 JSON 数据提交到端点:

curl -H "Content-Type: application/json" -X POST -d
'{"user":"tom","manager":"bob"}' https://httpbin.org/post

这提供了以下输出,请注意 Content-Type 现在是 JSON,而不是之前的 x-www-form-urlencoded 形式:

image 2023 10 31 17 48 10 693

现在,我们可以通过将相同的数据发送到 /put 端点来使用 PUT 发出 HTTP 请求:

curl -H "Content-Type: application/json" -X PUT -d
'{"user":"tom","manager":"bob"}' https://httpbin.org/put

让我们将请求类型更改为 PUT

image 2023 10 31 17 49 55 479

让我们用下面的 curl 请求向 DELETE 端点发出同样的请求(在本例中,我们将提交数据):

curl -H "Content-Type: application/json" -X DELETE -d '{"user":"tom"}' https://httpbin.org/delete

这有以下输出:

image 2023 10 31 17 50 59 236

在现实世界中,您可能并不一定需要提交与我们刚刚删除的资源相关的任何信息(这正是 DELETE 的用途)。相反,我们可能只想提交一条 204 No Content 消息。通常情况下,我不会回传信息。

HTTP/2 在高层保持了这种请求结构。请记住,大多数 HTTP/2 实现都需要 TLS (h2),而且大多数浏览器都不支持 HTTP/2 over clearartext (h2c),尽管在 RFC 标准中事实上是可行的。如果使用 HTTP/2,实际上需要对请求进行 TLS 加密。

说了这么多,但这就是你需要知道的关于 HTTP 请求的一切,而且是高层次的。我们并没有深入研究网络细节,但这种理解对于应用程序接口架构来说是必要的。

既然我们已经很好地理解了 HTTP 请求和 HTTP 通信中使用的方法,那么我们就可以继续理解是什么让 API 成为 RESTful。