【Browser】浏览器中的缓存机制

Posted by 西维蜀黍 on 2017-03-02, Last Modified on 2021-10-14

Background

浏览器中的缓存机制,其实就相当于HTTP协议定义的缓存机制,因为浏览器为我们实现了它。一般情况下我们会想到HTTP响应头中的 Pragma,Expires,Cache-Control,Last-Modified,If-Modified-Since,Etag ,因为这些域与缓存相关。

浏览器对这些域的处理:

Pragma域

Pragma行是为了兼容HTTP1.0,作用与

Cache-Control: no-cache

是一样的。

浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。

但是也有非HTTP协议定义的缓存机制,如使用HTML Meta 标签,Web开发者可以在HTML页面的节点中加入标签,代码如下:

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">

它告诉浏览器每次请求页面时都不要读缓存,都得往服务器发一次请求才行。

BUT!!! 事实上这种禁用缓存的形式用处很有限:

  1. 仅有IE才能识别这段meta标签含义,其它主流浏览器仅能识别“Cache-Control: no-store”的meta标签。
  2. 在IE中识别到该meta标签含义,并不一定会在请求字段加上Pragma,但的确会让当前页面每次都发新请求_(仅限页面,页面上的资源则不受影响)_。

做了测试后发现也的确如此,这种客户端定义Pragma的形式基本没起到多少作用。不过如果是在响应报文上加上该字段就不一样了:

如上图红框部分是再次刷新页面时生成的请求,这说明禁用缓存生效,预计浏览器在收到服务器的Pragma字段后会对资源进行标记,禁用其缓存行为,进而后续每次刷新页面均能重新发出请求而不走缓存。

Expires域

有了Pragma来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http1.0而言,Expires 就是做这件事的首部字段。

Expires 存储是一个用来控制缓存失效的日期

当浏览器看到Response中有一个Expires头时,它会和相应的页面一起保存到其缓存中,只要页面没有过期,浏览器就会使用缓存中保存的页面版本而不会进行任何向服务器发送的HTTP请求,功能等同于max-age(下文会详细描述);若该页面超过了Expires域中指定时间,Browser会重新访问Server获取页面数据。

设置的日期格式必须为GMT(格林尼治标准时间)。若此属性在一页上设置了多次,则使用最短的时间。

Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大(比如时钟不同步,或者跨时区),那么误差就很大,所以在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代。,当Cache-Control的max-ageExpires同时存在时,Expires值会被Cache-Control的max-age覆盖。

在客户端我们同样可以使用meta标签来知会IE_(也仅有IE能识别)页面(同样也只对页面有效,对页面上的资源无效)_缓存时间:

<meta http-equiv="Expires" content="Mon, 20 Jul 2009 23:00:00 GMT" />

如果希望在IE下页面不走缓存,希望每次刷新页面都能发新请求,那么可以把“content”里的值写为“-1”或“0”。

如果是在服务端报头返回Expires字段,则在任何浏览器中都能正确设置资源缓存的时间:

在上图里,缓存时间设置为一个已过期的时间点_(见红框),则刷新页面将重新发送请求(见蓝框)_。

那么如果Pragma和Expires一起上阵的话,听谁的?我们试一试就知道了:

我们通过Pragma禁用缓存,又给Expires定义一个还未到期的时间_(红框),刷新页面时发现均发起了新请求(蓝框)_,这意味着Pragma字段的优先级会更高。

Cache-Control域

该域在HTTP1.1中添加。

服务器响应浏览器请求时响应头中的Cache-Control使得每个资源都可以通过 Cache-Control HTTP 头来定义自己的缓存策略,Cache-Control 指令用来告诉 Browser,该资源在什么条件下可以缓存,以及可以缓存多久。

Cache-Control头参数的含义(响应头中的Cache-Control)

  • no-cache : 表示必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
  • no-store : 禁止缓存任何响应,也就是说每次用户请求资源时,都会向服务器发送一个请求,每次都会下载完整的响应。
  • public所有内容都将被缓存(客户端和代理服务器都可缓存)。
  • private : 浏览器可以缓存private响应,但是通常只为单个用户缓存,因此,不允许任何代理服务器对其进行缓存 。比如,用户浏览器可以缓存包含用户私人信息的 HTML 网页,但是 CDN 不能缓存。
  • max-age : 用来设置资源被缓存的最长时间(单位是秒);如果Cache-Control(且Cache-Control值为max-age)和Expires同时出现,则max-age有更高的优先级,浏览器会根据max-age的时间来确认缓存过期时间。在设置该值后,在设定的时间内都会使用这个版本的资源,即使服务器上的资源发生了变化,浏览器也不会得到通知。

