添加Expires和Cache-control头部
Expires和Cache-control实际上是HTTP中的缓存控制头部,它主要影响客户端的请求行为和服务器端的响应。
本文的许多内容来自《HTTP权威指南》,如有任何问题,欢迎指出。
一.缓存的基本概念
这里的缓存,单指web的缓存。当web请求抵达缓存时,如果本地有缓存的副本且缓存未过期,那么就可以从本地读取数据或文档,这样便可以:
1. 减少冗余的数据传输,一定程度上减少服务器的流量和压力。
2. 缓解了网络瓶颈的问题,不需要更多的带宽就能更快的加载页面。
3. 降低了对原始服务器的要求,服务器可以更快的响应,避免过载和峰值访问。
4. 降低了因距离带来的时延。
缓存可以是单个用户专有的,也可以是数千名用户共享的。其中专有缓存称为私有缓存,而共享的缓存则称为公有缓存。专有缓存比较常见,例如常用的浏览器便有内置的私有缓存-大多数浏览器都提供了缓存的功能,通过将常用的文档缓存在个人PC电脑的磁盘和内存中,减少二次请求的加载时间。共有缓存则是通过缓存代理服务器实现的,它可以接受来自多个用户的访问,为更多的用户提供缓存功能,因而可以更好的减少冗余流量。
对于一条包含了缓存控制的HTTP请求而言,缓存的处理过程包含了7个步骤。分别是:
1. 接收。缓存从网络中读取抵达的请求报文2. 解析。缓存对报文进行解析,提取出URL和HTTP请求首部。
3. 查询。查看本地的副本是否可用,如果没有,就获取一份副本,并将其保存在本地。
4. 过期检查。查看已经缓存的文档副本是否够新鲜,如果不是,就询问服务器是否有更新。
5. 构建响应。利用新的首部和已经缓存的主体来构建一条响应报文
6. 发送。缓存通过网络将响应发回给客户端。
7. 日志。可选地创建相应的日志。
二、缓存相关首部
与缓存相关的首部主要包括Cache-Control首部,Expires首部,If-Modified-Since和If-None-Match等,其中后两个首部主要用于客户端的再验证。
1. Cache-Control首部和Expires首部
Cache-Control是与缓存有关的最重要的头部之一。需要注意的是:它既可以在请求头中出现,也可以在响应头中出现。因此,对于该头部,首先需要知道的是它出现在请求头中还是出现在响应头中,显然它们是不同的。以csdn的bbs首页为例,浏览器访问bbs.csdn.net,抓取到的请求头为:
其中的Cache-Control:max-age=0 是告诉缓存系统,本次的请求没有失效日期。而相应的服务器的响应头则为:
其中的max-age=0表明缓存不会过期,private表明这是一个私有缓存,must-revalidate则表明客户端在使用缓存之前,必须要做重新验证。仔细观察请求和响应头我们还发现:请求头与缓存与关的还有另外一个头部:if-none-match,响应头中则包含了Etag头部。关于这两个头部的详细解释,会在之后给出,在此之前,先简单介绍下:缓存过期。通过特殊的HTTP Cache-Control和Expires首部,HTTP让原始服务器包含每个文档的过期时间,如同食物的保质期一样。在缓存的文档过期之前,缓存系统可以直接使用缓存构建响应而不必每次都请求服务器(除非请求头部中包含了其他阻止缓存的首部)。Cache-Control: max-age=xxx用于指定文档的生存期(秒为单位),Expires则是指定文档的过期时间,生存期与过期时间的区别在于:生存期用于指定从生成文档开始,文档的最大使用时间,而过期时间则是指定在什么时间文档会过期。说到这里,顺便提一句,由于Expires指定的是绝对的过期日期而不是秒数,由于很多服务器的时钟并不同步,或者不正确的,因而并不推荐使用Expires首部。如果响应中同时出现max-age和expires首部,则根据以往的经验,expires首部将会被覆盖。
Cache-Control的值按照不同的作用,可以分为以下几类:
a. 缓存的类型:
public(共有缓存,可被缓存代理服务器缓存)private(私有缓存,不能被共有缓存代理服务器缓存,可被用户的代理缓存如浏览器)。
b. 缓存的行为:
no-Store: 表明不希望缓存系统保留文档的任何副本,缓存通常会向客户端转发一条no-store的响应,然后删除对象。no-Cache: 标识为no-cache响应实际上可以缓存在本地缓存中的,只是在于原始的服务器再验证之前,不能提供给客户端使用。
must-revalidate: 该首部告诉缓存系统,在没有与原始服务器进行再验证的情况下,不能提供这个对象的陈旧副本。如果缓存在进行must-revalidate再验证的时候,原始服务器不可用,则缓存必须返回一条504 Gateway timeout的错误。
max-age: 前面已经讲过,该首部用于指定文档生成之后的最大有效时间。共有缓存的响应还可能包括一个s-maxage首部,该首部仅仅用于共享缓存。
c. 客户端行为:
Min-fresh:客户端接收响应时间小于当前时间加上指定时间的响应。Max-stale:客户端可以接收超出超时期间的响应消息。
由于这些首部很少用到,这里不再赘述。
2. If-Modified-Since和 If-none-Match客户端再验证
HTTP的条件请求方法可以高效地实现再验证,通过向原始服务器发送一个条件GET请求,请求服务器只有在文档与缓存中现有的副本不同时,才提供对象主体。
HTTP定义了5个条件请求首部,对缓存再验证来说最有用的2个首部是If-Modified-since和If-None-Match。
a. If-Modified-Since: 最常用的缓存再验证首部之一,如果在指定日期之后,文档修改过了,就执行请求,携带新首部的新文档将会被返回给缓存,同时,新的首部中包含了新的过期时间。该首部通常与last-Modified首部配合使用。以csdn 的blog为例,blog请求中静态资源如css的请求中,第一次请求的请求头为:
并不包含If-Modified-Since的首部,服务器端的响应会包含文档的最后修改时间,(该时间会被客户端作为之后请求的if-Modified-Since的时间依据)。如下图所示:
在第二次之后的请求中,会包含If_modified-Since的请求首部:
后续的请求中,由于文档并未修改,因此服务器会回送304 Not Modified响应,缓存系统会使用缓存的副本构建响应,该响应不需包含Content-Type和Content-Encoding,Transfer-Encoding等首部(因为文档不需要重新传输),如下所示:
b. If-none-Match: 实体标签再验证。If-Modified-Since能够很好的解决静态文档的再验证问题,但是很多情况下,仅仅使用文档的最后修改日期进行再验证是不够的,这是由于:
1. 有些文档可能被周期性的重写,但是数据可能是一样的,这样虽然文档的内容没有发生变化,但是文档的修改时间却发生了变化。
2. 有些文档虽然修改了,但是修改并不重要,因此不需要更新所有的缓存。
3. 有些服务器无法准确的判断文档的最后修改时间或无法正确的支持If-Modified-Since(比如,有的服务器是使用日期的字符串匹配比较而不是日期比较)
4. 对于文档变化小于1s的实时监控类应用,1s的粒度太大,需要更加精细的粒度控制。
HTTP允许通过实体标签Etag的方式进行条件GET请求,Etag标签可以是文档的版本号、序列号、指纹或者校验信息等,Etag可以有多个。以bbs.csdn.net首页为例,第一次请求文档时,服务器的HTTP响应会包含该文档的Etag信息(该Etag信息可以是文档的MD5摘要等):
在之后的请求中,客户端请求头会带上If-none-Match首部:
如果Etag匹配,服务器会回送304 not Modified响应,相反,服务器会返回新的文档和新的Etag标签。
3.Yslow优化法则的建议
Yslow中对于该法则的优化法则是:
a. 对于静态的内容,为Expires添加一个较远的过期日期。例如,csdn中,静态css文件的过期日期便是当前时间加一个星期(也就是一个星期后过期):
这种方式的弊端在于,如果在文档过期之前修改了文档,由于Expires设置的日期并未到,所以在这之前,浏览器都是使用缓存的副本,这当然会带来很多问题(如无法及时更新客户端的UI).多数的做法是,为你的静态资源添加标识,例如版本号,在文档发生变化时需要更新静态资源的引用,这样能保证客户端使用的静态资源总是最新的缓存副本。
b. 对于动态的内容,设置合适的Cache-Control策略。这里有个比较坑的地方,什么是合适的策略?由于并没有通用的设置,我们并不做过多的解释。不妨思考一下,如何针对特定的服务器和应用选择合适的缓存策略。
参考文献:
1. https://developer.yahoo.com/performance/rules.html
2. http://www.guojl.com/article/40/
3. http://www.cnblogs.com/cocowool/archive/2011/08/22/2149929.html
4. http://www.vktone.com/articles/http_browser_cache.html
5. http://blog.csdn.net/novofly/article/details/7613173