Conditional requests
HTTP具有条件请求 的概念,通过比较受影响的资源和验证器的值,可以更改结果,甚至请求的成功。这些请求可用于验证缓存的内容,避免无用的控制,验证文档的完整性(例如恢复下载时),或防止在上载或修改服务器上的文档时丢失更新。
原则
HTTP条件请求是执行不同的请求,具体取决于特定标头的值。这些头文件定义了一个先决条件,如果前提条件匹配,请求的结果将会不同。
不同的行为由所使用的请求的方法以及用于前提条件的一组标题定义:
- 对于通常试图获取文档的安全方法,
GET
条件请求可用于发回文档(如果相关的话)。因此,这节省了带宽。
- 对于不安全的方法,例如
PUT
通常上传文档的情况,条件请求可以用于上载文档,只有当它基于的原始文档与存储在服务器上的文档相同时。
验证器
所有条件标题都会尝试检查服务器上存储的资源是否与特定版本匹配。为了实现这一点,条件请求需要指示资源的版本。由于将整个资源字节与字节进行比较是不切实际的,并不总是需要的,所以请求传输描述版本的值。这些值称为验证器,有两种:
- 文档上次修改日期,即上次修改日期。
- 一个不透明的字符串,唯一标识每个版本,称为实体标签或etag。
比较相同资源的版本有点棘手:根据上下文,有两种相等性检查:
- 当期望字节到字节的身份时,例如在恢复下载时使用强验证。
- 当用户代理只需要确定两个资源是否具有相同的内容时使用弱验证。即使他们是微小的差异,就像不同的广告,或不同的日期页脚。
验证的类型与所使用的验证器无关。双方Last-Modified
并ETag
允许这两种类型的验证,虽然复杂性来实现它在服务器端可能会有所不同。HTTP默认使用强大的验证,并指定何时可以使用弱验证。
强大的验证
强有力的验证包括保证资源是字节到字节,与它所比较的资源相同。这对于一些条件标头是必需的,对其他标签是默认的。强大的验证非常严格,可能难以在服务器级别保证,但它确保在任何时候都不会丢失数据,有时会牺牲性能。
使用一个独特的标识符来进行强有力的验证是非常困难的Last-Modified
。通常这是通过使用ETag
资源的MD5散列(或派生)来完成的。
弱验证
弱验证与强验证不同,因为它认为如果内容是相同的,则文档的两个版本是相同的。例如,只有页脚中的日期不同或其他广告与另一页不同的页面会被视为与验证较弱的页面完全相同。这些相同的两个版本在使用强大验证时被认为是不同的 构建一个etags系统会产生弱验证,这可能很复杂,因为它涉及了解页面不同元素的重要性,但对优化高速缓存性能非常有用。
条件标头
一些称为条件标题的HTTP标题会导致有条件的请求。这些是:
如果ETag
遥远的资源等于此标题中列出的资源,If-Match
则成功。默认情况下,除非etag带有前缀'W/'
,否则会执行强有力的验证。If-None-Match
如果ETag
远程资源的不同于此标题中列出的每个资源,则成功。默认情况下,除非etag带有前缀'W/'
,否则会执行强有力的验证。如果Last-Modified
距离资源的日期比此标题中给出的日期更近,If-Modified-Since
则成功。如果Last-Modified
遥远资源的日期较旧或与此标题中给出的日期相同,If-Unmodified-Since
则成功。If-Range
与If-Match
或类似If-Unmodified-Since
,但可以只有一个单一的etag或一个日期。如果失败,范围请求失败,而不是206Partial Content
响应200OK
与完整的资源一起发送。
用例
缓存更新
条件请求最常见的用例是更新缓存。使用空缓存或无缓存时,请求的资源将以状态返回200
OK
。
与资源一起,验证器将在标题中发送。在这个例子中,无论是Last-Modified
和ETag
发送,但它同样一直只是其中之一。这些验证器会与资源一起缓存(如所有头文件),并在缓存过期时用于创建条件请求。
只要缓存不陈旧,根本就不会发出请求。但是一旦它变得陈旧,这主要由Cache-Control
头部控制,客户端不直接使用缓存值,而是发出条件请求。验证程序的值用作If-Modified-Since
和If-Match
标题的参数。
如果资源没有改变,服务器发回一个304
Not Modified
响应。这使得缓存再次变得新鲜,并且客户端使用缓存的资源。尽管响应/请求往返消耗了一些资源,但这比通过线路传输整个资源更有效。
如果资源发生了变化,那么服务器只是发送一个200
OK
响应,并带有新版本的资源,就像请求不是有条件的并且客户端使用这个新资源(并对其进行缓存)一样。
除了在服务器端设置验证器之外,这种机制是透明的:所有浏览器管理一个缓存并发送这样的条件请求,而不需要Web开发人员完成任何特殊工作。
完整的部分下载
文件的部分下载是HTTP的功能,允许恢复先前的操作,通过保留已获得的信息来节省带宽和时间:
支持部分下载的服务器通过发送Accept-Ranges
标题来广播此内容。一旦发生这种情况,客户端可以通过发送Ranges
缺少范围的标题来恢复下载:
原理很简单,但是存在一个潜在的问题:如果下载的资源在两次下载之间被修改,则获得的范围将对应于资源的两个不同版本,并且最终文档将被损坏。
为了防止这种情况,使用条件请求。对于范围,有两种方法可以做到这一点。如果前提条件失败,则使用更灵活的If-Modified-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的一个关键功能,并允许构建高效和复杂的应用程序。为了缓存或恢复下载,网站站长只需要正确配置服务器; 在一些环境中设置正确的etags可能会很棘手。一旦实现,浏览器将满足预期的条件请求。
对于锁定机制,情况恰恰相反:Web开发人员需要使用正确的标题发出请求,而网站管理员可以主要依靠应用程序来执行对它们的检查。
在这两种情况下,很显然,有条件的请求是Web背后的基本特征。