HTTP 条件请求

HTTP 有一个条件请求的概念,其中请求的结果,甚至成功与否,都可以通过将受影响的资源与验证器进行比较来控制。这些请求对于验证缓存内容非常有用,确保只有当它与浏览器已有的副本不同时才会被获取。条件请求还可用于在恢复下载时确保文档的完整性,或在服务器上上传或修改文档时防止丢失更新。

原理

HTTP 条件请求是根据特定标头的值以不同方式执行的请求。这些标头定义了一个前置条件,如果前置条件匹配或不匹配,请求的结果将有所不同。

不同的行为由所使用的请求方法和用于前置条件的标头集定义

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

验证器

所有条件标头都尝试检查服务器上存储的资源是否与特定版本匹配。为此,条件请求需要指示资源的版本。由于逐字节比较整个资源是不切实际的,并且并非总是我们想要的,请求会传输一个描述版本的值。这些值称为验证器,分为两种

  • 文档的上次修改日期,即last-modified日期。
  • 一个不透明的字符串,唯一标识每个版本,称为实体标签,或ETag

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

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

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

强验证

强验证在于保证资源与所比较的资源逐字节相同。这对于某些条件标头是强制性的,对于其他标头是默认的。强验证非常严格,在服务器级别可能难以保证,但它确实保证在任何时候都不会丢失数据,有时会牺牲性能。

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

注意:由于内容编码的更改需要更改 ETag,因此一些服务器在压缩来自源服务器的响应时(例如,反向代理)会修改 ETag。Apache 服务器默认将压缩方法的名称(-gzip)附加到 ETag,但这可以通过DeflateAlterETag指令进行配置

弱验证

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

条件标头

几个 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.

与资源一起,验证器在标头中发送。在此示例中,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 开发人员无需进行任何特殊工作。

部分下载的完整性

文件的部分下载是 HTTP 的一项功能,它允许通过保留已获得的信息来恢复以前的操作,从而节省带宽和时间

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

支持部分下载的服务器通过发送Accept-Ranges标头来广播此功能。一旦发生这种情况,客户端可以通过发送带有缺失范围的Range标头来恢复下载

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 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错误。然后由客户端处理错误:要么通知用户重新开始(这次是在最新版本上),要么向用户显示两个版本的差异,帮助他们决定要保留哪些更改。

处理资源的首次上传

资源的首次上传是前一个的特例。像资源的任何更新一样,如果两个客户端试图在相似的时间执行,它也会受到竞态条件的影响。为了防止这种情况,可以使用条件请求:通过添加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 的一个关键特性,允许构建高效且复杂的应用程序。对于缓存或恢复下载,网站管理员唯一需要做的工作是正确配置服务器;在某些环境中设置正确的 ETag 可能很棘手。一旦完成,浏览器将提供预期的条件请求。

对于锁定机制,情况正好相反:Web 开发人员需要使用适当的标头发出请求,而网站管理员主要可以依靠应用程序来为他们执行检查。

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

另见