当我们通过浏览器打开一个web页面的时候,浏览器需要从web服务器下载各种资源(网页、脚本、样式表等)。但并不是每次请求都需要从web服务器中获取的,浏览器自身可以设置缓存。本篇将会介绍些浏览器的缓存技术。

浏览器使用缓存的好处有:

  • 较少服务器的计算开销,避免由于内容的重复传输带来的带宽浪费
  • 浏览器自身缓存,只需要从本地缓存取资源,响应速度比网络快

因为浏览器和服务器是通过HTTP协议沟通的,所以浏览器缓存本质是HTTP协议的缓存。

不使用缓存

我们可以通过php动态网页来模拟设置一些缓存信息,通过添加必要的HTTP头信息(浏览器不区分内容是否是动态生成的)。响应头如果是POST模式递交数据,则返回的页面大部分不会被浏览器缓存,如果你发送内容通过URL和查询(通过GET模式),则返回的内容可以缓存下来供以后使用。

<?php
echo time();
?>

请求头如下:

Request URL:http://vmnet64.com/http_cache.php
Request Method:GET
Status Code:200 OK
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Cache-Control:max-age=0
Connection:keep-alive

响应头如下:

view source
Connection:keep-alive
Content-Type:text/html
Date:Wed, 27 Apr 2016 04:14:53 GMT
Server:nginx/1.6.3
Transfer-Encoding:chunked
X-Powered-By:PHP/5.4.16

由于这里服务端未使用缓存,所以刷新每次都会从服务器获取响应结果(笔者MAC中的浏览器有 chrome/safari)。

Last-Modified

一般web服务器会为静态文件的HTTP响应头自动生成最后修改时间,而动态内容一般不存在传统意义上的最后修改时间。这里为演示方便,给演示脚本http_cache.php设置了。

Last-Modified 设置

修改代码如下:

<?php
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
echo time();
?>

此时,Web服务器的HTTP响应头中增加了一个字段:

Last-ModifiedWed, 27 Apr 2016 04:47:52 GMT

它代表了Web服务器对浏览器的暗示,告诉它当前请求内容的最后修改时间。此时,再次刷新浏览器,它发出的HTTP请求多了一行字段:

If-Modified-Since:Wed, 27 Apr 2016 04:47:52 GMT

这意味着浏览器在询问Web服务器“请求的内容在这个时间之后是否有更新”。主要任务在Web服务器,它需要检查这个内容在该时间后是否有过更新,并反馈给浏览器结果。

对If-Modified-Since 的处理

下面在动态脚本中加入对浏览器的询问的接受处理过程,代码如下:

<?php
$modified_time = $_SERVER['HTTP_IF_MODIFIED_SINCE']
if (strtotime($modified_time) + 60 > time()) {
	header("HTTP/1.1 304");
	exit(1);
}
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
echo time();
?>

此时,再次刷新浏览器,可以看到请求头:

Request URL:http://vmnet64.com/http_cache.php
Request Method:GET
Status Code:304 
Remote Address:192.168.126.130:80
Response Headers
view source
Connection:keep-alive
Date:Wed, 27 Apr 2016 05:03:54 GMT
Server:nginx/1.6.3
X-Powered-By:PHP/5.4.16

以上PHP脚本,首先获取HTTP请求中的If-Modified-Since字段,然后将它将它和当前服务器时间比较,如果距离当前时间不到一分钟,就返回304状态码,此时浏览器收到304后,会取本地缓存的资源。但是对于静态网页来说,就不需要写脚本比较时间了,Web服务器只要将静态文件的最后修改时间与浏览器询问的时间进行对比即可

ETag

HTTP/1.1另一种协商方法,该方式不采用内容的最后修改时间,而是采用一串编码来标记内容,称之为ETag。该协商方式的原则是:如果一个内容的ETag没有变化,那么这个内容也一定没有更新

由于服务器控制了ETag如何生成,Web服务器可以通过If-None-Match来标识请求内容是否变化了。eTag的校验方式正越来越流行。

对服务器性能的影响

性能问题也需要分静态文件与动态文件。

  • 对静态类型的文件来说,服务器吞吐量的提升是非常相当明显,而且服务器的带宽使用率也大大降低
  • 对动态类型的文件来说,主要瓶颈是在Web服务器端的计算。所以,需要后端的优化

Expires

上述的Last-Modified缓存协商方式,每次都需要和Web服务器沟通,还有一种方式能够彻底消灭请求。

The goal of caching in HTTP/1.1 is t eliminate the need to send requests in mang cases

Expires这种标记,它通知浏览器该内容在何时过期,按时浏览器在内容过期之前不再询问服务器,而是直接通过本地缓存获得内容即可。Expires 字段接收以下格式的值:“Expires: Sun, 08 Nov 2009 03:37:26 GMT”。如果查看内容时的日期在给定的日期之前,则认为该内容没有失效并从缓存中提取出来。反之,则认为该内容失效,缓存将采取一些措施。

静态文件一般不开启Expires标记的支持,需要对Web服务器进行配置; 对于动态内容,则仍需要程序来添加。

Expires标记

以下将Expires标记添加到它的响应HTTP头中,代码如下:

<?php
$modified_time = $_SERVER['HTTP_IF_MODIFIED_SINCE']
if (strtotime($modified_time) + 60 > time()) {
	header("HTTP/1.1 304");
	exit(1);
}
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
**header("Expires: " . gmdate("D, d M Y H:i:s", time() + 60) . " GMT");**
echo time();
?>

HTTP响应中也会出现Expires的标记:

Expires:Wed, 27 Apr 2016 07:01:46 GMT

不同的请求方方式

对于浏览器的操作不同,浏览器的处理方式就不同了,这也是很有意思的。浏览器缓存机制详解文章中专门罗列出了对不同浏览器的不同操作,浏览器的请求资源的方式。

Cache-Control(适应本地的过期时间)

由于通过Expires制定的过期时间,是来自于Web服务器的系统时间,如果用户本地时间和服务器时间不一致,则会影响到本地缓存的有效期检查。HTTP/1.1中通过一个标记来弥补Expires的不足:Cache-Control。

格式如下:

Cache-Control = “Cache-Control” “:” cache-directive

下表展示了cache-directive常用的值:

Cache-directive 说明
public 所有内容都将被缓存(客户端和代理服务器都可缓存)
private 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
no-cache 必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。若存在合适的ETag,no-cache会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。
no-store 所有内容都不会被缓存到缓存或 Internet 临时文件中
must-revalidation/proxy-revalidation 如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
max-age=xxx (xxx is numeric) 缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高

Cache-Control 是最重要的规则。这个字段用于指定所有缓存机制在整个请求/响应链中必须服从的指令。因此该字段也是浏览器优先考虑的。

参考阅读

cache-cotrol

浏览器缓存机制详解