若报文中同时出现了 Pragma、Expires 和 Cache-Control,会以 Cache-Control 为准。

Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。在RFC中规范了 Cache-Control 的格式为:

"Cache-Control"  ":"  cache-directive

作为请求首部时,cache-directive 的可选值有:

作为响应首部时,cache-directive 的可选值有:

Last-Modified、If-Modified-Since

在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:

   Last-Modified: Fri, 12 May 2006 18:53:33 GMT

客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间之后文件是否有被修改过:

   If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT

如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。

Last-Modified/If-Modified-Sincec搭配使用

Last-Modified/If-Modified-Since也配合Cache-Control使用。

**Last-Modified:**标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。

**If-Modified-Since:**当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。

Etag/If-None-Match

Etag/If-None-Match也要配合Cache-Control使用。

Etag 是根据实体内容生成一段hash字符串,标识资源的状态,由服务端产生。浏览器会将这串字符串传回服务器,验证资源是否已经修改,如果没有修改,过程如下:

1 Etag配合If-None-Match使用

Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。

ETags和If-None-Match是一种常用的判断资源是否改变的方法。类似于Last-Modified和HTTP-IF-MODIFIED-SINCE。但是有所不同的是Last-Modified和HTTP-IF-MODIFIED-SINCE只判断资源的最后修改时间,而ETags和If-None-Match可以是资源任何的任何属性,不如资源的MD5等。

ETags和If-None-Match的工作原理是在HTTP Response中添加ETags信息。当客户端再次请求该资源时,将在HTTP Request中加入If-None-Match信息(ETags的值)。如果服务器验证资源的ETags没有改变(该资源没有改变),将返回一个304状态;否则,服务器将返回200状态,并返回该资源和新的ETags。

2 使用Etag相对于Last-modified的优点(使用ETag可以解决Last-modified存在的一些问题)

  1. 某些服务器不能精确得到资源的最后修改时间,这样就无法通过最后修改时间判断资源是否更新
  2. 如果资源修改非常频繁,在秒以下的时间内进行修改,而Last-modified只能精确到秒
  3. 一些资源的最后修改时间改变了,但是内容没改变,使用ETag就认为资源还是没有修改的。

3 既生Last-Modified何生Etag

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag一起使用时,服务器会优先验证ETag。

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

ETag生成规则

我们对ETag寄予厚望,希望它对于每一个url生成唯一的值,资源变化时ETag也发生变化。神秘的Etag是如何生成的呢?以Apache为例,ETag生成靠以下几种因子

  1. 文件的i-node编号,此i-node非彼iNode。是Linux/Unix用来识别文件的编号。是的,识别文件用的不是文件名。使用命令’ls –I’可以看到。
  2. 文件最后修改时间
  3. 文件大小: 生成Etag的时候,可以使用其中一种或几种因子,使用抗碰撞散列函数来生成。所以,理论上ETag也是会重复的,只是概率小到可以忽略。

组合使用方法

1 最基本的设置:Cache-control: max-age=[secs] (类似于Expires域的功能)

这个是最最基础的一种策略,[secs]是cache在客户端存活的秒数,例如 Cache-control: max-age=1800 表明cache的时间是半小时,只使用这样一个声明就可以使浏览器能够将这个HTTP响应的内容写入临时目录做cache。

这里是简要过程:

—首次访问时—

  1. 浏览器第一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现无Cache存储,遂发出请求到Web Server
  3. Web Server响应资源,并设定Cache-control:max-age=300
  4. 浏览器收到响应将资源呈献给用户的同时,在临时文件目录以 http://test.qq.com/test.cgi 为key缓存这个响应

—5分钟内—

  1. 浏览器再一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现存在cache存储,检查保鲜期max-age < 300,还未过期,则直接读取(不访问Web Server),响应给用户。

—5分钟后—

  1. 浏览器再一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现存在Cache存储,检查保鲜期max-age,已经过期,则发请求到Web Server

2 保鲜期(Cache-control: max-age) + 最后修改时间验证(Last-Modified)

这里的要素是,在给出保鲜期的同时,给出一个资源的验证方式: Last-Modified: [UTC time] ([UTC time]标示这个响应资源的最后修改时间) 例如 Last-Modified: Mon, 06 Jul 2009 09:21:48 GMT 这个响应头只有配合Cache-control的时候才有实际价值,只是声明校验资源的方式,并不能影响资源的保鲜期时长

** 这里是简要过程:**

—首次访问时—

  1. 浏览器第一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现无Cache存储,遂发出请求到Web Server
  3. web server响应资源,并设定Cache-control:max-age=300 Last-Modified: Mon, 06 Jul 2009 09:21:48 GMT
  4. 浏览器收到响应将资源呈献给用户的同时,在临时文件目录以 http://test.qq.com/test.cgi 为key缓存这个响应

