http协议中与cache相关的header主要有这么几个:
1.Pragma:只能取固定值no-cache,用于http/1.0里面,禁止浏览器缓存
2. Expires:用于指定当前缓存的文档在什么时候被认为过期,GMT格式,超过了这个日期浏览器就不使用本地缓存,而是像服务器发出新的请求。
3.Cache-Control
no-cache:禁止浏览器和代理服务器缓存,可以单独指定不缓存某些信息头,例如:no-cache=Set-Cookie不缓存cookie信息
no-store:专指不能缓存到硬盘
max-age:xx秒以后文档过时,可以代替Expires,如果同时出现,max-age优先
s-max-age:代理服务器中缓存的文档的有效期
must-revalidate:对于客户机的请求,代理服务器必须向服务器验证缓存的文档是否过时,总是获取最新的文档给客户机
4.if-modified-since
当客户机访问一个已经缓存的页面时,只有该页面自某个指定的时间以来发生过更改,才需要重新获取该文档。
if-modified-since用于指定这个时间,GMT格式。
5.last-modified:服务端设置的文档的最后的更新日期。
6.if-match:定义判断网页过期的条件
服务器给客户机传送网页的时候,可以传递代表实体内容特征的头字段(ETag),这种头字段被叫做实体标签。当客户机再次向服务端发请求的时候,
会使用if-match携带实体标签信息。
7.ETag:用于服务器向客户端传送的代表实体内容特征的标记信息。
下面我们就通过具体的例子来看一下,这几个header是如何控制浏览器的缓存行文的:
1. 缓存协商机制Last-Modified/If-Modified-Since
浏览器会缓存浏览过的页面,如果服务器告诉了浏览器Last-Modified,浏览器在下次访问缓存的页面的时候,就可以使用If-Modified-Since询问服务器可否使用本地缓存,如果可以,服务器会发送304状态码。
但是,如果服务器没有输出Last-Modified头字段,浏览器就不会发送If-Modified-Since头。
我们先写一个CacheServlet:
@WebServlet(urlPatterns={"/cacheservlet.html"})
public class CacheServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=gb2312");
PrintWriter out = response.getWriter();
out.print("hello,cache!");
out.close();
}
}
清一下浏览器的缓存,访问 http://localhost:8080/cache/cacheservlet.html,可以看到输出:hello,cache!
通过httpwatch,可以看到浏览器发出的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
服务端返回的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:30:19 GMT
hello,cache!
可以看到服务器并没有输出last-modified头字段,再次刷新浏览器,可以看到同样的结果,浏览器也并没有发送If-Modified-Since。打开浏览器的缓存:
我们可以看到,已经缓存了cacheservlet.html这个页面,但是,“上次修改时间”是“无”,“截止日期”也是“无”。因此,无论怎么刷新,都是返回200.
下面我们修改一下我们的servlet,添加
last-modified头:
@WebServlet(urlPatterns={"/cacheservlet.html"})
public class CacheServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=gb2312");
response.setDateHeader("Last-Modified", new Date().getTime());
PrintWriter out = response.getWriter();
out.print("hello,cache!");
out.close();
}
}
然偶我们清一下缓存,重启tomcat,再次访问这个页面,
浏览器的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
服务端的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 17 Apr 2012 07:33:09 GMT
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:33:09 GMT
hello,cache!
可以看出来,这次输出了Last-Modified头,然后看一下缓存:
这次就有上次修改时间了,因为服务端输出的last-modified是按照GMT格式的,浏览器显示的东八区的,二者正好相差了8个小时。
这次我们再刷新浏览器:
客户端的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
If-Modified-Since: Tue, 17 Apr 2012 07:33:09 GMT
Host: localhost:8080
Connection: Keep-Alive
服务端的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 17 Apr 2012 07:34:29 GMT
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:34:29 GMT
hello,cache!
显然,这次客户端的请求就携带了If-Modified-Since头,但是服务器的默认实现中:
@Override
protected long getLastModified(HttpServletRequest req) {
return super.getLastModified(req);
}
这个方法默认返回-1,因此,服务器认为客户端的缓存过期,因此还是返回了200.
具体参考HttpServlet的源码:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
}
…….
}
现在,我们让浏览器缓存页面60秒,修改CacheServlet:
@WebServlet(urlPatterns={"/cacheservlet.html"})
public class CacheServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long clientLastModified = request.getDateHeader("If-Modified-Since");
if(clientLastModified + 60 * 1000 > new Date().getTime()){//如果在60秒以内,直接返回304,,让浏览器使用本地缓存
response.setStatus(304);
return;
}
response.setContentType("text/html;charset=gb2312");
response.setDateHeader("Last-Modified", new Date().getTime());
PrintWriter out = response.getWriter();
out.print("hello,cache!");
out.close();
}
}
清一下缓存,重启tomcat,再次访问这个页面,
客户端的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
If-Modified-Since: Tue, 17 Apr 2012 07:34:29 GMT
Host: localhost:8080
Connection: Keep-Alive
服务端的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 17 Apr 2012 07:36:08 GMT
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:36:08 GMT
hello,cache!
刷新页面,客户端的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
If-Modified-Since: Tue, 17 Apr 2012 07:36:08 GMT
Host: localhost:8080
Connection: Keep-Alive
服务端的响应:
HTTP/1.1 304 Not Modified
Server: Apache-Coyote/1.1
Date: Tue, 17 Apr 2012 07:36:08 GMT
可以看到,服务端返回304,浏览器就可以直接使用本地缓存,一分钟以后,再次刷新:
客户端的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
If-Modified-Since: Tue, 17 Apr 2012 07:36:08 GMT
Host: localhost:8080
Connection: Keep-Alive
服务端的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 17 Apr 2012 07:37:28 GMT
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:37:28 GMT
hello,cache!
虽然客户端发送了If-Modified-Since头,但是,因为已经超过了60秒,因此返回200.
2. 彻底的消灭请求-Expires
虽然浏览器的缓存没有过期的情况下,服务器可以返回304,能节省带宽提高服务器的处理效率,但是,毕竟浏览器还是向服务器发出了请求,能不能在不过期的时候,浏览器可以不询问服务器,直接使用本地的缓存?答案是肯定的。Expires头用来告诉浏览器,缓存的页面何时过期,在过期之前,浏览器可以直接使用本地的缓存,而不需要询问服务器。
修改我们的CacheServlet:
@WebServlet(urlPatterns={"/cacheservlet.html"})
public class CacheServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long clientLastModified = request.getDateHeader("If-Modified-Since");
if(clientLastModified + 60 * 1000 > new Date().getTime()){
response.setStatus(304);
return;
}
response.setContentType("text/html;charset=gb2312");
response.setDateHeader("Last-Modified", new Date().getTime());
response.setDateHeader("Expires", new Date().getTime() + 60 * 1000);//60秒之后,浏览器的缓存过期
PrintWriter out = response.getWriter();
out.print("hello,cache!");
out.close();
}
}
清一下缓存,重启tomcat,再次访问这个页面,
客户端的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
服务端的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 17 Apr 2012 07:44:39 GMT
Expires: Tue, 17 Apr 2012 07:45:39 GMT
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:44:39 GMT
hello,cache!
浏览器的缓存:
可以看出来,这次的截止日期已经不再是无。
F5刷新浏览器,还是会向服务器发请求,而不是直接使用本地的缓存,
客户端请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
If-Modified-Since: Tue, 17 Apr 2012 07:44:39 GMT
Host: localhost:8080
Connection: Keep-Alive
服务端响应:
HTTP/1.1 304 Not Modified
Server: Apache-Coyote/1.1
Date: Tue, 17 Apr 2012 07:44:40 GMT
但是,如果是直接在浏览器按回车,会显示:
也就是说是直接取的本地缓存,而并没有与服务器交互。
到此为止,似乎很完美,但是,有这么两个问题:(1)浏览器兼容问题,有的浏览器不支持Expires头(2)服务器的时间很可能和客户端的时间不一致,如果客户端的时间落后于服务器的时间一个小时的话,如果服务器的expires设定为1个小时之内,就都没有用,这确实是个问题!很幸运的是,http还提供了另一个头叫做:Cache-Control,他可以设定缓存的时间,用秒作单位,这个时间是相对于客户端的浏览器的。
我们再次修改我们的CacheServlet:
@WebServlet(urlPatterns={"/cacheservlet.html"})
public class CacheServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long clientLastModified = request.getDateHeader("If-Modified-Since");
if(clientLastModified + 60 * 1000 > new Date().getTime()){
response.setStatus(304);
return;
}
response.setContentType("text/html;charset=gb2312");
response.setDateHeader("Last-Modified", new Date().getTime());
response.setDateHeader("Expires", new Date().getTime() + 60 * 1000);
response.setHeader("Cache-Control", "max-age="+60);//告诉浏览器缓存60秒
PrintWriter out = response.getWriter();
out.print("hello,cache!");
out.close();
}
}
清一下缓存,重启tomcat,再次访问这个页面,
客户端的请求:
GET /cache/cacheservlet.html HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.2)
Accept-Encoding: gzip, deflate
If-Modified-Since: Tue, 17 Apr 2012 07:44:39 GMT
Host: localhost:8080
Connection: Keep-Alive服务端的响应:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Tue, 17 Apr 2012 07:50:42 GMT
Expires: Tue, 17 Apr 2012 07:51:42 GMT
Cache-Control: max-age=60
Content-Type: text/html;charset=gb2312
Content-Length: 12
Date: Tue, 17 Apr 2012 07:50:42 GMT
hello,cache!
已经输出了Cache-Control头.
几种不同的刷新之间的区别:
(1) ctrl+F5:强制刷新,不管浏览器有没有缓存页面,都会向服务器发送请求,而且不会发送If-modified-since头。
(2) F5:会使用If-modified-since进行缓存协商,但是expires无效
(3) 在地址栏按回车和点击页面超链接,会进行协商,而且expires头有效。
服务端页面缓存:
前面说的都是浏览器端的缓存,通过使用诸如velocity或者freemarker之类模板技术,很容易就能够实现服务端的页面缓存,大体的思路应该是这样:
当controller收到一个请求的时候:
1.查找memcache里面是否缓存过与这个url匹配的ResponseContent对象
2.如果没有找到,就调用action的方法,然后把生成的ResponseContent存放到memcache里面
3.如果能找到,说明服务端缓存可用
3.1如果客户端缓存可用(根据last-modified判断),直接返回304,使用客户端缓存
3.2如果客户端缓存失效,使用服务端缓存的ResponseContent,输出到客户端。
但是,请注意,使用了缓存以后,是无法保证页面的实时性的。
【注】
(1)在做实验的时候,请设置浏览器的检查网页更新为自动。
(2)IE浏览器比较笨,它不会缓存没有后缀名或者后缀名是.do的文件(我的是IE8),因此,在做实验的时候,需要把Servlet的映射路径变为html的样子,否则,很有可能实验失败。
(3)其他与缓存相关的http头还有:
Pragma,ETag/If-None-Match
(4) 禁止浏览器缓存
Pragma:no-cache // HTTP 1.0的不缓存响应头
Expires:0
Cache-Control :no-cache
Cache-Control :no-store //该设置是防止Firefox缓存
参考:构建高性能WEB站点 郭欣,深入体验javaweb开发内幕-核心基础 张孝祥