协商的过程很容易理解,首先,当浏览器向Web服务器请求一些内容时,Web服务器需要告诉浏览器哪些内容可以被缓存,一旦浏览器知道某个内容可以缓存后,下次当浏览器需要请求这个内容时,它便不会直接向服务器请求完整内容,而是询问服务器是否可以使用本地的缓存,服务器在收到浏览器的询问后需要作出果断的回应,到底是允许浏览器使用本地缓存还是将最新的内容传回浏览器。
If-Modified-Since/304
) HTTP协议中规定使用GMT时间,也就是格林威治标准时间,而我们国家使用的是GMT+8时区,所以在HTTP头信息中的时间会比我们的正常时间早8个小时,但它一点都不影响HTTP缓存的正常工作。
HTTP响应头中多出了以下的标记:
Last-Modified: Fri, 20 Mar 2009 07:53:02 GMT
浏览器不再无动于衷,它在HTTP请求头中增加了以下这段标记:
If-Modified-Since: Fri, 20 Mar 2009 07:53:02 GMT
这意味着浏览器在询问Web服务器:“我请求的内容在这个时间之后是否有更新?”此时Web服务器肩负起了重要责任,它需要检查这个内容在该时间后是否有过更新,并反馈给浏览器,这一过程便相当于我们传统意义上的缓存过期检查,对于静态内容来说,Web服务器可以轻松搞定,只要获得静态文件的最后修改时间并将其和浏览器询问的时间进行对比即可。
这里我们需要关注的是响应状态码的变化,304 Not Modified意味着Web服务器告诉浏览器,这个内容没有更新,浏览器可以使用本地缓存的内容。同时,Web服务器没有将内容的正文传送给浏览器。
If-None-Match/304
) HTTP/1.1还支持另一种缓存协商方法,那就是ETag,它与前面所讲的协商方式非常类似,但它没有采用内容的最后修改时间,而是采用了一串编码来标记内容,称为ETag。一个原则是,如果一个内容的ETag没有变化,那么这个内容也一定没有更新。
ETag由Web服务器来生成,比如Apache为一个静态文件的HTTP响应头增加了以下标记:
ETag: "74177-b-46585209c1bc0"
浏览器获得这个内容的ETag后,便会在下次请求该内容时,在HTTP请求头中附加以下标记来询问服务器该内容是否发生变化:
If-None-Match: "74177-b-46585209c1bc0"
这时服务器就需要重新计算这个内容的ETag值,并与HTTP请求中的ETag进行比较,如果相同的话,便返回304状态码,如果不同的话,则将最新的内容返回给浏览器。
HTTP/1.1并没有规定ETag的具体格式和计算方法,也就是说,Web服务器可以自由定义ETag的格式和计算方法,比如一种简单的方法是对文件内容计算md5值作为ETag,总之只要能够起到标识内容的作用即可,ETag: "1944822255"
使用基于最后修改时间的缓存协商存在一些缺点,比如,有时候一些文件需要频繁的更新,但是内容可能并没有变化,那么如果采用基于最后修改时间的缓存协商,那么每次文件的修改时间变化后,不论内容是否真的变化,浏览器都会重新获取全部内容;再比如, 同一个文件存储在多台Web服务器上,用户的请求在这些服务器之间轮询,实现负载均衡,而这些服务器上同一个文件的最后修改时间很难保证完全相同 ,这便会导致用户的请求每次切换到新的服务器时就需要重新获取所有内容。这时候,如果使用直接标记内容的某种ETag算法,就可以避免这些问题。
HTTP中还有另一个标记,那就是Expires,它告诉浏览器该内容在何时过期,暗示浏览器在该内容过期之前不需要再询问服务器,而直接使用本地缓存即可。
这样的好处显而易见,一旦浏览器丝毫不用请求服务器,那将完全节省了带宽和服务器处理等开销,可谓皆大欢喜。
Expires标记更像善于放权的管理者,浏览器一旦看到某个内容附带Expires标记后,便拥有了极大的权力,它无须在过期之前每次都询问服务器,完全可以自作主张,而Last-Modified标记让浏览器感到拘束,它们不得不每次都询问服务器,即便它们认为这样做毫无意义。
Expires的格式类似于Last-Modified,它指示了内容过期的绝对时间,比如:
Expires: Sun, 10 Feb 2002 16:00:00 GMT对于静态内容,Web服务器在默认情况下不会开启Expires标记的支持,我们需要进行一定的配置。
通过Expires指定的过期时间,是来自于Web服务器的系统时间, 如果用户本地的时间和服务器时间不一致的话,那一定会影响到本地缓存的有效期检查 。
很容易想象,比如服务器端为某个内容设置的过期时间为1个小时,可是假如浏览器的时间比服务器晚了2个小时,那么这个内容将被浏览器认为立即过期。当然,一般我们使用的操作系统(如Winddows)都会使用基于GMT的标准时间,然后本地时间通过时区来进行偏移计算,而HTTP中使用的也是GMT时间,所以一般不会因为时区而导致本地和服务器相差数个小时。但是,没有人能保证用户本地的时间都是与你的服务器一致的,甚至有时候你的服务器时间也许就是错误的,这些都会影响到浏览器缓存的正常工作,让我们的一片苦心付诸东流。
幸运的是,HTTP/1.1中还有一个标记用于弥补Expires的不足,那就是Cache-Control,它的格式如下所示:
Cache-Control: max-age=<second>
max-age指定了缓存过期的相对时间,单位是秒,并且这个时间是相对于浏览器本地时间而言。
对于静态内容,事实上Web服务器在开启Expires的同时,也会自动添加响应的Cache-Control标记,用于兼容HTTP/1.1,我们来请求一个GIF图片,它位于Apache服务器上,我们为Apache设置了GIF的过期策略,如下所示:
ExpiresActive on
ExpiresByType image/gif "access plus 1 hours"
接下来我们在浏览器上请求这个图片的URL,然后跟踪HTTP响应头,如下所示:
HTTP/1.1 200 OK Date: Tue, 24 Mar 2009 04:51:03 GMT Server: Apache/2.2.11 (Unix) PHP/5.2.1 DAV/2 SVN/1.4.3 Last-Modified: Wed, 27 Feb 2008 18:11:26 GMT ETag: "7815c-303-44727bbbf0f80" Accept-Ranges: bytes Content-Length: 771 Cache-Control: max-age=3600 Expires: Tue, 24 Mar 2009 05:51:03 GMT Keep-Alive: timeout=30, max=97 Connection: Keep-Alive Content-Type: image/gif
可以从以上头信息中计算出,Expires时间刚好是Date时间之后的1个小时,同时,max-age的值为3600秒。
值得一提的是,目前的主流浏览器都将HTTP/1.1作为首选,所以 当HTTP响应头中同时含有Expires和Cache-Control时,浏览器会优先考虑Cache-Control。对于没有Cache-Control的情况,浏览器则会服从Expires的指示 。
对于主流浏览器,一般有以下三种请求页面的方法:
这种方式可以叫强制刷新,它使得网页以及其中的所有组件都直接向Web服务器发送请求,并且不使用缓存协商,这样的目的是获取所有内容的最新版本
也可以按住Ctrl键然后单击浏览器的刷新按钮获得同样的结果。在实际使用中,很少有用户会这样操作。
这种方式便是一般的刷新,我们经常使用,它等同于单击浏览器的刷新按钮。它允许浏览器在请求中附加必要的缓存协商,但不允许浏览器直接使用本地缓存,也就是说,它能够让Last-Modified发挥效果,但是对Expires无效。
我想这种方式大家使用最多,还有一种操作也等同于这种方式,那就是在浏览器地址栏中输入URL后按回车键,Firefox中常用这种方式,因为它没有“转到”按钮。这几种方式允许浏览器以最少的请求来获取网页的数据,浏览器会对所有没有过期的内容直接使用本地缓存,所以,Expires标记只对这种方式有效,以后你不用在按了F5键后对浏览器没有使用本地缓存感到奇怪。