缓存在应用开发中是一个很重要的环节,利用好缓存能够有效地提升用户的体验,加快用户对资源的获取效率。在HTTP中,HTTP定义了一套缓存机制来指导客户端对于资源的缓存模式。
强缓存和协商缓存
在HTTP中,缓存的方式分为两种:强缓存和协商缓存。他们的区别在于强缓存不会向服务器发起请求,只要缓存资源没有过期,就会直接利用存储在客户端内存或硬盘中的缓存资源;而协商缓存则是无论缓存资源是否过期,客户端都要向服务器发起一个请求,用以向服务器确认缓存资源是否可用。
HTTP/1.0的缓存模式:Pragma、Expires
如今各大主流浏览器虽然都已经使用了HTTP/1.1,但是还是不能排除有些用户使用着旧的用户代理,上面运行的HTTP版本仍是1.0的版本,所以了解HTTP/1.0遗留下的缓存字段还是有必要的。
Pragma
Pragma是HTTP/1.0遗留下的一个旧的字段,不过奇怪的是在如今的HTTP/1.1的版本中,它仍然能够起作用,而且优先级还比HTTP/1.1定义的Cache-Control字段的优先级要高;它的字段值唯一,只有一个可用的值no-cache;当响应字段中使用了Pragma字段时,就会指示客户端,每次对该资源的访问请求都该向服务器发起一个请求,而不应直接使用客户端缓存。
Expires
Pragma是用于禁止缓存的字段,相对的就存在开启缓存的字段,也就是Expires字段。Expires字段的有效值是一个GMT格式的时间字符串,用以表示缓存资源在这个日期之前都是有效的。不过,这个字段的局限性在于,这个时间字符串表示的是服务器的时间,如果客户端的时间和服务器的时间不一致的话,那么就可能导致在客户端上这个缓存的有效期并不准确。Expires在现代浏览器中主要都是用于兼容使用了旧版本HTTP/1.0的浏览器,在使用了HTTP/1.1的浏览器中,它都被接下来要介绍的Cache-Control替代了。
HTTP/1.1:Cache-Control
由于Expires存在的局限性,在HTTP/1.1里,它就被新出现的Cache-Control字段替代了。Cache-Control不像Expires用一个绝对的日期作为缓存过期的凭证,而是使用缓存时间策略,使用秒数来指示缓存是否过期,这样就能解决客户端、服务器时间不一致的问题。Cache-Control是一个通用首部,其可用的字段值如下:
请求报文:
no-cache:表示客户端通知代理不打算使用缓存资源而是要向服务器发起一个请求
no-store:表示任何内容都不会被缓存在客户端或Internet临时文件中
max-age=delta-seconds:表示客户端希望使用在存在时间不大于该值的缓存资源
max-stale(=delta-seconds):表示客户端通知代理,其愿意接受一个过期的资源(其中括号中是一个可选值,表示客户端指示的可接受的最大过期时间)
min-fresh=delta-seconds:表示客户端希望接受一个在该时间内更新过的资源
no-transform:表示客户端不希望接受到的资源被转码(比如压缩)
only-if-cached:表示客户端希望能只接受缓存的资源(如果有),而不用向服务器重新请求
其中delta-seconds的单位为秒。
响应报文:
no-cache:通知客户端对该资源的请求都应该向服务器发一个请求,而不应直接使用缓存
no-store:任何内容都不会被缓存在客户端或Internet临时文件中(包括下文会提到的ETag和Last-Modified)
max-age=delta-seconds:表示该资源的缓存有效时间
s-maxage:同max-age,不过是用于共享资源的(也就是代理)
public:表示该缓存资源可以共享,也就是中间代理也可以缓存该资源
private:该资源只能够缓存在私人客户端上
must-revalidate:告知客户端对该资源的验证请求必须向源服务器发起验证,而不是代理
proxy-revalidate:与must-revalidate类似,但只用于代理服务器
no-transform:指示客户端在缓存时不应对资源的实体数据做任何改变
在使用Cache-Control时,我们可以按需求合理的组合上述字段值,比如:
Cache-Control:max-age=60,must-revalidate
这段字段表示缓存的有效时间是60秒,在这段时间内客户端可以不用向服务器发起请求,直接使用缓存资源;而过了60秒以后,客户端再请求资源一定要向源服务器发起请求进行缓存验证。
再如:
Cache-Control:no-cache,no-store,must-revalidate
这段字段用于禁止客户端对资源进行缓存,并且一定要向源服务器发起请求。其中no-cache,no-store同时使用可以用来禁止不同浏览器使用后退按钮的时候使用缓存资源的行为。(IE中使用no-cache即可禁止这一行为,但是在FireFox中需要使用no-store才可禁止)
协商缓存:Last-Modified,Etag
前文所述的是关于浏览器强缓存的相关内容,但是强缓存总有过期的时候,过期以后客户端就会向服务器发起请求重新请求资源,如果这段时间内该资源在服务器上仍然没有改变,那么我们就会期望客户端继续使用缓存的资源。这就是协商缓存的内容,当前的HTTP中是使用Last-Modified和ETag这两种方式来实现的。
Last-Modified和If-Modified-Since
Last-Modified字段是一个响应首部字段,用于指示该资源在服务器上的最后一次修改时间;与之相对的是If-Modified-Since,一个请求首部字段,用于通知服务器如果该资源在该字段表示的时间之后有修改的话,就应返回新的资源;否则,应该允许客户端使用缓存。这两个字段的值都是一个GMT格式的时间字符串。
服务器接受到含有If-Modified-Since的报文以后,会比较该值与资源的Last-Modified,如果Last-Modified小于或等于If-Modified-Since,服务器就会返回304状态码(Not Modified),并不会附加实体内容,指示客户端可以继续使用缓存;否则,就会返回200状态码,将新的资源作为实体一并返回。
一般来说,客户端在接收到带有Last-Modified的响应报文后,会将该时间与资源一同存储在缓存当中,在下次向服务器发起请求时,就会将该时间值取出并设置为If-Modified-Since的值一并发送给服务器。
这种模式的缺点在于,Last-Modified的单位是秒,所以如果一份资源在一秒内经过了多次改变的话,Last-Modified是无法检测到这种频率的变化的;而且,如果有些资源是周期性改变的,但是实际上他们的内容没有变,这种情况下Last-Modified还是会变化,那么客户端就会请求一份与缓存内容没有差异的资源。
ETag与If-None-Matched
为了弥补上述的Last-Modified存在的缺陷,HTTP/1.1还存在另一种实现协商缓存的机制:ETag。ETag是服务器上一份资源的唯一标识,生成这个ETag的方式不是唯一的,可以是用MD5算法生成的,也可以是一个根据内容生成的hash值。由于ETag是根据文件内容生成的,所以只要文件的内容不变,ETag就不会变;而如果一份文件在1秒内经历了多次改变,那么ETag也会检测到这一变化生成新的ETag值。
服务器在将带有ETag的响应报文发送给客户端后,客户端会将该Etag值和资源一同存储在缓存当中,在下次向服务器发起请求的时候,就会将该ETag值取出并设置为If-None-Matched的值发送给服务器,服务器对比该字段和该资源的ETag值,如果相等,则返回304;否则,返回200
ETag和Last-Modified的取舍
ETag虽然能弥补Last-Modified的许多缺陷,不过生成ETag是要消耗服务器资源的,所以对于改动不那么频繁的文件,我们未必一定要使用ETag,只使用Last-Modified也未尝不可。
Vary字段在HTTP缓存中的使用
在响应报文中使用Vary头部,可以指示客户端对于后续的请求,是使用缓存中的资源还是向源服务器发起一个新的请求。当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。比如: