HTTP 条件请求

HTTP 有一个条件请求的概念,其中请求的结果,甚至成功的请求,可以通过将受影响的资源与验证器的值进行比较来改变。这种请求对于验证缓存的内容很有用,并避免无用的控制,以验证文档的完整性,例如在恢复下载时,或在将文档上传或修改到服务器时防止更新丢失。

原则

HTTP 条件请求是根据特定标头的值以不同方式执行的请求。这些标头定义了先决条件,如果先决条件匹配或不匹配,则请求的结果将不同。

不同的行为由使用的请求方法和用于先决条件的标头集定义。

  • 对于像安全方法,如GET,它通常尝试获取文档,条件请求可用于发送回文档,如果仅相关。因此,这节省了带宽。
  • 对于像不安全方法,如PUT,它通常上传文档,条件请求可用于上传文档,仅当它所基于的原始文档与存储在服务器上的文档相同。

验证器

所有条件标头都尝试检查存储在服务器上的资源是否与特定版本匹配。为此,条件请求需要指示资源的版本。由于逐字节比较整个资源是不可行的,而且并不总是想要的,因此请求传输一个描述版本的 value。此类 value 称为验证器,分为两种。

  • 文档的最后修改日期,即最后修改日期。
  • 一个不透明字符串,唯一地标识每个版本,称为实体标签etag

比较同一资源的版本有点棘手:根据上下文,相等性检查有两种类型。

  • 强验证用于需要逐字节一致性的情况,例如在恢复下载时。
  • 弱验证用于用户代理只需要确定两个资源是否具有相同内容的情况。即使存在细微的差异(例如不同的广告或具有不同日期的页脚),也可能将资源视为相同。

验证的类型与使用的验证器无关。 both Last-ModifiedETag 允许两种类型的验证,尽管在服务器端实现它的复杂度可能会有所不同。HTTP 默认使用强验证,并指定何时可以使用弱验证。

强验证

强验证包括确保资源与它所比较的资源在字节上完全相同。对于某些条件标头而言,这是必需的,对于其他条件标头而言,这是默认的。强验证非常严格,可能难以在服务器级别保证,但它确实保证任何时候都不会丢失数据,有时会以性能为代价。

使用 Last-Modified 来获得强验证的唯一标识符非常困难。通常,这是使用带有资源的 MD5 哈希值(或其派生值)的 ETag 来完成的。

弱验证

弱验证与强验证不同,因为它将两个版本的文档视为相同,如果内容等效。例如,一个页面仅在页脚或不同的广告中日期不同,与另一个页面相比,在弱验证下将被认为是相同的。这两个相同的版本在使用强验证时被认为是不同的。构建一个创建弱验证的 etags 系统可能很复杂,因为它涉及了解页面不同元素的重要性,但在优化缓存性能方面非常有用。

条件头

几个 HTTP 标头,称为条件标头,会导致条件请求。这些是

If-Match

如果远程资源的 ETag 等于此标头中列出的一个,则成功。它执行强验证。

If-None-Match

如果远程资源的 ETag 与此标头中列出的每个值不同,则成功。它执行弱验证。

If-Modified-Since

如果远程资源的 Last-Modified 日期比此标头中给出的日期更近,则成功。

If-Unmodified-Since

如果远程资源的 Last-Modified 日期早于或与此标头中给出的日期相同,则成功。

If-Range

类似于 If-MatchIf-Unmodified-Since,但只能有一个 etag 或一个日期。如果失败,范围请求将失败,并且不会出现 206 Partial Content 响应,而是发送一个带有完整资源的 200 OK

用例

缓存更新

条件请求最常见的用例是更新缓存。对于空缓存或没有缓存,请求的资源将与状态 200 OK 一起发送回。

The request issued when the cache is empty triggers the resource to be downloaded, with both validator values sent as headers. The cache is then filled.

与资源一起,验证器在标头中发送。在此示例中,both Last-ModifiedETag 被发送,但它也可能仅发送其中一个。这些验证器与资源一起缓存(就像所有标头一样),并将用于创建条件请求,一旦缓存变得陈旧。

只要缓存没有过时,就不会发出任何请求。但一旦它变得陈旧,这主要由 Cache-Control 标头控制,客户端不会直接使用缓存的值,而是发出条件请求。验证器的值用作 If-Modified-SinceIf-None-Match 标头的参数。

如果资源没有改变,服务器将发送回一个 304 Not Modified 响应。这将使缓存再次更新,并且客户端将使用缓存的资源。尽管存在消耗一些资源的响应/请求往返,但这比再次通过网络传输整个资源更有效。

With a stale cache, the conditional request is sent. The server can determine if the resource changed, and, as in this case, decide not to send it again as it is the same.

如果资源发生了变化,服务器只需发送回一个 200 OK 响应,其中包含新版本的资源(就好像请求不是条件性的)。客户端将使用此新资源(并将其缓存)。

In the case where the resource was changed, it is sent back as if the request wasn't conditional.

