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