什么是 HTTP 缓存#
我们为什么需要缓存(cache)?
我们知道,我们每次访问一个网页都需要加载资源,如果这些资源都从远端的服务器上加载,整个请求和响应时间会很长,有没有快一点的办法呢?有,可以从附近的 CDN 请求资源,减少了长距离的网络传输,响应速度自然就提升了。可是,CDN 还是离客户端有点远,有没有办法在浏览器缓存数据,尽量少的向外请求资源呢?其实这些思想都是缓存的思想,在每个节点都做缓存就可以极大的减少远端资源请求,加快响应速度。
缓存中有几个概念
- Stale Content:陈旧的内容,即被缓存且过期的内容
- Fresh Content:新鲜的内容,即被缓存且未过期的内容
- Cache Validation:是联系服务器以检查缓存内容的有效性,并在其即将过期时得到更新的过程。
- Cache Invalidation:是删除缓存中可用的任何陈旧内容的过程。
缓存按照位置可以分为浏览器缓存,代理缓存,反向代理缓存
浏览器缓存#
浏览器缓存仅限于一个用户,并且与其他缓存不相同,并且它可以存储私有的响应。
代理缓存#
与服务于单个用户的浏览器缓存不同,代理缓存可以服务于访问相同内容的数百个不同用户。往往是由 ISP 实施的。
反向代理缓存#
反向代理缓存一般部署在服务端,用于减轻服务器的负载。
缓存头#
每当服务器发出一些响应时,它都会伴随一些 HTTP 标头,以指导缓存与否以及如何缓存此响应
Expires#
在 HTTP/1.1 和引入缓存控制之前,有一个 Expires 标头,它只是一个时间戳,告诉缓存应该将某些内容视为新鲜的时间。比如
Expires: Mon, 13 Mar 2017 12:22:00 GMT
当然,缓存上的时钟必须与服务器上的时钟同步,否则可能无法实现所需的结果。
尽管 Expires 还在使用,但我们现在应该优先考虑Cache-Control
Pragma#
这也是来自 HTTP/1.1 的旧特性,我们可能会看到在这里和那里使用Pragma: no-cache
,希望阻止响应被缓存。
Cache-Control#
缓存控制(Cache-Control)规定了内容应被缓存多长时间和以何种方式缓存。
cache-control 可以包含多个值,
private
将缓存设置为私有意味着该内容不会在任何代理中被缓存,它只会被客户端(即浏览器)缓存。
public
如果设置为公开,除了被客户端缓存外,还可以被代理机构缓存;为许多其他用户服务。
no-store
指定该内容不被任何一个缓存所缓存。
no-cache
no-cache 表示可以保持缓存,但缓存的内容在被提供之前要从服务器上重新验证(例如,使用 ETag)。也就是说,仍然有一个对服务器的请求,但是是为了验证,而不是为了下载缓存的内容。
max-age: seconds
max-age 规定了内容被缓存的秒数。例如,如果 cache-control 看起来像下面这样。
cache-control: max-age=900, private
s-maxage: seconds
s-maxage 这里 s - 前缀代表共享。这条指令专门针对共享缓存。像 max-age 一样,它也可以得到一些东西被缓存的秒数。如果存在,它将覆盖共享缓存的 max-age 和 expires 头文件。
must-revalidate
must-revalidate 有时可能发生的情况是,如果你有网络问题,而内容无法从服务器检索,浏览器可能会在没有验证的情况下提供陈旧的内容。 must-revalidate 可以避免这种情况。如果这个指令存在,意味着在任何情况下都不能提供陈旧的内容,在提供数据之前必须从服务器上重新验证。
proxy-revalidate
proxy-revalidate 与 must-revalidate 类似,但它对共享或代理缓存的规定是一样的。换句话说,proxy-revalidate 对于 must-revalidate 来说就像 s-maxage 对于 max-age 一样。
Mixing Values
我们可以以不同的方式组合这些指令以实现不同的缓存行为,但是no-cache/no-store
和public/private
是互斥的。如果同时指定no-store
和no-cache
,则no-store
将优先于no-cache
。
Validators#
到目前为止,我们只讨论了内容是如何被缓存的,以及多长时间的缓存内容被认为是新鲜的,但我们没有讨论客户端如何从服务器上进行验证。下面我们讨论用于此目的的头信息。
Etag
Etag 或 "Entity Tag" 是在 HTTP/1.1 规范中引入的。Etag 只是一个独特的标识符,服务器将其与一些资源联系在一起。这个 ETag 后来被客户端用来做有条件的 HTTP 请求,说明 "如果 ETag 与我的 ETag 不一样,就给我这个资源",只有在 ETag 不匹配的情况下才会下载内容。
生成 ETag 的方法在 HTTP 文档中没有指定,通常使用一些抗碰撞的哈希函数来给资源的每个版本分配 ETag。可以有两种类型的 etag,即强和弱的 etag。
ETag: "j82j8232ha7sdh0q2882" - Strong Etag
ETag: W/"j82j8232ha7sdh0q2882" - Weak Etag (prefixed with `W/`)
强验证 ETag 意味着两个资源是完全相同的,它们之间根本没有区别。而一个弱的 ETag 意味着两个资源虽然严格意义上不一样,但可以被认为是一样的。例如,弱的 ETag 可能对动态内容很有用。
现在我们知道什么是 Etag 了,但浏览器是如何提出这个请求的呢? 通过向服务器提出请求,同时在If-None-Match
头中发送可用的 Etag。
考虑一下这样的情况:你打开了一个网页,其中加载了一个标志图片,缓存期为 60 秒,ETag 为abc123xyz
。大约 30 分钟后,你重新加载该网页,浏览器会注意到 60 秒内新鲜的标志现在已经过时了;它将触发一个请求给服务器,在 if-none-match 头中发送过时的标志图像的 ETag
If-None-Match: "abc123xyz"
然后,服务器将比较这个 ETag 和资源的当前版本的 ETag。如果两个 ETag 都匹配,服务器将发回 304 未修改的响应,这将告诉客户,它所拥有的副本仍然是好的,它将被视为新的 60 秒。如果两个标志都不匹配,即标志很可能已经改变,客户端将被发送新的标志,它将用来替换它所拥有的陈旧的标志。
Last-Modified
服务器可能包括Last-Modified
标头,表明一些内容最后被修改的日期和时间。
Last-Modified: Wed, 15 Mar 2017 12:30:26 GMT
当内容过期时,客户端将向服务器发出一个条件性请求,其中包括它在名为If-Modified-Since
的头中的最后修改日期,以获得更新的 Last-Modified 日期;如果它与客户端的日期相匹配,内容的Last-Modified
日期将被更新,以便在另外的 n 秒内被视为新鲜。如果收到的Last-Modified
日期与客户拥有的日期不一致,则从服务器重新加载内容,并替换为客户拥有的内容。
If-Modified-Since: Wed, 15 Mar 2017 12:30:26 GMT
你现在可能会问,如果缓存的内容既有Last-Modified
,又有ETag
,怎么办?那么,在这种情况下,两者都要使用,也就是说,当且仅当 ETag 与新检索的内容相匹配,并且 Last-Modified 日期也相匹配时,将不会有资源重新下载。如果 ETag 不匹配或者 Last-Modified 日期大于服务器上的日期,内容就必须重新下载。