在计算机和网络的世界里,小到 CPU,大到 Internet,缓存无处不在。个人认为,缓存策略主要解决两个问题:第一个是解决不同设备 I/O 速度不同导致的资源等待问题;第二个是解决相同资源重复传送造成的资源浪费问题。
例如最常提到的 CPU 三级缓存,就是为了解决 CPU 计算速度快,而读取内存速度慢导致 CPU 等待的问题。而我们上网的过程中,浏览器对已经请求过的资源进行缓存,则属于缓存策略解决的第二个问题。
本文主要分析 HTTP 协议、浏览器缓存和网站速度优化。

HTTP 属于应用层协议,由请求报文和响应报文组成,都包括 HTTP 头信息(Header)和主体信息(Body)。
在响应信息中,主体信息就是浏览器最终看到的内容;请求报文中,一般只有 POST 等包含请求主体的请求才会携带 Body。
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 和 Last-Modified 为 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。这是因为强制刷新时,浏览器会要求服务器重新验证或获取资源,不再直接使用之前的缓存。
上面说到的缓存,实际上只是在发送 HTML 内容时减少了重复传输。服务器原本应该执行的业务逻辑仍然会继续运行,客户端和服务器之间也依然需要建立连接。
网站加载速度虽然提高了,但是对服务器压力的改善并不明显。
HTTP 还提供了另外一种直接读取本地缓存的方式。在这种方式下,浏览器直接读取本地缓存,而不向服务器发送请求。
如下图:

HTTP 提供 Cache-Control 和 Expires 两个头部字段来控制本地缓存。
Cache-Control 可以通过 max-age 指定缓存的有效时间,Expires 则直接指定一个未来的过期时间。具体的用法可以点击这里。
需要注意的是,对于直接读取的本地缓存,浏览器的实际行为还会受到刷新方式和具体缓存策略的影响。普通刷新和强制刷新通常都会触发重新验证或重新请求。
对于一个优秀的网站来说,采用合理的多级缓存是很有必要的。不仅能够缩短网站的打开时间,还可以减少带宽和服务器资源消耗,从而服务更多用户。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。