http缓存机制是指在客户端与服务器发送与接收消息过程中,如果客户端或者中间代理服务器已缓存资源,重复利用已缓存资源,而省略服务器重新发送资源的机制。
## http报文中与缓存相关的首部字段

通用首部字段

字段名称 说明
Cache-Control 控制缓存的行为
Pragma http1.0的字段,值为”no-cache”时禁用缓存

请求首部字段

字段名称 说明
If-Match 比较Etag是否一致
If-None-Match 比较Etag是否不一致
If-Modified-Since 比较资源最后更新的时间是否一致
If-Unmodified-Since 比较资源最后更新的时间是否不一致

响应首部字段

字段名称 说明
Etag 资源的匹配信息

实体首部字段

字段名称 说明
Expires http1.0中的字段,实体主体过期的时间
Last-Mpdified 资源的最后一次修改的时间

字段详细说明

Pragma与Expires


  1. Pragma和Expires是http1.0中字段
    当服务器响应字段设置Pragma=no-cache时,不对资源进行缓存
    客户端则需要添加 <meta http-equiv="Pragma" content="no-cache">(不同浏览器行为不同)

  2. Expires设置资源缓存过期时间,值为GMT时间
    若资源请求时已经超过该时间,则重新请求

Cache-Control


该字段规定http主要缓存行为。作为请求首部时

字段值 说明
no-cache 告知代理服务器不直接使用缓存,要求向原服务器发起请求
no-store 所有内容都不会被保存到缓存或Internet临时文件
max-age=delta-seconds 告知服务器客户端希望接收一个存在时间不大于delta-seconds的资源
max-state [=delta-seconds] 告知(代理)服务器愿意接收一个超过缓存时间不大于delta-seconds的资源,若delta-seconds没有设置,则为任意超出时间
min-fresh=delta-seconds 告知(代理)服务器希望接收一个在小于delta-seconds内被更新过的资源
no-transform 告知(代理)服务器希望接收没有被转换(比如压缩)的实体数据
only-if-cached 告知(代理)服务器希望接收缓存的资源(若有),而不向原服务器发起请求
cache-extension 自定义扩展值,若服务器不识别则该值将被忽略掉

作为响应首部时

字段值 说明
public 任何情况下都得缓存该资源
Private [=”field-name”] 响应报文中的全部或部分(指明field-name的字段)仅开放给某些用户或代理服务器做缓存使用,其他用户不能缓存该资源
no-cache 不直接使用缓存,需要向服务器发起(新鲜度校验)请求
no-store 所有内容都不会进行缓存
no-transform 告知客户端缓存文件时不对数据进行任何改变
only-if-cached 告知(代理)服务器客户端希望获取缓存的内容(若有),而不向原服务器发起请求
must-revalidate 当前资源一定要向原服务器发送验证请求,请求失败会返回504
proxy-revalidate 与must-validate相似,但仅能用于共享缓存
max-age=delta-seconds 告知客户端资源在delta-seconds内缓存有效,无需向原服务器发起请求
s-max-age=delta-seconds 与max-age相似,但仅能用于共享缓存
cache-extension 自定义扩展值,若服务器不识别该值将被忽略

Cache-Control 允许自由组合可选值,例如:
Cache-Control: max-age=3600, must-revalidate
它意味着该资源是从原服务器上取得的,且其缓存(新鲜度) 的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。

no-cache与no-control的不同
“no-cache”表示必须先与服务器确认返回的响应是否发生了变化,然后才能使用该响应来满足后续对同一网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,但如果资源未发生变化,则可避免下载。

相比之下,“no-store”则要简单得多。它直接禁止浏览器以及所有中间缓存存储任何版本的返回响应,例如,包含个人隐私数据或银行业务数据的响应。每次用户请求该资产时,都会向服务器发送请求,并下载完整的响应。

缓存校验字段


上述缓存控制行为指定客户端是否向服务器发起请求,若不请求则使用浏览器缓存,响应首部状态为(200 from Cache)。
但是即便向服务器发起请求也不一定要从服务器获取资源,若资源相对上次请求未发生改变,那重新接收也会浪费带宽和时间。

为了让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,Http1.1新增了几个首部字段来做这件事情。
1.Last-Modified 与If-Modified-Since

服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT ”的形式加在实体首部上一起返回给客户端。

客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码即可。