除了在服务器端设置验证器之外,这种机制是透明的:所有浏览器都管理缓存并发送此类条件请求,而无需 Web 开发人员做任何特殊工作。

部分下载的完整性

文件的 partial downloading 是 HTTP 的一项功能,它允许恢复之前的操作,通过保留已获得的信息来节省带宽和时间。

A download has been stopped and only partial content has been retrieved.

支持 partial downloads 的服务器通过发送 Accept-Ranges 标头来广播这一点。一旦发生这种情况,客户端可以通过发送带有丢失范围的 Ranges 标头来恢复下载。

The client resumes the requests by indicating the range he needs and preconditions checking the validators of the partially obtained request.

原理很简单,但存在一个潜在的问题:如果下载的资源在两次下载之间被修改,则获得的范围将对应于资源的两个不同版本,最终文档将被损坏。

为了防止这种情况,使用了条件请求。对于范围,有两种方法可以做到这一点。更灵活的方法使用 If-Unmodified-SinceIf-Match,如果先决条件失败,服务器将返回错误;然后,客户端从头开始重新启动下载。

When the partially downloaded resource has been modified, the preconditions will fail and the resource will have to be downloaded again completely.

即使这种方法有效,当文档发生变化时,它还会增加一个额外的响应/请求交换。这会影响性能,HTTP 有一个特定的标头来避免这种情况:If-Range

The If-Range headers allows the server to directly send back the complete resource if it has been modified, no need to send a 412 error and wait for the client to re-initiate the download.

这种解决方案更有效,但稍微不那么灵活,因为条件中只能使用一个 etag。很少需要这种额外的灵活性。

使用乐观锁机制避免丢失更新问题

Web 应用程序中常见的操作是更新远程文档。这在任何文件系统或源代码控制应用程序中都很常见,但任何允许存储远程资源的应用程序都需要这种机制。常见的网站,如维基和其他 CMS,也需要这种机制。

使用 PUT 方法,你可以实现这一点。客户端首先读取原始文件,修改它们,最后将它们推送到服务器。

Updating a file with a PUT is very simple when concurrency is not involved.

不幸的是,一旦我们考虑到并发,事情就变得有点不准确。当一个客户端正在本地修改其新资源副本时,第二个客户端可以获取相同的资源并在其副本上执行相同的操作。接下来发生的事情非常不幸:当他们提交回服务器时,第一个客户端的修改会被下一个客户端推送丢弃,因为第二个客户端不知道第一个客户端对资源的更改。谁获胜的决定没有传达给另一方。要保留哪个客户端的更改将随他们提交的速度而变化;这取决于客户端、服务器的性能,甚至取决于在客户端编辑文档的人员。获胜者将在每次都改变。这是一个竞争条件,会导致难以检测和调试的问题行为。

When several clients update the same resource in parallel, we are facing a race condition: the slowest win, and the others don't even know they lost. Problematic!

没有办法解决这个问题,而不会惹恼两个客户端中的一个。但是,要避免丢失更新和竞争条件。我们希望结果可预测,并期望在客户端更改被拒绝时通知它们。

条件请求允许实现乐观锁算法(大多数维基或源代码控制系统使用)。这个概念是允许所有客户端获取资源的副本,然后让他们在本地修改它,通过成功允许第一个客户端提交更新来控制并发。所有基于现在已过时版本资源的后续更新都将被拒绝。

Conditional requests allow to implement optimistic locking: now the quickest wins, and the others get an error.

这是使用 If-MatchIf-Unmodified-Since 标头实现的。如果 etag 与原始文件不匹配,或者文件自获取以来已被修改,则更改将被 412 Precondition Failed 错误拒绝。然后由客户端来处理错误:要么通知用户重新开始(这次使用最新的版本),要么向用户显示两个版本的diff,帮助他们决定要保留哪些更改。

处理资源的第一次上传

资源的第一次上传是前面提到的边缘情况。与任何更新资源一样,如果两个客户端尝试在类似的时间执行,它也会受到竞争条件的影响。为了防止这种情况,可以使用条件请求:通过添加 If-None-Match'*' 的特殊值,表示任何 etag。只有在资源之前不存在的情况下,请求才会成功。

Like for a regular upload, the first upload of a resource is subject to a race condition: If-None-Match can prevent it.

If-None-Match 仅适用于符合 HTTP/1.1(及更高版本)的服务器。 如果不确定服务器是否符合标准,您需要首先发出 HEAD 请求到资源以进行检查。

结论

条件请求是 HTTP 的一项关键功能,它允许构建高效且复杂的应用程序。 对于缓存或恢复下载,网站管理员只需要正确配置服务器;在某些环境中设置正确的 etags 可能很棘手。 一旦实现,浏览器将提供预期的条件请求。

对于锁定机制,情况恰恰相反:Web 开发人员需要发出包含正确标头的请求,而网站管理员则可以主要依赖应用程序来执行检查。

在这两种情况下,很明显,条件请求是 Web 背后的一个基本功能。