HTTP协议非常强大。跟SQL查询缓存和应用级别的内部缓存一样,HTTP也提供了非常强大的缓存机制,如果忽略这些,确实是一大损失。
我们先来定义几个概念:
源服务器:提供HTTP服务的主机
缓存服务器:HTTP缓存服务器位于客户端和源服务器之间。他可能是CDN服务器,也可能是客户端的缓存缓存代理服务器。如果使用这种方法,那么源服务器就不是直接和客户端通信了,需要中间的缓存代理服务器进行一次转发。
客户端:向代理服务器或者源服务器发起HTTP请求的一方。
实体:被访问的文档。实体包含实体头部,实体内容是HTTP请求的返回值。图片,js,css,HTML文档都是实体。
头部(headers)
所有实体的提供还有大多数情况下实体内容都是通过请求的头部信息来控制的。HTTP请求有两种不同类型的头部:请求头部和返回头部。
所有的头部信息都是key-value格式的。我们后面会看到。
请求头部是客户端发送的,其中还包括一些客户端的信息,请求会生成、缓存已知实体的信息,以及客户端自身的信息。
返回头部是HTTP服务器发送的,包括返回实体的内容,返回请求的信息,以及实体的缓存信息。
还有一些头部是在以上描述之外的,比如cookie还有自定义头部(通常都是x开头的)。
另外再提一下HTTP的状态码,状态码是一个三位的数字,代表了请求返回的信息。我们主要关注下面三个状态码:
200 OK:这是最常见的返回码,表示一切正常。
304 Not Modified:当源服务器告知实体没有修改的时候返回的状态码。有时get会使用特别的请求,我们后面再讨论。
504 Gateway Timeout:这个通常是代理服务器无法访问源服务器的时候返回的状态码。这表示缓存服务器必须验证实体,但是连接失败了。
Cache-Control
缓存控制是提供给源服务器的可以用来控制返回给客户端或者缓存服务器的一些操作。我们来看看最常用的几种:
max-age
max-age是以秒为单位来表示实体多久被刷新一次,格式写为:max-age=N,N就是秒数。这个值是告知给客户端的,缓存服务器在s-maxage值为空的时候也会使用这个参数的值。
s-maxage
s-maxage的格式跟max-age一样,只不过他是给缓存服务器使用的。
must-revalidate
这个参数用来告知客户端和缓存服务器,在GET请求的时候必须与源服务器验证实体是否为最新版本。
当然还有很多的HTTP规范的细节,这就留给读者自己研究吧。
当一个实体已经不是最新版本,又有must-revalidate参数,这时缓存服务器向源服务器请求这个实体,如果源服务器没有返回,那么就要抛 出一个504 Gateway Timeout。如果在源请求头中没有包含这个参数,这时候缓存服务器要么抛出一个504要么提供一个旧版本的实体。
如果我们想让一个实体在客户端缓存一个小时,在缓存服务器缓存二十分钟,那么我们应该这样写:
Cache-Control:max-age=3600,s-maxage=1200
这样子,客户端的浏览器会在一小时之内都使用本地的缓存,而缓存服务器会在20分钟之后再次请求。
如果我们只有Cache-Control这个缓存信息的话,那么有条件的GET就不会发送,只有普通的GET才会发送。当出现了Last-Modified或者ETag的头部信息的时候才会发送有条件的GET请求。
Last-Modified
这个参数提供了实体最近一次被修改的时间。这个名字起得不错,当实体被修改了之后,这个参数也就会被修改。我们来看一个例子:
$ curl -I
http://i.cdn.turner.com/cnn/.element/img/2.0/sect/connect/avatar.gif<br>HTTP/1.1 200 OK
Date: Mon, 16 Aug 2010 13:52:46 GMT
Expires: Mon, 16 Aug 2010 14:12:56 GMT
* Last-Modified: Fri, 23 Oct 2009 19:22:47 GMT
* Cache-Control: max-age=3600
Content-Type: image/gif
Accept-Ranges: bytes
Server: Apache
Content-Length: 365
我们可以看到Cache-Control给出了一个max-age值,Last-Modified中也有了一个日期。这种情况下,客户端会在一个小 时之内都是用本地的缓存图片。时间一到,这个实体就会被认为是过期的。但是因为有Last-Modified参数,我们就可以发送一个有条件的请求:
$ curl --header "If-Modified-Since: Fri, 23 Oct 2009 19:22:47 GMT"
-i
http://i.cdn.turner.com/cnn/.element/img/2.0/sect/connect/avatar.gif
HTTP/1.1 304 Not Modified
Date: Mon, 16 Aug 2010 13:59:27 GMT
Expires: Mon, 16 Aug 2010 14:47:41 GMT
这时候服务器返回了304 Not Modifed,告诉我们可以使用硬盘上的缓存。在这个请求中没有返回实体内容,因为304不会返回实体内容,只是返回了头部信息和HTTP状态码。
如果我们把 if-modified-since改为一个更新的时间,就会返回200:
$ curl --header "If-Modified-Since: Fri, 23 Oct 2009 10:22:47 GMT"
-I
http://i.cdn.turner.com/cnn/.element/img/2.0/sect/connect/avatar.gif
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Content-Length: 365
Content-Type: image/gif
Expires: Mon, 16 Aug 2010 14:47:41 GMT
Last-Modified: Fri, 23 Oct 2009 19:22:47 GMT
Accept-Ranges: bytes
Server: Apache
Date: Mon, 16 Aug 2010 14:02:48 GMT
Connection: keep-alive
Last-Modified: Fri, 23 Oct 2009 19:22:47 GMT
Cache-Control: max-age=3600
Connection: keep-alive
任何时候缓存都需要这个Last-Modified参数,这个是用来验证的最简单的办法了。
expires
关于这个参数,有很多的误解。我们先来看个例子:
HTTP/1.1 200 OK
* Date: Mon, 16 Aug 2010 13:52:46 GMT
* Expires: Mon, 16 Aug 2010 14:12:56 GMT
Last-Modified: Fri, 23 Oct 2009 19:22:47 GMT
* Cache-Control: max-age=3600
Content-Type: image/gif
Accept-Ranges: bytes
Server: Apache
Content-Length: 365
我们可以看到一些很有意思的事情,expires表示的是实体20分钟以后过期,但是Cache-Control中确是一个小时以后。到底哪个才算数呢?
如果你猜的是Expires,那么那就错了,Section 13.2.4 RFC 2616中说:
The max-age directive takes priority over Expires, so if max-age is present in a response, the calculation is simply:<br>freshness_lifetime = max_age_value<br>Otherwise, if Expires is present in the response, the calculation is:<br>freshness_lifetime = expires_value – date_value
在这个例子中我们可以看到,刷新的周期是3600秒,Expires就完全被忽略了。如果没有Cache-Control信息,那么刷新周期就是Expires的时间了。所有的时间都是以服务器的时间为准,所以本地的时间和时区的影响是没有的。
ETag
ETag是根据实体内容生成的一段hash字符串,保证他有用的原理基于如果实体内容发生改变,那么每次生成的ETag都绝对不同。可以想象他们为MD5或者SHA1之后的结果。
$ curl -I
http://symkat.com/s/jquery.beautyOfCode.js
HTTP/1.1 200 OK
Content-Type: application/javascript
Accept-Ranges: bytes
* ETag: "1028101788"
Last-Modified: Fri, 30 Jul 2010 19:48:57 GMT
Content-Length: 8275
Date: Mon, 16 Aug 2010 14:26:35 GMT
Server: lighttpd/1.4.19
我们可以看到这个实体的ETag值是1028101788,我们创建一个链接:
$ curl -I --header 'If-None-Match: "1028101788"'
http://symkat.com/s/jquery.beautyOfCode.js
* HTTP/1.1 304 Not Modified
Content-Type: application/javascript
Accept-Ranges: bytes
ETag: "1028101788"
Last-Modified: Fri, 30 Jul 2010 19:48:57 GMT
Date: Mon, 16 Aug 2010 14:31:30 GMT
Server: lighttpd/1.4.19
返回了304,这也节省了再次传文件的带宽。
一个比较好的系统应该完全使用上面的这些策略。综合Last-Modified和ETag用来验证实体的版本,避免带宽的浪费是最佳的缓存策略。