HTTP协议,浏览器缓存和网站速度优化

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

HTTP缓存

HTTP属于应用层协议,由请求包和响应包组成,都包括HTTP头信息(header)和主体信息(body)。在响应信息中,主体信息就是用浏览器看到的内容;请求包中,一般只有POST类型的请求才包含主体信息。web浏览器一般有两种缓存方式,一种是缓存body,通过头部信息对服务器和本地信息进行比对, 如果符合某些特征,服务器返回304 Not Modified,浏览器接受返回,并加载之前返回的内容,整个过程如下图:

HTTP缓存原理

当浏览器第一次请求信息,服务器发送如下返回头:
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),如下图:
200 ok from cache

HTTP提供两个头标签Cache-Control和Expires来控制本地缓存,Cache-Control可以通过max-age来指定过期时间,Expires指定未来一个过期时间的,具体的用法可以点击这里。需要注意的是,对于直接读取的本地缓存,只适用新打开的页面,浏览器刷新,强制刷新,一般都会重新请求新数据。

对了一个优秀的网站来说,采用多级缓存很有必要,不断能够缩短网站的打开时间,还能节省网站资源,服务更多人群。

扩展阅读:
1,?HTTP 缓存
2, Caching in HTTP

发表回复