当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串,比如你打开头条号的首页,打开开发者工具,再刷新页面,查看network,可以看到有不少请求就是命中了协商缓存。
查看单个请求的ResponseHeader,也能看到304的状态码和Not Modified的字符串,只要看到这个就可说明这个资源是命中了协商缓存,然后从客户端缓存中加载的,而不是服务器最新的资源。
协商缓存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对Header来管理的。
【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.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。
- HTTP协议说明定义ETag为“被请求变量的实体值”。另一种说法是ETag是一个可以与Web资源关联的记号(token)。典型的Web资源可以一个Web页,但也可能是JSON或XML文档。服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,以下是服务器端返回的格式:ETag:”50b1c1d4f775c61:df3”客户端的查询更新格式是这样的:
If-None-Match : W / “50b1c1d4f775c61:df3”
如果ETag没改变,则返回状态304然后不返回,这也和Last-Modified一样。
把ETags和GET请求的“If-None-Match”头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
其过程如下:
客户端请求一个页面(A)。 服务器返回页面A,并在给A加上一个ETag。 客户端展现该页面,并将页面连同ETag一起缓存。
客户再次请求页面A,并将上次请求时服务器返回的ETag一起传递给服务器。
服务器检查该ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304(未修改——Not
Modified)和一个空的响应体。
- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间:
浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值:
服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 NotModified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header:
浏览器收到304的响应后,就会从缓存中加载资源。
如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值。
有时候刷新页面后协商缓存会改变为强缓存,如下图所示:
【Last-Modified,If-Modified-Since】都是根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合起来管理协商缓存是非常可靠的,但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况,而这种问题又很不容易被定位出来,而当这种情况出现的时候,就会影响协商缓存的可靠性。所以就有了另外一对header来管理协商缓存,这对header就是【ETag、If-None-Match】。它们的缓存管理的方式是:
浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上ETag的header,这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系,所以能很好的补充Last-Modified的问题:
浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match的header,这个header的值就是上一次请求时返回的ETag的值
服务器再次收到资源请求时,根据浏览器传过来If-None-Match和然后再根据资源生成一个新的ETag,如果这两个值相同就说明资源没有变化,否则就是有变化;如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化:
浏览器收到304的响应后,就会从缓存中加载资源。
- 协商缓存的管理
协商缓存跟强缓存不一样,强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。大部分web服务器都默认开启协商缓存,而且是同时启用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】,比如apache
如果没有协商缓存,每个到服务器的请求,就都得返回资源内容,这样服务器的性能会极差。
【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】一般都是同时启用,这是为了处理Last-Modified不可靠的情况。有一种场景需要注意:
分布式系统里多台机器间文件的Last-Modified必须保持一致,以免负载均衡到不同机器导致比对失败;
分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样);
协商缓存需要配合强缓存使用,你看前面这个截图中,除了Last-Modified这个header,还有强缓存的相关header,因为如果不启用强缓存的话,协商缓存根本没有意义。
**如果资源已经被浏览器缓存下来,在缓存失效之前,再次请求时,默认会先检查是否命中强缓存,如果强缓存命中则直接读取缓存,如果强缓存没有命中则发请求到服务器检查是否命中协商缓存,如果协商缓存命中,则告诉浏览器还是可以从缓存读取,否则才从服务器返回最新的资源。这是默认的处理方式,这个方式可能被浏览器的行为改变**:
当ctrl+f5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
当f5刷新网页时,跳过强缓存,但是会检查协商缓存
缓存分很多种:服务器缓存,第三方缓存,浏览器缓存等。其中浏览器缓存是代价最小的,因为浏览器缓存依赖的是客户端,而几乎不耗费服务器端的资源。
让浏览器做缓存需要给浏览器发送指定的Http头,告诉浏览器缓存多长时间,或者坚决不要缓存。
1.Expires(过期时间)
- HTTP头信息Expires(过期时间) 属性是HTTP控制缓存的基本手段,这个属性告诉缓存器:相关副本在多长时间内是新鲜的。过了这个时间,缓存器就会向源服务器发送请求,检查文档是否被修改。几乎所有的缓存服务器都支持Expires(过期时间)属性;
大部分Web服务器支持你用几种方式设置Expires属性;一般 的可以设计一个绝对时间间隔:基于客户最后查看副本的时间(最后访问时间)或者根据服务器上文档最后被修改的时间;
Expires 头信息:对于设置静态图片文件(例如导航栏和图片按钮)可缓存特别有用;因为这些图片修改很少,你可以给它们设置一个特别长的过期时间,这会使你的网站对 用户变得相应非常快;他们对于控制有规律改变的网页也很有用。
记住:HTTP的日期时间必须是格林威治时 间(GMT),而不是本地时间。举例:
Expires: Fri, 30 Oct 1998 14:19:41
所以使用过期时间属性一定要确认你的Web服务器时间设置正确,一个途径是通过网络时间同步协议(Network Time Protocol NTP),和你的系统管理员那里你可以了解更多细节。
- 缺点:首先:是牵扯到了日期,这样Web服务器的时间和缓存服务器的时间必须是同步的,如果有些不同步, 要么是应该缓存的内容提前过期了,要么是过期结果没及时更新。其次:如果你设置的过期时间是一个固定的时间,如果你返回内容的时候又没有连带更新下次过期的时间,那么之后所有 访问请求都会被发送给源Web服务器,反而增加了负载和响应时间;
2.Cache-Control(缓存控制) HTTP头信息
HTTP 1.1介绍了另外一组头信息属性:Cache-Control响应头信息,让网站的发布者可以更全面的控制他们的内容,并定位过期时间的限制。
有用的 Cache-Control响应头信息包括:
max-age=[秒] — 执行缓存被认为是最新的最长时间。类似于过期时间,这个参数是基于请求时间的相对时间间隔,而不是绝对过期时间,[秒]是一个数字,单位是秒:从请求时间 开始到过期时间之间的秒数。
s-maxage=[秒] — 类似于max-age属性,除了他应用于共享(如:代理服务器)缓存
public — 标记认证内容也可以被缓存,一般来说: 经过HTTP认证才能访问的内容,输出是自动不可以缓存的;
no-cache — 强制每次请求直接发送给源服务器,而不经过本地缓存版本的校验。这对于需要确认认证应用很有用(可以和public结合使用),或者严格要求使用最新数据 的应用(不惜牺牲使用缓存的所有好处);
no-store — 强制缓存在任何情况下都不要保留任何副本
must-revalidate — 告诉缓存必须遵循所有你给予副本的新鲜度的,HTTP允许缓存在某些特定情况下返回过期数据,指定了这个属性。
proxy-revalidate — 和 must-revalidate类似,除了他只对缓存代理服务器起作用
举例:
Cache-Control: max-age=3600, must-revalidate
Expires是绝对时间间隔;Cache-control是相对时间间隔
其实就应该给Expires设一个永远不会过期的时间,比如你现在有一个文件叫logo.gif,需要用一个新的logo的时候,你不要去 覆盖原来的文件,而把新的logo存成logo_v2.gif,让相关网页引用新的logo_v2.gif,这样可以让新老网页同时工作,实在犯不上为了 节省存储空间覆盖原有文件。
3.Last-Modified/If-Modified-Since
Web 服务是不变的:通常服务器知道你所请求的数据的最后修改时间,并且 HTTP 为服务器提供了一种将最近修改数据连同你请求的数据一同发送的方法。
如果你第二次 (或第三次,或第四次) 请求相同的数据,你可以告诉服务器你上一次获得的最后修改日期:在你的请求中发送一个 If-Modified-Since 头信息,它包含了上一次从服务器连同数据所获得的日期。如果数据从那时起没有改变,服务器将返回一个特殊的 HTTP 状态代码 304,这意味着 “从上一次请求后这个数据没有改变”。
所有现代的浏览器都支持最近修改 (last-modified) 的数据检查。浏览器自动发送首次访问时从服务器获得的最近修改日期。服务器简单地返回 304: Not Modified (没有修改),因此浏览器就会知道从本地缓存加载页面。
4. ETag/If-None-Match
ETag 是实现与最近修改数据检查同样的功能的另一种方法:没有变化时不重新下载数据。其工作方式是:服务器发送你所请求的数据的同时,发送某种数据的 hash (在 ETag 头信息中给出)。hash 的确定完全取决于服务器。当第二次请求相同的数据时,你需要在 If-None-Match: 头信息中包含 ETag hash,如果数据没有改变,服务器将返回 304 状态代码。与最近修改数据检查相同,服务器仅仅 发送 304 状态代码;第二次将不为你发送相同的数据。在第二次请求时,通过包含 ETag hash,你告诉服务器:如果 hash 仍旧匹配就没有必要重新发送相同的数据,因为你还有上一次访问过的数据。
总结:
Expires/Cache-Control
Header是控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只是Cache-Control比Expires可以控制的多一些,
而且Cache-Control会重写Expires的规则。Last-Modified/If-Modified-Since和ETag/If-None-Match是浏览器发送请求到服务器后判断文件是否已经修改过,如果没有修改过就只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器。