web缓存位于客户端。缓存会根据进来的请求保存输出内容的副本,HTML页面,图片,文件(统称为副本)等。然后,当下一个请求来到时,如果是相同的URL,缓存直接使用副本响应访问请求,而不是向源服务器再次发送请求。
web缓存的具体实现是由浏览器来实现的,浏览器在计算机上开辟一块硬盘空间用来存储已经看过的网站的副本。
浏览器缓存根据非常简单的规则进行工作:在同一个会话过程中(即当前浏览器没有被关闭之前)会检查一次并确定缓存的副本足够新。
这个缓存对于用户点击“后退”或者点击刚访问过的链接特别有用,如果在浏览过程中访问到同一个图片,这些图片可以从浏览器缓存中调出并即时显示。
HTTP有一些简单的机制可以在不要求服务器记住有哪些缓存拥有其文档副本的情况下,保持已缓存数据与服务器数据之间充分一致。HTTP将这些简单的机制称为文档过期(document expira-tion)和服务器再验证(server revalidation)。
文档过期
通过特殊的HTTP Cache-Control首部和Expires首部,HTTP让原始服务器向每个文档附加了一个过期日期,就像一个产品上面的过期日期一样,这些首部说明了在多长时间内可以将这些内容视为新鲜的。
在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系,也就是说,在缓存过期之前,发送的请求都不会到服务器这边。当然,除非客户端请求中包含有阻止提供已缓存或未验证资源的首部。但是一旦已缓存文档过期,缓存就必须与服务器进行核对,询问文档是否被修改过,如果被修改过,就要获取一份新鲜(带有新的过期日期)的副本。
服务器用HTTP/1.0+的Expires首部或HTTP/1.1的Cache-Control: max-age响应首部来指定过期日期,同时还会带有响应主体。
Expires首部和Cache-Control: max-age首部所做的事情本质上是一样的,但由于Cache-Control首部使用的是相对时间而不是绝对日期,所以我们更倾向于使用比较新的Cache-Control首部。因为绝对日期依赖于计算机时钟的正确设置。
下面我们可以来一个案例看一下,我们用firefox来测试
首先,我们先把浏览器所有的缓存给清了(主要是为了方便做测试)
代码:
<?php echo '<a href="/">缓存测试</a>' . date('Y-m-d H:i:s');
我们先访问一下
我们可以看到状态码是200,响应头大小为196字节,请求头大小为385字节。
我们再来看下浏览器里面是否有这个网址的缓存。
虽然有记录,但是我们一看Expire时间,就是当前访问的时间,所以这条记录相当于立刻过期。
我们在浏览器的地址栏回车访问一下(千万不要强制刷新,强制刷新的话,缓存完全没有作用)。
我们从响应头大小和请求头大小和传输的字节数,就可以知道每一次的请求都到达了服务器。
Exprie和Cache-Control
下面我们使用一下Exprie和Cache-Control,我们过期时间定位1天,也就是86400秒
<?php header('Cache-Control: max-age=86400'); header('Expires:' . gmdate(' D, d M Y H:i:s', time() + ' 86400') . 'GMT'); echo '<a href="/">缓存测试</a>' . date('Y-m-d H:i:s');
我们再来刷新一下
我们可以从响应首部看到了Cache-Contrl首部和Expires首部,这说明我们的代码生效了。
我们从上图已经看到,浏览器缓存里面已经存放了zhoujun.cms.com链接的副本了。
那照这样看来,是不是在一天内,我访问zhoujun.cms.com,返回的数据都是从浏览器缓存中返回,而不是去访问服务器。我们可以在浏览器地址栏回车访问一下。
首先我们可以看返回的内容,从响应头大小和请求头大小和传输列的数据,我们就知道这次是直接从浏览器的缓存里面取出来的内容,而不是到服务器里面查出来的。
关于这个超链接,我们可以多点几次,效果还是和上图一样的。
从这个例子中,我们也可以验证我们前面的观点:在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系。那如果过了一天,缓存过期了呢?那么这时候浏览器会怎么做呢?这部分内容是需要数据服务器再验证的。
服务器再验证
仅仅是已缓存文档过期了并不意味着它和原始服务器上目前处于活跃状态的文档有实际的区别,这只是意味着到了要进行核对的时间了。这种情况称为“服务器再验证”,说明缓存需要询问服务器文档是否发生了变化。
- 如果再验证显示内容发生了变化,缓存会获取一份新的文档副本,并将其存储在旧文档的位置上,然后将文档发送给客户端。
- 如果再验证显示内容没有发生变化,缓存只需要获取新的首部,包括一个新的过期日期,并对缓存中的首部进行更新就行了
If-Modified-Since:Date再验证
最常见的缓存再验证首部是If-Modified-Since。通常表示只有在某个日期之后资源发生了变化的时候,If-Modified-Since请求才会指示服务器执行请求。
- 如果自指定日期后,文档被修改了,If-Modified-Since条件就为真,通常GET就会成功执行,携带新首部的新文档会被返回给缓存,新首部除了其他信息之外,还包含了一个新的过期时间。
- 如果自指定日期后,文档没被修改过,条件就为假,就会向客户返回一个小的304 Not Modified响应报文,为了提高有效性,不会返回文档的主体
If-Modified-Since首部可以与Last-Modified服务器响应首部配合工作,原始服务器会将最后的修改日期附加到所提供的文档上去,当缓存要对已缓存文档进行再验证时,就会包含一个If-Modified-Since首部。
下面我们来一个例子:
这次我们为了看效果,还是先把浏览器的缓存给全部清除掉
源代码:
<?php header('Cache-Control: max-age=180'); header('Expires:' . gmdate(' D, d M Y H:i:s', time() + ' 180') . ' GMT'); header(' Last-Modified:' . gmdate(' D, d M Y H:i:s') . ' GMT'); echo '<a href="/">缓存测试</a>' . date('Y-m-d H:i:s');
我们为了尽快看到效果。缓存过期时间设置为3分钟。接下来我们先访问一次
我们看下响应头的Expires首部和Last-Modified首部,从这里就知道我们的代码生效了,那么我们接着看下浏览器的缓存
我们可以看到过期时间为2020-05-19 00:01:14, 那么在这之前,我们刷新的话,都是直接取的缓存,这个我们就不再截图了。 三分钟过去了,我们再刷新,就看到下面的画面了。
我们从请求首部里面看到了一个If-Modified-Since首部,它的意思是问服务器在这个时间点之后有没有改动,但我们目前的程序是写的很简单的,并没有针对这个If-Modified-Since首部做任何处理。
按照常规情况,我们是需要将利用If-Modified-Since做下判断,然后来决定是返回304还是200。
下面我们把程序再完善一下,把If-Modified-Since给利用起来。
第一步还是先把浏览器的缓存给清除掉
源代码:
<?php
/** * 我们取出文件的最后修改时间 */
$file_last_mod_time = filemtime(__FILE__);
$expireTime = 180;
header('Cache-Control: max-age=' . $expireTime);
header('Expires:' . gmdate('D, d M Y H:i:s', time() + $expireTime) . 'GMT');
header('Last-Modified:' . gmdate('D, d M Y H:i:s') . 'GMT');
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
/**
* 如果$_SERVER[‘HTTP_IF_MODIFIED_SINCE’]大于小于当前文件的最后修改时间,那么就说明从上次缓存开始,这个文件没有做任何的改动
*/
if (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $file_last_mod_time) {
header('HTTP/1.1 304 Not Modified', true, 304);
exit();
}
}
echo '<a href= "/"> 缓存测试</a >' . date('Y-m-d H:i:s');
接着,我们用浏览器访问一下
我们看一下浏览器的缓存
我们再访问一下链接
从上面可以看到,我们是直接走的浏览器缓存。3分钟缓存时间快到了,我们再接着访问链接看下
我们可以看到请求头里面带有If-Modified-Since,返回的状态码是304,说明从If-Modified-Since这个时间之后,我们的文件没有做任何修改。
If-None-Match:实体标签再验证
有些情况仅适用最后修改日期进行再验证是不够的
- 有些文档可能会被周期性地重写(比如,从一个后台进程中写入),但实际包含的数据常常是一样的,尽管内容没有变化,但修改日期会发生变化。
- 有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如增加注释
- 有些服务器无法准确地判定其页面的最后修改日期。
- 有些服务器提供的文档会在亚秒间隙发生变化(比如 实时监视器),对这些服务器来说,以一秒为粒度的修改日期可能就不够用了。
为了解决这些问题,HTTP允许用户对被称为实体标签(ETag)的版本标识符进行比较。
实体标签是附加上文档上的任意标签,它们可能包含了文档的序列号或版本号,或者是文档内容的校验及其他指纹信息。
当发布者对文档进行修改时,可以修改文档的实体标签来说明这个新的版本。这样,如果实体标签被修改了,缓存就可以用If-None-Match条件首部来GET文档的新副本了。
下面我们来一个案例
第一步先清除浏览器的缓存
源代码:
<?php
/** * 我们取出文件的最后修改时间 */
$file_last_mod_time = filemtime(__FILE__);
$etag = md5_file(__FILE__);
$expireTime = 180;
header('Cache-Control: max-age=' . $expireTime);
header('Expires:' . gmdate('D, d M Y H:i:s', time() + $expireTime) . 'GMT');
header('Last-Modified:' . gmdate('D, d M Y H:i:s') . 'GMT');
header("ETag:" . $etag);
$ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
$etagHeader = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : '';
if ($ifModifiedSince && $etagHeader) {
if (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $file_last_mod_time && $etagHeader == $etag) {
header('HTTP/1.1 304 Not Modified', true, 304);
exit();
}
} elseif ($ifModifiedSince) {
/**
* 如果$_SERVER[‘HTTP_IF_MODIFIED_SINCE’]大于小于当前文件的最后修改时间,那么就说明从上次缓存开始,这个文件没有做任何的改动
*/
if (strtotime($ifModifiedSince) >= $file_last_mod_time) {
header('HTTP/1.1 304 Not Modified', true, 304);
exit();
}
} elseif ($etagHeader) {
/**
* 就说明文件的内容没有任何改变
*/
if ($etag == $etagHeader) {
header('HTTP / 1.1 304 Not Modified', true, 304);
exit();
}
}
echo '<a href ="/"> 缓存测试</a > ' . date('Y-m-d H:i:s');
我们开始访问页面
我们可以看到响应头里面带有一个Etag首部
我们在浏览器缓存里面也看到了我们当前访问的域名,我们接下来再刷新看一下
三分钟过去了,我们再访问,就可以看到请求头部带有If-None-Match,并且我们也可以看到状态码是304
有了这个Etag的话,就算我们改了文件,但只要内容没变,照样返回304。
但我们从前面的说明也看到了,有些文档可能被修改了,但所做修改并不重要,不需要让世界范围内的缓存都重装数据(比如增加注释) 这种情况也是验证通过的,但这种情况是属于弱验证器,这种属于nginx或者apache这种来配合使用,我这边就不做案例了
什么时候应该使用实体标签和最近修改日期
如果服务器回送了一个实体标签,HTTP/1.1客户端就必须使用实体标签验证器。如果服务器只回送了一个Last-Modified值,客户端就可以使用If-Modified-Since验证。如果实体标签和最后修改日期都提供了,客户端就应该使用这两种再验证方案,这样HTTP/1.0和HTTP/1.1缓存就都可以正确响应了。
除非HTTP/1.1原始服务器无法生成实体标签验证器,否则就应该发送一个出去,如果使用弱实体标签有优势的话,发送的可能就是个弱实体标签,而不是强实体标签。而且,最好同时发送一个最近修改值。
如果HTTP/1.1缓存或服务器收到的请求既带有If-Modified-Since,又带有实体标签条件首部,那么只有这两个条件都满足时,才能返回304 NotModified响应。
总结
下面我们用一幅图来总结这个流程(图来自HTTP权威指南)
参考资料:
《HTTP权威指南》
《构建高性能Web站点》