HTTP 条件请求
HTTP 有一个条件请求的概念,其中请求的结果,甚至成功的请求,可以通过将受影响的资源与验证器的值进行比较来改变。这种请求对于验证缓存的内容很有用,并避免无用的控制,以验证文档的完整性,例如在恢复下载时,或在将文档上传或修改到服务器时防止更新丢失。
原则
验证器
所有条件标头都尝试检查存储在服务器上的资源是否与特定版本匹配。为此,条件请求需要指示资源的版本。由于逐字节比较整个资源是不可行的,而且并不总是想要的,因此请求传输一个描述版本的 value。此类 value 称为验证器,分为两种。
- 文档的最后修改日期,即最后修改日期。
- 一个不透明字符串,唯一地标识每个版本,称为实体标签或etag。
比较同一资源的版本有点棘手:根据上下文,相等性检查有两种类型。
- 强验证用于需要逐字节一致性的情况,例如在恢复下载时。
- 弱验证用于用户代理只需要确定两个资源是否具有相同内容的情况。即使存在细微的差异(例如不同的广告或具有不同日期的页脚),也可能将资源视为相同。
验证的类型与使用的验证器无关。 both Last-Modified
和 ETag
允许两种类型的验证,尽管在服务器端实现它的复杂度可能会有所不同。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-Match
或If-Unmodified-Since
,但只能有一个 etag 或一个日期。如果失败,范围请求将失败,并且不会出现206
Partial Content
响应,而是发送一个带有完整资源的200
OK
。
用例
缓存更新
条件请求最常见的用例是更新缓存。对于空缓存或没有缓存,请求的资源将与状态 200
OK
一起发送回。
与资源一起,验证器在标头中发送。在此示例中,both Last-Modified
和 ETag
被发送,但它也可能仅发送其中一个。这些验证器与资源一起缓存(就像所有标头一样),并将用于创建条件请求,一旦缓存变得陈旧。
只要缓存没有过时,就不会发出任何请求。但一旦它变得陈旧,这主要由 Cache-Control
标头控制,客户端不会直接使用缓存的值,而是发出条件请求。验证器的值用作 If-Modified-Since
和 If-None-Match
标头的参数。
如果资源没有改变,服务器将发送回一个 304
Not Modified
响应。这将使缓存再次更新,并且客户端将使用缓存的资源。尽管存在消耗一些资源的响应/请求往返,但这比再次通过网络传输整个资源更有效。
如果资源发生了变化,服务器只需发送回一个 200 OK
响应,其中包含新版本的资源(就好像请求不是条件性的)。客户端将使用此新资源(并将其缓存)。
除了在服务器端设置验证器之外,这种机制是透明的:所有浏览器都管理缓存并发送此类条件请求,而无需 Web 开发人员做任何特殊工作。
部分下载的完整性
文件的 partial downloading 是 HTTP 的一项功能,它允许恢复之前的操作,通过保留已获得的信息来节省带宽和时间。
支持 partial downloads 的服务器通过发送 Accept-Ranges
标头来广播这一点。一旦发生这种情况,客户端可以通过发送带有丢失范围的 Ranges
标头来恢复下载。
原理很简单,但存在一个潜在的问题:如果下载的资源在两次下载之间被修改,则获得的范围将对应于资源的两个不同版本,最终文档将被损坏。
为了防止这种情况,使用了条件请求。对于范围,有两种方法可以做到这一点。更灵活的方法使用 If-Unmodified-Since
和 If-Match
,如果先决条件失败,服务器将返回错误;然后,客户端从头开始重新启动下载。
即使这种方法有效,当文档发生变化时,它还会增加一个额外的响应/请求交换。这会影响性能,HTTP 有一个特定的标头来避免这种情况:If-Range
这种解决方案更有效,但稍微不那么灵活,因为条件中只能使用一个 etag。很少需要这种额外的灵活性。
使用乐观锁机制避免丢失更新问题
Web 应用程序中常见的操作是更新远程文档。这在任何文件系统或源代码控制应用程序中都很常见,但任何允许存储远程资源的应用程序都需要这种机制。常见的网站,如维基和其他 CMS,也需要这种机制。
使用 PUT
方法,你可以实现这一点。客户端首先读取原始文件,修改它们,最后将它们推送到服务器。
不幸的是,一旦我们考虑到并发,事情就变得有点不准确。当一个客户端正在本地修改其新资源副本时,第二个客户端可以获取相同的资源并在其副本上执行相同的操作。接下来发生的事情非常不幸:当他们提交回服务器时,第一个客户端的修改会被下一个客户端推送丢弃,因为第二个客户端不知道第一个客户端对资源的更改。谁获胜的决定没有传达给另一方。要保留哪个客户端的更改将随他们提交的速度而变化;这取决于客户端、服务器的性能,甚至取决于在客户端编辑文档的人员。获胜者将在每次都改变。这是一个竞争条件,会导致难以检测和调试的问题行为。
没有办法解决这个问题,而不会惹恼两个客户端中的一个。但是,要避免丢失更新和竞争条件。我们希望结果可预测,并期望在客户端更改被拒绝时通知它们。
条件请求允许实现乐观锁算法(大多数维基或源代码控制系统使用)。这个概念是允许所有客户端获取资源的副本,然后让他们在本地修改它,通过成功允许第一个客户端提交更新来控制并发。所有基于现在已过时版本资源的后续更新都将被拒绝。
这是使用 If-Match
或 If-Unmodified-Since
标头实现的。如果 etag 与原始文件不匹配,或者文件自获取以来已被修改,则更改将被 412 Precondition Failed
错误拒绝。然后由客户端来处理错误:要么通知用户重新开始(这次使用最新的版本),要么向用户显示两个版本的diff,帮助他们决定要保留哪些更改。
处理资源的第一次上传
资源的第一次上传是前面提到的边缘情况。与任何更新资源一样,如果两个客户端尝试在类似的时间执行,它也会受到竞争条件的影响。为了防止这种情况,可以使用条件请求:通过添加 If-None-Match
和 '*'
的特殊值,表示任何 etag。只有在资源之前不存在的情况下,请求才会成功。
If-None-Match
仅适用于符合 HTTP/1.1(及更高版本)的服务器。 如果不确定服务器是否符合标准,您需要首先发出 HEAD
请求到资源以进行检查。
结论
条件请求是 HTTP 的一项关键功能,它允许构建高效且复杂的应用程序。 对于缓存或恢复下载,网站管理员只需要正确配置服务器;在某些环境中设置正确的 etags 可能很棘手。 一旦实现,浏览器将提供预期的条件请求。
对于锁定机制,情况恰恰相反:Web 开发人员需要发出包含正确标头的请求,而网站管理员则可以主要依赖应用程序来执行检查。
在这两种情况下,很明显,条件请求是 Web 背后的一个基本功能。