不管是软件应用仍是硬件应用,缓存都扮演着重要的角色,其对提高性能的重要性不容置疑。html
本文主要介绍 HTTP 缓存,涉及其原理和应用。HTTP 缓存主要经过 HTTP 首部来控制。web
先看一个简单的缓存示例:浏览器
app.js
时,服务器会返回资源内容和相关头部,其中 Cache-Control: max-age=120
告诉浏览器说,这个资源的缓存有效期为 120 秒,从当前时间 Date: Mon, 05 Mar 2018 08:00:00 GMT
开始算起。浏览器收到资源后便将 app.js
及其相应头部存储在本地。05 Mar, 2018 08:02:00 GMT
以前再次请求 app.js
,则浏览器会直接使用存储在本地的资源,而不用再次向服务器发起请求。这个过程当中,咱们就说 app.js
被缓存且命中了。缓存
在进一步理解缓存以前,先看下跟缓存相关的几个概念:服务器
HTTP 缓存涉及到请求-响应链上的多个角色,包括客户端(本文指浏览器)、代理和服务器。
其中,浏览器自身也实现了缓存功能。浏览器在请求资源时,老是先从本地缓存中查找,若是找到未过时资源,则直接使用,不然向服务器发起请求。
代理也是服务器的一种,但通常状况下不会把它单独抽出来分析,只有在跟它有关的地方会把它区分于源服务器。因此,下文的示例图中将不会把它列进去。app
HTTP 缓存的理解基本上能够总结为三个问题:性能
问题 1 说明存储缓存数据的设备是多样的,能够存储于各级代理服务器,也能够存储于浏览器本地。ui
问题 2 说明使用什么办法来判断缓存数据是否已通过期,固然是比较时间啦,那么如何比较呢?google
问题 3 说明缓存虽然过时了,可是其内容仍然可能与服务端一致,这时就不必从新下载相同数据,只须要向服务端询问下是否能够继续使用缓存便可。spa
带着上面三个问题去理解 HTTP 缓存头部设置会更有助于理解和记忆。
有人根据是否须要进行问题 3 中的从新验证把缓存策略的设置分为强缓存和协商缓存,强缓存无须再次验证的缓存策略,协商缓存是须要再次验证的缓存策略。
二者的区别在于,协商缓存多发起了一次 HTTP 请求。
HTTP 缓存主要经过 HTTP 首部来实现缓存控制。这些与缓存相关的 HTTP 首部这里统称为缓存首部,具体首部以下表所示。
首部字段 | 首次定义 | 首部类型 |
---|---|---|
Pragma | HTTP/1.0 | 通用首部 |
Age | HTTP/1.1 | 响应首部 |
Expires | HTTP/1.0 | 实体首部 |
Cache-Control | HTTP/1.1 | 通用首部 |
Etag | HTTP/1.1 | 响应首部 |
If-Match | HTTP/1.1 | 请求首部 |
If-None-Match | HTTP/1.1 | 请求首部 |
If-Modified-Since | HTTP/1.0 | 请求首部 |
Last-Modified | HTTP/1.0 | 实体首部 |
其中,“首次定义”是指首次出如今哪一个 HTTP 版本。之因此列出这项内容,是由于实际应用须要考虑兼容旧版 HTTP 。
现代的 HTTP 缓存策略主要使用 Cache-Control
实现,它是目前最新的缓存首部,用于取代较老的缓存首部如 Pragma
、Expires
等。因此应用中应该倾向于使用 Cache-Control
。可是为了支持只实现了 HTTP/1.0 的客户端设备,服务端一般仍是都会同时设置 Expires
、Pragma
和 Cache-Control
等,此时 Cache-Control
会有更高的优先级。提醒一下,现代浏览器都已支持 Cache-Control
。
Cache-Control
是通用首部,这意味着它既能够出如今请求中,也能够出如今响应中。
Cache-Control
的值可由多个字段组合而成,以逗号分隔,如 Cache-Control: private,max-age=3600
。下面对经常使用的可取字段进行说明。
public
: 表示当前响应数据全部用户共享的,能够被任何设备缓存,包括客户端、代理服务器等。
private
: 表示当前响应数据是单个用户所独占的,只能被客户端缓存,不能被代理服务器缓存。
max-age=<seconds>
: 指定缓存的有效时间,单位为秒。其值是任意整数,0 和负数表示缓存过时,正数值加上当前响应头中的 Date
首部值即为过时时间。
max-stale[=<seconds>]
: 只用于请求,表示客户端仍然愿意接受过时缓存,只要过时时间没超过指定时间,若是未指定时间,则表示任何过时的时间。
min-fresh=<seconds>
: 只用于请求,表示客户端愿意接受还剩余多少秒过时的缓存。
s-maxage=<seconds>
: 功能与 max-age
一致,但它仅做用于共享缓存,对私有缓存无效。
no-cache
: 并不是字面意思,它并不是禁止缓存,而是强制在使用已缓存数据以前,须要去服务端验证一下是否可使用缓存数据。
no-store
: 真正的禁止缓存,任何设备都不容许缓存,每次请求都须要向服务端从新获取数据。
no-transform
: 表示响应的实体数据不该被转换。Content-Encoding
、Content-Range
和 Content-Type
首部也不能被修改。实际应用中,有些代理服务器会对图片资源进行格式转换以节省空间或者带宽。
做为通用首部,其部分指令值能够出如今请求首部,也能够出如今响应首部,二者可能略有区别:
指令值 | 请求 | 响应 |
---|---|---|
public |
- | 可共享数据,可被任何设备缓存 |
private |
- | 用户私有数据,只能被客户端缓存 |
no-cache |
使用前需验证 | 使用前需验证 |
no-store |
禁止使用缓存数据 | 禁止缓存 |
max-age |
要求资源的 age 小于这个时间 | 最大过时时间 |
min-fresh |
要求资源至少还剩余多少过时时间 | - |
max-stale |
超过过时时间多少秒内仍愿意接受 | - |
no-transform |
不要转换格式 | 不要转换格式 |
这些指令用在请求首部的状况比较少见,最可能接触的地方是 Chrome DevTools
中的 Network
标签页。
其中,有个 Disable cache
选项,选中后 DevTools
会自动给全部请求头部加上 Cache-Control: no-cache
首部,以告诉浏览器和代理使用本地缓存以前必须先验证。
If-Modified-Since
首部比较的是资源的修改时间,精度为秒,是一种缓存过时后的经常使用验证方式。通常来讲,验证资源是否修改过,对比资源的修改时间是一种最简单的办法。
使用过程以下:
app.js
时,服务器响应带上 Last-Modified
首部,告诉客户端当前资源的最后修改时间。客户端根据 Cache-Control: max-age=120
,把 app.js
和响应首部缓存起来。app.js
时,把以前保存的 Last-Modified
时间放入 If-Modified-Since
首部发给服务器。服务器发现资源的 Last-Modified
时间没有发生改变,因而直接响应 304 。客户端收到 304 后,直接使用缓存的 app.js
,同时更新缓存有效期。app.js
时,把以前保存的 Last-Modified
时间放入 If-Modified-Since
首部发给服务器。服务器发现资源的 Last-Modified
时间已经发生改变,因而响应 200 ,将修改后的 app.js
和新的 Last-Modified
发送给客户端。客户端收到 200 后,从新下载新的 app.js
,并把新的 app.js
和响应首部缓存起来,替换原先的旧缓存。ETag
叫实体标签(Entity Tag),用于表示实体资源是否发生变化,其生成原理相似 MD5 ,也是一种用于验证的首部。当响应的首部信息或者消息实体发生变化时,实体标签也会改变。
使用过程以下:
app.js
时,服务器响应带上 ETag
首部,告诉客户端当前资源的实体标签。客户端根据 Cache-Control: max-age=120
,把 app.js
和响应首部缓存起来。app.js
时,把以前保存的 ETag
值放入 If-None-Match
首部发给服务器。服务器发现本身的资源 ETag
值并没有发生改变,因而直接响应 304 。客户端收到 304 后,直接使用缓存的 app.js
,同时更新缓存有效期。app.js
时,把以前保存的 ETag
值放入 If-None-Match
首部发给服务器。服务器发现本身的资源 ETag
值已经发生改变,因而响应 200 ,将修改后的 app.js
和新的 ETag
发送给客户端。客户端收到 200 后,从新下载新的 app.js
,并把新的 app.js
和响应首部缓存起来,替换原先的旧缓存。当客户端本地存储有多个版本的资源时,会把全部的实体标签都上传,形如 ETag: "abc","def"
,服务端会使用 ETag
首部返回匹配中的实体标签值。
实体标签分为强标签(Strong ETag)和弱标签(Weak ETag),弱标签以 W/
开头,如 ETag: W/"1234"
。强标签使用强比较,弱标签使用弱比较。强比较意味着两个比较对象的每个字节都相同,弱比较意味着二者语义相同(Semantic Equivalence)。举个栗子,假如响应首部包含一个渲染时间 Rendered-Time
,A 响应的渲染时间为 365,B 响应的渲染时间为 345,两个响应的实体内容一致。这种状况下,咱们能够说 A 和 B 弱比较相等,强比较不相等。
通常来讲,静态内容使用强标签,动态生成的内容使用弱标签。
由此能够看出,实体首部能够解决一些 Last-Modified
没法解决的问题:
If-Match
和 ETag
的另外一种用法:避免“空中碰撞”,以防编辑冲突。当客户端使用 PUT 或者 POST 更新服务端资源时,须要使用 If-Match
来携带实体标签给服务端,以确保客户端要修改的资源没有被别人修改过,避免覆盖别人的修改。不过这种用法比较少,能够不用深究。
Expires
指明资源的过时时间,如 Expires: Wed, 04 Jul 2012 08:26:05 GMT
。非法的日期格式(如 0)将会被当作过去的时间,表示该资源已通过期。
若是 Expires
和 Cache-Control
的 max-age
或者 s-maxage
同时出现,Expires
将被忽略。
Age
表示资源在代理服务器上已经缓存了多久时间,单位为秒。若是是 Age: 0
,代表该资源刚刚从服务器获取。它的计算方式通常使用代理服务器当前的时间减去缓存资源的 Date
时间。
Pragma
是 HTTP/1.0 中引入的首部,如今使用时通常用于向后兼容 HTTP/1.0,不鼓励使用。
Pragma: no-cache
的做用与 Cache-Control: no-cache
一致,表示须要跟服务器进行验证后才能使用缓存资源。
并非每一个服务器都会返回明确的缓存策略,这种状况下客户端会采起启发式缓存策略。注意,只有在服务端没有返回明确的缓存策略时才会激活启发式缓存策略。
启发式缓存策略会根据其余的首部信息来计算一个过时时间,其余的首部一般是 Date
和 Last-Modified
。此时,缓存有效期通常取二者差值的 10% 。
使用启发式缓存策略时,若是超过当前时间 24 小时且从未警告过,浏览器或者代理服务器应该在响应中产生一个警告首部字段 Warning: 113
。