至于传递标记起来的最终修改时间的请求报文首部字段一共有两个:

⑴ If-Modified-Since: Last-Modified-value

示例为 If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT
该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。

当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。

⑵ If-Unmodified-Since: Last-Modified-value

告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了) ,则应当返回412(Precondition Failed) 状态码给客户端。

当遇到下面情况时,If-Unmodified-Since 字段会被忽略:

  • Last-Modified值对上了(资源在服务端没有新的修改);
  • 服务端需返回2XX和412之外的状态码;
  • 传来的指定日期不合法
    Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源) 。

2.ETag 与 If-None-Match

为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了 ETag 实体首部字段。

服务器会通过某种算法,给资源计算得出一个唯一标志符(比如md5标志) ,在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符 ”一起返回给客户端。

客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。

如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag) 发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

那么客户端是如何把标记在资源上的 ETag 传去给服务器的呢?请求报文中有两个首部字段可以带上 ETag 值:

⑴ If-None-Match: ETag-value

示例为 If-None-Match: “56fcccc8-1699”
告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。

当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。

⑵ If-Match: ETag-value

告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。

If-Match 的一个应用场景是,客户端走PUT方法向服务端请求上传/更替资源,这时候可以通过 If-Match 传递资源的ETag。

需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样。

如果 Last-Modified 和 ETag 同时被使用,则要求它们的验证都必须通过才会返回304,若其中某个验证没通过,则服务器会按常规返回资源实体及200状态码。

缓存实践


当我们在一个项目上做http缓存的应用时,我们还是会把上述提及的大多数首部字段均使用上,例如使用 Expires 来兼容旧的浏览器,使用 Cache-Control 来更精准地利用缓存,然后开启 ETag 跟 Last-Modified 功能进一步复用缓存减少流量。

那么Expires 和 Cache-Control 的值应设置为多少合适呢?

答案是不会有过于精准的值,均需要进行按需评估。

例如页面链接的请求常规是无须做长时间缓存的,从而保证回退到页面时能重新发出请求,百度首页是用的 Cache-Control:private,腾讯首页则是设定了60秒的缓存,即 Cache-Control:max-age=60。

而静态资源部分,特别是图片资源,通常会设定一个较长的缓存时间,而且这个时间最好是可以在客户端灵活修改的。
当然这需要有一个前提——静态资源能确保长时间不做改动。如果一个脚本文件响应给客户端并做了长时间的缓存,而服务端在近期修改了该文件的话,缓存了此脚本的客户端将无法及时获得新的数据。

解决该困扰的办法也简单——把服务侧ETag的那一套也搬到前端来用——页面的静态资源以版本形式发布,常用的方法是在文件名或参数带上一串md5或时间标记符:

https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg

如果文件被修改了,才更改其标记符内容,这样能确保客户端能及时从服务器收取到新修改的文件。

其它相关首部字段


其他几个跟缓存有关系,但没那么主要的响应首部字段。

1.Vary

表示服务端会以什么基准字段来区分、筛选缓存版本。

当存在这样的使用场景:服务器需要根据不同的客户端(如不同浏览器)来发送不同的资源,尽管可以通过User-Agent来进行识别,但如果请求的是代理服务器,就会出现问题了。

因此 Vary 便是处理该问题的首部字段,我们可以在响应报文加上:
Vary: User-Agent
便能知会代理服务器需要以 User-Agent 这个请求首部字段来区别缓存版本,防止传递给客户端的缓存不正确。

Vary 也接受条件组合的形式:
Vary: User-Agent, Accept-Encoding
这意味着服务器应以 User-Agent 和 Accept-Encoding 两个请求首部字段来区分缓存版本。

2.Date 与 Age

HTTP并没有提供某种方法来帮用户区分其收到的资源是否命中了代理服务器的缓存,但在客户端我们可以通过计算响应报文中的Date和Age字段来得到答案。

Date是原服务器发送该资源响应报文的时间(GMT格式),如果Date 的时间与“当前时间”差别较大,或者连续F5刷新发现 Date 的值都没变化,则说明你当前请求是命中了代理服务器的缓存。
Age是响应报文中的首部字段,它表示该文件在代理服务器中存在的时间 ,如文件被修改或替换,Age会重新由0开始累计。

通常还满足这么个条件:
静态资源Age + 静态资源Date = 原服务端Date