在计算机和网络的世界里,小到CPU,大到Internet,缓存无处不在。个人认为,缓存策略主要解决两个问题,第一个是解决不同设备IO速度不同的资源等待问题,第二个是解决相同资源重复传送的资源浪费问题。例如最常提到的CPU的三级缓存,就是为了解决CPU计算速度快,而读取内存速度慢导致CPU等待的问题。而我们上网的过程过,浏览器对已经请求的资源进程缓存,则属于缓存策略解决的第二个问题。本文主要分析一下HTTP协议,浏览器缓存和网站速度优化。
HTTP属于应用层协议,由请求包和响应包组成,都包括HTTP头信息(header)和主体信息(body)。在响应信息中,主体信息就是用浏览器看到的内容;请求包中,一般只有POST类型的请求才包含主体信息。web浏览器一般有两种缓存方式,一种是缓存body,通过头部信息对服务器和本地信息进行比对, 如果符合某些特征,服务器返回304 Not Modified,浏览器接受返回,并加载之前返回的内容,整个过程如下图:
当浏览器第一次请求信息,服务器发送如下返回头:
HTTP/1.1 200 OK
Connection:keep-alive
Content-Type:text/html
Last-Modified:Thu, 04 Mar 2015 01:47:20 GMT
<html>
….
</html>
这个返回头中有一个非常重要的信息Last-Modified,它告诉浏览器次资源上次更改的时间,浏览器接受这个信息,并缓存下来,当第二次请求这个资源的时候,浏览器自动加上一条If-Modified-Since:Thu, 04 Mar 2015 01:47:20 GMT,服务器接收这个请求, 并比对资源更改时间和这个时间,如果这段时间内没法发生变化,则只返回头部信息,而不返回HTML实体:
HTTP/1.1 304 Not Modified
Connection:keep-alive
Content-Type:text/html
Last-Modified:Thu, 04 Mar 2015 01:47:20 GMT
由于html的内容一般比较大,这样一来,就大大增加了网站的加载速度,减少了宽带的消耗。
Last-Modified的信息只能精确到秒,如果一秒之内文件资源作出了更改,浏览器则不会实时的加载到新的内容,人们又发明了另一个标签对Etag和If-None-Match,和上面的工作原理类似,Web服务器通过Etag标签发送资源的某种hash值,浏览器接收之后,下次访问则通过If-None-Match把缓存到的hash值发送给服务器,如果文件内容发生了变化,则前后两次的hash只不一样,服务器就发送最新的文件内容,否则返回304 Not Modified。
下面这段代码是利用Etag和Modifed给PHP页面做缓存的例子:
<?php class Cache304{ public $content; private $contlen; private $etag; private $modified; //设置过期时间,单位秒 public $expiresenconds; public function __construct($ex){ (int) $this->expiresenconds=$ex; $this->etag=isset($_SERVER["HTTP_IF_NONE_MATCH"])?$_SERVER["HTTP_IF_NONE_MATCH"]:false; $this->modified=isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])?$_SERVER["HTTP_IF_MODIFIED_SINCE"]:false; ob_start(array($this,'get_content')); } public function get_content($html){ $this->content=$html; $this->contlen=strlen($html); return $html; } public function init(){ register_shutdown_function(array($this,'show')); } public function show(){ ob_end_clean(); //Etag验证 if($this->etag == md5($this->content)){ header("HTTP/1.1 304 Not Modified"); header("Vary:etag"); exit(); }else{ //注意发送Etag必须要发送Content-Length标签 header("Etag:".md5($this->content)); header("Content-Length:$this->contlen"); header("Last-Modified:".gmdate("D, d M Y H:i:s")." GMT"); echo $this->content; exit(); } //Last-Modified验证 if($this->modified && (strtotime(gmdate("D, d M Y H:i:s")." GMT") - strtotime($this->modified) < $this->expiresenconds)){ header("HTTP/1.1 304 Not Modified"); header("Last-Modified:".$this->modified); header("Vary:Modified"); exit(); }else{ header("Etag:".md5($this->content)); header("Content-Length:$this->contlen"); header("Last-Modified:".gmdate("D, d M Y H:i:s")." GMT"); echo $this->content; exit(); } } } $a = new Cache304(3600); $a->init(); //假设一下是原本要输出的内容 echo "你好<br/>"; echo "今天是2015年3月5日<br/>"; echo "欢迎来到我的网站"; ?>
运行这段代码,当第二次访问时,服务器确实返回的是304 Not Modified。当强制刷新再次访问时,发现服务器返回的状态码又变成了200,这是因为,当我们强制刷新访问时,浏览器不再发送”If-Modified-Since”和”If-None-Match”的请求头,并且还在请求头里面加了一个”Pragma:no-cache”标识(Chrome 39.0.2171.95 m),这样是为了通知服务器发送最新的内容。
上面说到的缓存,实际上只在发送HTML内容这个地方实现了缓存,该有的业务逻辑服务器上都有运行,客户端和服务器一样建立了TCP链接,网站速度虽然快了,但是对服务器的压力却没有多大的改善。HTTP提供另外一种直接读取本地缓存的方式,这种方式下浏览器直接读取缓存,而不向服务器发送请求,返回的状态码是200 OK(from cache),如下图:
HTTP提供两个头标签Cache-Control和Expires来控制本地缓存,Cache-Control可以通过max-age来指定过期时间,Expires指定未来一个过期时间的,具体的用法可以点击这里。需要注意的是,对于直接读取的本地缓存,只适用新打开的页面,浏览器刷新,强制刷新,一般都会重新请求新数据。
对了一个优秀的网站来说,采用多级缓存很有必要,不断能够缩短网站的打开时间,还能节省网站资源,服务更多人群。
扩展阅读:
1,?HTTP 缓存
2, Caching in HTTP