—5分钟内—

  1. 浏览器再一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现存在cache存储,检查保鲜期max-age < 300,还未过期,则直接读取(不访问Web Server),响应给用户。

—5分钟后—

  1. 浏览器再一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现存在cache存储,检查保鲜期max-age,已经过期发现资源具有Last-Modified声明,则为请求带上头 If-Modified-Since: Mon, 06 Jul 2009 09:21:48 GMT 发送请求到web server
  3. web server收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对,若最后修改时间较新,说明资源又被改动过,则响应整片资源内容,HTTP 200 (需要整块内容写为包体).若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体),告知浏览器继续使用所保存的cache,(这里当然也可以根据自己的需要决定是200还是304,我们的CGI毕竟是一种原始的实现)

3 保鲜期(Cache-control: max-age) + 自定义标识验证(Etag)

这里的要素是,在给出保鲜期的同时,给出另一种资源的验证方式: ETag: [custom flag] [custom flag]标示这个响应资源的由开发者自己确定的签名验证标识,例如 ETag: “abcdefg”,这个响应头只有配合Cache-control的时候才有实际价值,是声明校验资源的方式。ETag的使用为我们实现304响应提供了更多的灵活性,我们可以抛开必须将验证转化成时间格式的限制。

** 这里是简要过程:** —首次访问时—

  1. 浏览器第一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现无Cache存储,遂发出请求到Web Server
  3. web server响应资源,并设定
    • Cache-control:max-age=300
    • ETag: “abcdefg”
  4. 浏览器收到响应将资源呈献给用户的同时,在临时文件目录以 http://test.qq.com/test.cgi 为key缓存这个响应

—5分钟内— (同#1中II)

—5分钟后—

  1. 浏览器再一次请求资源http://test.qq.com/test.cgi
  2. 查询临时文件目录发现存在cache存储,检查保鲜期max-age,已经过期发现资源具有ETag声明,则为请求带上头 If-None-Match: “abcdefg”,发送请求到web server
  3. web server收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,Etag可以是一个版本号,可以是短时间戳,可以是资源校验和(强烈不推荐使用),或者干脆是一个常量(可以干脆拿来做容错)。 If-None-Match发来的串与我们的自有值比对,根据我们自己的任何策略算法,可以自由决定如何返回浏览器,304或200。 这里有一个使用ETag来做容错的例子(应用列表目前在使用):
  • 我们的每次正常返回都是200 Cache-control: max-age=1800 ETag: “anything” 这里anything是个常量,我们只用来告诉浏览器,cache过期要发带If-None-Match的请求过来
  • 这样来自客户端的一大部分请求基本上都会带上If-None-Match头,我们的CGI据此可以知道这个请求的客户端是否有cache,此时如果 CGI联系server失败,那么可以直接返回304,驱使客户端使用上一次cache的正确结果,且更新保鲜期max-age为300秒,这样我们实现 了一个基于HTTP cache的容错,如果我们的资源还能实现一套时间戳存储的话,那么我们可以在正常情况下也实现校验后的304,从而节省流量

几种状态码的区别

用户行为与缓存

浏览器缓存行为还有用户的行为有关,如果大家对 强制刷新(Ctrl + F5)

因设置缓存参数客户端数据不能及时更新的解决方案

背景:某次投产,某系统投产后由于强缓存设置时间不恰当导致变更的功能没有体现。后来通过变更文件路径强行解决问题。

变更上下文根,导致URL变化一定可以解决问题。但我们不可能每一次都这么做;还有,在浏览器端关闭缓存、或者清除缓存后再继续浏览、同时使用Ctrl+F5刷新,也可以解决问题,但是我们也不可能让每一个客户在投产后都做一次这个操作。 那我们怎么办呢?从问题原因来看,是将经常变化的资源缓存时间设置的过长导致的。理论上来讲,只要正确划分经常变化资源与不经常变化资源就可以解决问题。但是谁也不能保证不经常变化的资源就一定不会变化。 万一不经常变化的资源变更了怎么办呢?**在资源请求的URL中增加一个参数,比如:css/main.css?v=20160105。这个参数是一个版本号,客户化在js代码中,每一次投产的时候变更一下,当这个参数变化的时候,强缓存都会失效并重新加载。**这样一来,即使是不常变化的资源,投产以后也需要重新加载。这样就完美的解决了问题。

哪些请求不能被缓存?

无法被浏览器缓存的请求:

  1. HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
  2. 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
  3. 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
  4. POST请求无法被缓存
  5. HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存