HTTP缓存的机制有哪些?

1. 背景

这两天研究了下 HTTP 的缓存机制,其中版本是 HTTP 1.0/1.1 。现在将几个和 HTTP cache 有关的 Header 的用法做一个总结。

2. 缓存机制

2.1 Header 取值

服务器在返回的 response 中主要使用两个 Header 来控制浏览器的缓存行为:

  1. Expires :在 HTTP 1.0 版本中定义,为了兼容老版本 UA 常常也会加上该 Header,后面跟一个绝对时间字符串,表示过期时间。
  2. Cache-Control :在 HTTP 1.1 版本中定义,除了提供了同 Expires 相同并更精确的缓存功能,还提供了验证机制,它可以取以下这些值:
    • max-age :功能和 Expires 类似,但是后面跟一个以“秒”为单位的相对时间,来供浏览器计算过期时间。
    • no-cache :提供了过期验证机制,下文会再着重介绍。
    • no-store :表示当前请求资源禁用缓存。
    • private :指示只有用户客户端可以缓存,而 CDN 等不可。
    • public :指示用户客户端和 CDN 都可以缓存当前资源。

2.2 直接缓存

当 response 中设置了 Expires: {date_string} 或者 Cache-Control: max-age={secs} 时,浏览器会缓存所请求资源,如果下次请求时间没有超过缓存过期时间,浏览器会直接从缓存中读取请求资源,而不会向浏览器发送请求。

当二者同时设置时,以 Cache-Control 的设置为准。

2.3 no-cache 验证

如果设置了 Cache-Control: no-cache ,那么浏览器会在下次请求相同资源时,同样向 server 发送请求,并带上相关验证 Header 字段,供 server 判断资源是否过期,并决定是返回该资源新的内容(状态码200,已过期),或是返回资源未修改的标识给客户端使其从缓存中读取(状态码304,Not Modified)。其中相应的验证 Header 字段和判断方法会在下文介绍。

2.4 优先级

这里牵扯到一个优先级问题,就是 Expires 和 Cache-Control 两个 Header 的优先级问题。经上文得知 max-age (为了描述方便省去 Cache-Control )的优先级要高于 Expires ,那么 no-cache 和 Expires 呢?(虽然同时设置二者不合逻辑),结果可以通过下面的例子得出,示例代码如下:

var http = require('http');

var counter = 1;

http.createServer(function (req, res) {
  var now = new Date();
  var expires = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds() + 1); 

  res.writeHead(200, {'Content-Type': 'text/plain', 'Cache-Control': 'max-age=10', 'Expires': expires.toUTCString()});
  //res.writeHead(200, {'Content-Type': 'text/plain', 'Cache-Control': 'no-cache', 'Expires': expires.toUTCString()});
  
  res.write('hello world!\n');


  console.log('Request ' + (counter++) + ' has been sent!');
  console.log(req.url);

  res.end();

}).listen(1337, '127.0.0.1');


console.log('Server running at http://127.0.0.1:1337/');
var http = require('http');
 
var counter = 1;
 
http.createServer(function (req, res) {
  var now = new Date();
  var expires = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds() + 1); 
 
  res.writeHead(200, {'Content-Type': 'text/plain', 'Cache-Control': 'max-age=10', 'Expires': expires.toUTCString()});
  //res.writeHead(200, {'Content-Type': 'text/plain', 'Cache-Control': 'no-cache', 'Expires': expires.toUTCString()});
  
  res.write('hello world!\n');
 
 
  console.log('Request ' + (counter++) + ' has been sent!');
  console.log(req.url);
 
  res.end();
 
}).listen(1337, '127.0.0.1');
 
 
console.log('Server running at http://127.0.0.1:1337/');

例子中2行 writeHead() 代码分别对应2种情况:

  1. 同时设置 Expires 和 max-age ,但二者值不同。
  2. 同事设置 Expires 和 no-cache 。

对应第一种情况,结果如下图:

HTTP缓存的机制有哪些?_第1张图片

可以看出超过1s( Expires 设置的过期时间)后继续访问,浏览器仍然从缓存中取出资源,在 server 中也可以看到第二次请求没有到 server 端。

需要注意的是,第一次请求后不能直接 F5 刷新,而是新开一个窗口输入该 url 来请求资源观察,因为 F5 刷新浏览器会忽略这两个字段。

对应第二种情况,结果如下图:

观察 server 端,可以看到,即使设置了 Expires ,在缓存“过期”前,请求仍然会到达 server 端。同样需要注意的是,浏览器每次访问该 url,实际发送了两次请求,第二个对应的是 /favicon.ico ,这是浏览器的行为,所以两个请求可以视为一次操作。

综上, Cache-Control 的优先级要高于 Expires 。

3. 验证机制

当设置了 no-cache 时,server 如何判断是否需要返回文件内容呢?有以下两种方式。

3.1 Last-Modified / If-Modified-Since

第一种是通过时间戳控制。server 每次返回 response 时,会在 Header 中添加Last-Modified 后接一个表示当前资源修改时间的时间戳,下次浏览器请求资源时会将该时间戳作为 Header 中 If-Modified-Since 的值发送到 server,然后由 server 判断是返回最新资源内容还是返回304使浏览器从缓存读取(以减少数据传输量)。

需要注意:在 Chrome 的 devtool 中勾选 Disable cache 选项后,发送的请求会去掉If-Modified-Since 这个 Header。

3.2 ETag / If-None-Match

对于时间戳判断的方式,有一种弊端。时间戳的最小单位是1s,如果在1s内资源发生了改变,那么将无法获取最新资源。所以又有使用 ETag (entity tag)的方式来判断文件是否更新,由 server 来根据文件内容来生成一个唯一的 hash 值(md5等),截取一部分作为 Header 中 ETag 的值返回。下次浏览器请求时会将该值作为 Header 中If-None-Match 的值发送给 server,server 可以重新计算该资源的 ETag 值来对比判断文件内容是否变化,从而决定返回操作。

当同时设置时,该验证方式的优先级要高于时间戳的验证方式。

4. 总结

以上就是对常用 HTTP 缓存方式的学习记录,实际应用中,还是应该结合自己网站的情况来设置合理的缓存策略。

你可能感兴趣的:(java)