网络特有的延迟以及数据传输的成本,制约互联网快速获取Web资源。为此,HTTP协议引入缓存以空间换时间,使浏览器缓存和重用已获取的资源,解决网络延迟和数据传输成本高的问题,提升访问体验。随着HTTP协议1.0->1.1->2的演进,关于缓存控制的部分有一些变化。但我觉着,在开发Web服务时,首先要关注请求头If-Modified-Since/响应头Last-Modified、请求头If-None-Match/响应头ETag、响应头Cache-Control。因为这三个HTTP头可以满足你的大部分HTTP缓存需求,并且,当今绝大多数浏览器都支持这三个HTTP头。我们所要做的,就是确保每个服务器响应都提供正确的HTTP头指令,以指导浏览器何时缓存响应以及缓存多久。
HTTP缓存在哪儿?
如图1所示HTTP缓存存在于浏览器和Web代理中。当然在服务器内部,也存在着各种缓存,比如本地缓存GuavaCache/Ehcache,分布式缓存Memcached/Redis等,但这不是本文要讨论的HTTP缓存。所谓的HTTP缓存控制,就是一种协议约定,通过设置不同的响应头Cache-Control来控制浏览器和Web代理对缓存的使用策略,通过设置请求头If-Modified-Since/响应头Last-Modified、请求头If-None-Match/响应头ETag,来对缓存的有效性进行验证。
本文对请求头If-Modified-Since/响应头Last-Modified、请求头If-None-Match/响应头ETag、响应头Cache-Control进行实践,分析这三种HTTP缓存头在浏览器和服务器的表现。实践方式,服务端实现使用Spring boot+Java,前端实现使用HTML,访问方式结合浏览器和nc命令行交叉使用,其中nc执行如下命令,在端口8080建立连接,可以设置请求头,模拟浏览器发送输入HTTP请求。
nc 127.0.0.1 8080
注意:由于前端和后端打印时间对象类不一样,将可能打印GMT或者CST时间,GMT比CST晚8小时。
请求头If-Modified-Since/响应头Last-Modified
如图2所示,请求头If-Modified-Since/响应头Last-Modified交互过程如下:
- 如图2红色部分所示,浏览器第一次向服务器请求资源时,服务器返回状态200和资源内容,同时在响应头Last-Modified字段标记该资源内容在服务器最后被修改时间,格式类似:
Last-Modified: Wed, 02 Aug 2017 10:34:50 GMT
,供下一次浏览器在请求头If-Modified-Since使用; - 如图2紫色部分所示,浏览器第二次向服务器请求该资源时,根据HTTP协议规定,浏览器在请求头If-Modified-Since写入上次服务器返回的响应头Last-Modified值,向服务器询问该时间之后资源是否被修改过,格式类似:
If-Modified-Since: Wed, 02 Aug 2017 10:34:50 GMT
; - 如图2绿色部分所示,当服务器资源文件有修改过,重新返回资源和状态码200给浏览器,同时在响应头Last-Modified字段标记该资源内容在服务器最后被修改时间;
- 如图2蓝色部分所示,当服务器资源内容没有被修改过,只给浏览器返回状态码304(Not Modified),不返回请求资源,浏览器根据304使用本地缓存,从而节省带宽,提高网页响应速度。
下面举例实践。
后端Java代码如下,其中CacheControlController#getResourceLastModified()
方法标注资源最后修改时间和用来设置Last-Modified: Wed, 02 Aug 2017 19:30:20 GMT
。
package com.demo.web.http;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@RequestMapping("http")
public class CacheControlController {
@RequestMapping("/cache-control/last-modified")
public String lastModified(ServletWebRequest request) {
if (request.checkNotModified(getResourceLastModified())) {
//it will return 304 with empty body
return null;
}
return "http/cache-control/last-modified";
}
private static long getResourceLastModified() {
ZonedDateTime zdt = ZonedDateTime.of(LocalDateTime.of(2017, 8, 2,
19, 30, 20),
ZoneId.of("GMT"));
return zdt.toInstant().toEpochMilli();
}
}
前端HTML代码:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP cache control Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>
</head>
<body>
<h3>实践Cache-Control last-modified</h3>
<a href="last-modified">刷新页面</a>
</body>
</html>
如图3绿色方框所示,当请求头If-Modified-Since: Wed, 02 Aug 2017 19:30:15 GMT
早于资源最后修改时间Wed, 02 Aug 2017 19:30:20 GMT
,表示Web资源在上一次返回后有修改过,所以服务器重新返回资源内容,返回状态码200,并在响应头返回资源最后修改时间Last-Modified: Wed, 02 Aug 2017 19:30:20 GMT
。
如图4红色和绿色方框所示,当请求头If-Modified-Since: Wed, 02 Aug 2017 19:30:15 GMT
等于或晚于资源最后修改时间Wed, 02 Aug 2017 19:30:20 GMT
,表示资源在上一次返回后没有修改过,所以服务器只返回状态码304,不返回资源内容,浏览器使用本地缓存资源。
请求头If-None-Match/响应头ETag
ETag全称Entity Tag,用来标识一个Web资源,反映资源内容的变化。在具体的实现中,ETag可以是Web资源的hash值,也可以是一个服务器内部维护的版本号。
如图5所示,请求头If-None-Match/响应头ETag交互过程如下:
- 如图5红色部分所示,浏览器第一次向服务器请求资源时,服务器返回状态码200和资源内容,同时在响应头ETag字段标识该资源内容,格式类似:
ETag: "50b1a1d4f885c61:df4"
,供下一次浏览器在请求头If-None-Match使用; - 如图5紫色部分所示,浏览器第二次向服务器请求该资源时,根据HTTP协议规定,浏览器在请求头If-None-Match写入上次服务器返回的响应头ETag值,与服务器的资源标识对比后判断资源是否被修改过,格式类似:
If-None-Match: "50b1a1d4f885c61:df4"
; - 如图5绿色部分所示,当服务器资源内容有修改过,给浏览器重新返回资源和状态码200,同时在响应头ETag字段标记该资源文件的实体值;
- 如图5蓝色部分所示,当服务器资源内容没有修改过,只给给浏览器返回状态码304(Not Modified),不返回请求资源,浏览器根据304使用本地缓存,节省带宽,提高网页访问速度。
下面举例实践。
后端Java代码如下,CacheControlController#getETag()
方法表明资源的唯一标识token和设置ETag值为:"etag_version"
。
package com.demo.web.http;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
@RequestMapping("http")
public class CacheControlController {
@RequestMapping("/cache-control/etag")
public String etag(ServletWebRequest request) {
if (request.checkNotModified(getETag())) {
//it will return 304 with empty body
return null;
}
return "http/cache-control/etag";
}
private static String getETag() {
return "etag_version";
}
}
前端HTML代码:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP cache control Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>
</head>
<body>
<h3>实践Cache-Control ETag</h3>
<a href="etag">刷新页面</a>
</body>
</html>
如图6,当服务器资源唯一标识token:"etag_version"不等于If-None-Match: "etag_version_new"
,表示资源在上一次返回后有改变,所以服务器重新返回资源内容和状态码200。
如图7,当服务器资源唯一标识token:"etag_version"等于If-None-Match: "etag_version"
,表示资源在上一次返回后无变化,所以服务器不返回资源文件,只返回状态码304,浏览器使用本地缓存资源。
请求头If-Modified-Since/响应头Last-Modified、请求头If-None-Match/响应头ETag总结
Last-Modified和Etags如何帮助提高性能?
服务端首先产生Last-Modified/Etag标记,并在HTTP响应头返回给客户端,服务端稍后使用它们判断页面是否已经被修改。客户端通过请求头If-Modified-Since/If-None-Match将Last-Modified/Etag标记传回给服务器,要求服务器验证其(客户端)缓存。
过程如下:
1. 客户端请求一个页面(A)。
2. 服务器返回页面A,并在A加上响应头Last-Modified/ETag。
3. 客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
4. 客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag作为请求头If-Modified-Since/If-None-Match一起传递给服务器。
5. 服务器检查If-Modified-Since/If-None-Match,判断出该页面自上次客户端请求之后还未被修改,直接返回状态码304和一个空的响应体。
Cache-Control
每个Web资源都可以通过HTTP响应头Cache-Control定义自己的缓存策略,Cache-Control控制谁在什么条件下可以缓存响应以及可以缓存多久。 最快的请求是不必与服务器进行通信:通过响应的本地副本,我们可以避免所有的网络延迟以及数据传输的成本。为此,HTTP规范允许服务器返回一系列不同的Cache-Control指令,控制浏览器或者其他中继代理缓存如何缓存某个响应以及缓存多长时间。
Cache-Control响应头在 HTTP/1.1规范中定义,当前的所有浏览器都支持 Cache-Control,取代了之前用来定义响应缓存策略的头(例如 Expires)。
Cache-Control常用的指令包括如下四个,下面将对这四个指令举例实践。
- max-age
- no-cache
- no-store
- public/private
max-age
max-age指令指定从当前请求开始,允许获取的响应被重用的最长时间(单位为秒)。例如Cache-Control:max-age=30
表示响应可以再缓存和重用 30 秒。注意,在max-age指定的时间之内,浏览器不会向服务器发送任何请求,包括验证缓存是否有效的请求,也就是说,如果在这段时间之内,服务器上的资源发生了变化,那么浏览器将不能得到通知,而使用老版本的资源。所以在设置缓存时间的长度时,需要慎重。
后端Java代码如下,响应头设置:Cache-Control:max-age=30
。
package com.demo.web.http;
import org.springframework.http.CacheControl;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@RequestMapping("http")
public class CacheControlController {
@RequestMapping("/cache-control/max-age")
public String maxAge(HttpServletResponse response) {
System.out.println("服务端访问时间:" + new Date());
String headerValue = CacheControl.maxAge(30, TimeUnit.SECONDS).getHeaderValue();
response.addHeader("Cache-Control", headerValue);
return "http/cache-control/max-age";
}
}
前端HTML代码:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP cache control Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>
</head>
<body>
<h3>实践Cache-Control max-age=10</h3>
<p id="time"></p>
<a href="max-age">刷新页面</a>
<script>
$("#time").text("页面访问时间:" + new Date());
</script>
</body>
</html>
如图8所示,浏览器第一次请求http://127.0.0.1:8080/http/cache-control/max-age
,服务器返回响应头:
Cache-Control:max-age=30
Date:Wed, 02 Aug 2017 13:09:46 GMT
浏览器渲染页面访问时间:Wed Aug 02 2017 21:09:46 GMT+0800(CST)
。
服务端打印日志:服务端访问时间:Wed Aug 02 21:09:46 CST 2017
。
如图9所示,浏览器第二次请求http://127.0.0.1:8080/http/cache-control/max-age
,响应头与图8一样,不一样的是页面访问时间:Wed Aug 02 2017 21:10:03 GMT+0800(CST)
,Status Code多了(from disk cache)
,并且服务端没有打印日志,这些区别表明在30s内第二次请求没有与服务器通信,浏览器直接使用本地缓存的副本渲染页面。
200 OK (from cache) 与 304 Not Modified 区别
200 OK (from cache) 是浏览器没有跟服务器确认,直接使用浏览器本地缓存;而 304 Not Modified 是浏览器和服务器多确认一次缓存有效性,再用浏览器的缓存。200(from cache) 速度最快,因为不需要访问远程服务器,直接使用本地缓存。304 的过程是先请求服务器,,然后服务器告诉浏览器这个资源没变,浏览器再使用本地缓存。
如图10所示,当超过30s(max-age=30)后重新请求http://127.0.0.1:8080/http/cache-control/max-age
,服务器返回响应头:
Cache-Control:max-age=30
Date:Wed, 02 Aug 2017 13:10:31 GMT
浏览器渲染页面访问时间:Wed Aug 02 2017 21:10:32 GMT+0800(CST)
。服务端打印日志:服务端访问时间:Wed Aug 02 21:10:31 CST 2017
。与图8和图9比较,30s后浏览器不再使用本地缓存的副本渲染页面,重新请求服务器资源。
max-age=0
max-age=0是表示响应可以再缓存和重用 0 秒,max-age指令的特殊情况,下面先举例实践,再给出结论。
后端Java代码如下,响应头设置:Cache-Control:max-age=0
。
package com.demo.web.http;
import org.springframework.http.CacheControl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@RequestMapping("http")
public class CacheControlController {
@RequestMapping("/cache-control/max-age0")
public String maxAge0(ServletWebRequest request, HttpServletResponse response) {
System.out.println("服务端访问时间:" + new Date());
if (request.checkNotModified(getETag())) {
//it will return 304 with empty body
return null;
}
String headerValue = CacheControl.maxAge(0, TimeUnit.SECONDS).getHeaderValue();
response.addHeader("Cache-Control", headerValue);
return "http/cache-control/max-age0";
}
private static String getETag() {
return "etag_version";
}
}
前端HTML代码:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP cache control Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>
</head>
<body>
<h3>实践Cache-Control max-age=0</h3>
<p id="time"></p>
<a href="max-age0">刷新页面</a>
<script>
$("#time").text("页面访问时间:" + new Date());
</script>
</body>
</html>
如图11所示,浏览器第一次请求http://127.0.0.1:8080/http/cache-control/max-age0
,服务器返回响应头:
Cache-Control:max-age=0
Date:Thu Aug 03 12:45:51 GMT
浏览器渲染页面访问时间:Thu Aug 03 20:45:52 GMT+0800(CST)
。服务端打印日志:服务端访问时间:Thu Aug 03 20:45:51 CST 2017
。
如图12所示,浏览器第二次请求http://127.0.0.1:8080/http/cache-control/max-age0
,服务器返回Status
Code 304 (Not Modified),响应头:
Cache-Control:max-age=0
Date:Thu Aug 03 12:46:09 GMT
浏览器渲染页面访问时间:Thu Aug 03 12:46:09 GMT+0800(CST)
。服务端打印日志:服务端访问时间:Thu Aug 03 20:46:09 CST 2017
。
表明max-age=0情况下,响应缓存和重用 0 秒,浏览器每次都会请求服务器获取资源。
max-age结论
max-age>0 时,响应缓存和重用指定秒数,在指定秒数内直接使用游览器缓存的副本。
max-age<=0 时,向服务端发送HTTP请求确认,该资源是否有修改。有修改则返回200和最新资源;无修改则返回304,使用游览器缓存的副本。
no-cache
如果服务器在响应头中设置Cache-Control:no-cache
,那么浏览器在使用缓存的资源之前,必须先与服务器确认上次返回的资源是否被更改,如果资源未被更改,直接使用浏览器缓存的副本,避免重新下载。这个验证之前的响应是否被修改,就是通过上面介绍的请求头If-None-match和响应头ETag来实现的。
注意,no-cache这个名字有一点误导。设置no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下资源是否跟服务器还保持一致。如果设置了no-cache,而ETag的实现没有反映出资源的变化,浏览器的缓存数据就不会更新,浏览器与服务器的每次请求连接反而降低性能。所以在服务器资源变化不频繁情况下,设置no-cache对于性能反而有所下降。
后端Java代码如下,响应头设置:Cache-Control:no-cache
。
package com.demo.web.http;
import org.springframework.http.CacheControl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
@RequestMapping("http")
public class CacheControlController {
@RequestMapping("/cache-control/no-cache")
public String noCache(ServletWebRequest request, HttpServletResponse response) {
System.out.println("服务端访问时间:" + new Date());
if (request.checkNotModified(getETag())) {
//it will return 304 with empty body
return null;
}
String headerValue = CacheControl.noCache().getHeaderValue();
response.addHeader("Cache-Control", headerValue);
return "http/cache-control/no-cache";
}
private static String getETag() {
return "etag_version";
}
}
前端HTML代码:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP cache control Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>
</head>
<body>
<h3>实践Cache-Control no-cache</h3>
<p id="time"></p>
<a href="no-cache">刷新页面</a>
<script>
$("#time").text("页面访问时间:" + new Date());
</script>
</body>
</html>
如图13所示,结合请求头If-None-Match/响应头ETag
一节,第一次请求http://127.0.0.1:8080/http/cache-control/no-cache
,服务器返回码200,响应头:
Cache-Control:no-cache
ETag:"etag_version"
服务器日志打印如下:
服务端访问时间:Thu Aug 03 20:06:11 CST 2017
如图14所示,第二次请求http://127.0.0.1:8080/http/cache-control/no-cache
,请求头携带If-None-Match: "etag_version"
,服务器返回码304,浏览器使用本地缓存,响应头如下:
ETag:"etag_version"
服务器日志打印如下:
服务端访问时间:Thu Aug 03 20:07:15 CST 2017
结合图13和图14的分析,结论:如果服务器在响应头设置Cache-Control:no-cache
,那么浏览器在使用缓存的资源之前,必须先与服务器确认上次返回的资源是否被更改,如果资源未被更改,服务器返回304 Not Modified,浏览器使用本地缓存,避免重新下载资源。
no-cache/max-age=0区别
相同之处
no-cache并不是表示无缓存,而是指使用缓存一定要先经过验证;而max-age=0表示缓存到本地,只是在下次重新访问该页面时又会强制地从服务器验证资源。所以大部分情况下,这俩其实是一样的。
不同之处
当点击浏览器的前进后退按钮时,被no-cache的资源会重新加载;而被设置成max-age的读取则会从本地读取资源。当然这也需要根据浏览器实现的情况来看,某些浏览器如IE9之前的IE,并没有遵循http协议,直接统一了这两个字段的行为为no-cache。
手动刷新页面(F5),浏览器会直接认为缓存已经过期(可能缓存还没有过期),在请求中加上字段:Cache-Control:max-age=0,发包向服务器查询是否有文件是否有更新。
强制刷新页面(Ctrl+F5),浏览器会直接忽略本地的缓存(有缓存也会认为本地没有缓存),在请求中加上字段:Cache-Control:no-cache(或 Pragma:no-cache),发包向服务重新拉取文件。
当然,各个浏览器对于刷新和强制刷新的实现方式也有一些区别。
no-cache/must-revalidate区别
no-cache: 告诉浏览器、缓存服务器,不管本地副本是否过期,使用资源副本前,一定要到源服务器进行副本有效性校验。
must-revalidate:告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。
no-store
如果服务器在响应头设置Cache-Control:no-store
,根据HTTP协议约定,浏览器和任何中继的Web代理,都不会存储这次响应的数据。当下次请求该资源时,浏览器只能重新请求服务器,重新从服务器读取资源。
后端Java代码如下,响应头设置:Cache-Control:no-store
。
package com.demo.web.http;
import org.springframework.http.CacheControl;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
@RequestMapping("http")
public class CacheControlController {
@RequestMapping("/cache-control/no-store")
public String noStore(ServletWebRequest request, HttpServletResponse response) {
System.out.println("服务端访问时间:" + new Date());
if (request.checkNotModified(getETag())) {
//it will return 304 with empty body
return null;
}
String headerValue = CacheControl.noStore().getHeaderValue();
response.addHeader("Cache-Control", headerValue);
return "http/cache-control/no-store";
}
private static String getETag() {
return "etag_version";
}
}
前端HTML代码:
<!DOCTYPE HTML>
<html>
<head>
<title>HTTP cache control Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>
</head>
<body>
<h3>实践Cache-Control no-store</h3>
<p id="time"></p>
<a href="no-store">刷新页面</a>
<script>
$("#time").text("页面访问时间:" + new Date());
</script>
</body>
</html>
如图12所示,结合请求头If-None-Match/响应头ETag
一节,第一次请求http://127.0.0.1:8080/http/cache-control/no-store
,服务器返回码200,响应头:
Cache-Control:no-store
ETag:"etag_version"
服务器日志打印如下:
服务端访问时间:Thu Aug 03 20:08:23 CST 2017
如图16所示,第二次请求http://127.0.0.1:8080/http/cache-control/no-store
,浏览器请求头不再携带If-None-Match: "etag_version
,服务器返回码200,响应头如下:
Cache-Control:no-store
ETag:"etag_version"
服务器日志打印如下,说明浏览器每次重新请求服务器获取资源,而不会缓存资源副本。
服务端访问时间:Thu Aug 03 20:09:03 CST 2017
结合图15和图16的分析,结论:如果服务器在响应中设置Cache-Control:no-store
,那么浏览器和任何中继的Web代理,都不会存储这次响应的数据。当下次请求该资源时,浏览器只能重新请求服务器,重新从服务器读取资源。
public和private
如果设置Cache-Control:public
,表示该响应可以在浏览器或者任何中继的Web代理中缓存,public是默认值,即Cache-Control:max-age=60等同于Cache-Control:public, max-age=60。
在服务器设置Cache-Control:private, max-age=60
,表示只有用户的浏览器可以缓存private响应,不允许任何中继Web代理对其进行缓存。例如,浏览器可以缓存包含用户私人信息的 HTML 网页,但是 CDN 不能缓存。
鉴于单机无法模拟中继Web代理,本小结暂不实践。
Cache-Control策略优先级
如下图17所示,所谓的HTTP缓存控制,就是一种协议约定,通过设置不同的响应头Cache-Control来控制浏览器和Web代理对缓存的使用策略,通过设置请求头If-Modified-Since/响应头Last-Modified、请求头If-None-Match/响应头ETag,来对缓存的有效性进行验证。
注意,当Cache-Control: no-store时,浏览器不会在请求头带ETag。
清除缓存方法
最常用的办法就是修改文件的版本号,或者生成随机文件名,或者改变文件的最后修改时间。如果你只是在本地测试,想手动清楚缓存的话,可以使用图18的用户操作,模拟上面的HTTP缓存实践。
[图片上传失败...(image-20774a-1525271449369)]