• 网络协议-HTTP协议详解-HTTP缓存


    首先需要声明的是,我们这里讨论的缓存是基于 HTTP 协议实现的缓存,这些缓存通常存储在 HTTP 客户端,通过请求头或响应头来协商和标识,而不是那些存储在 Memcached 或者 Redis 服务器中的缓存,后者更多用来缓存从数据库中获取的数据。

    为什么需要缓存

    在通过客户端访问服务器时,对于某些静态资源文件或页面(比如 HTML 文档、CSS、JavaScript 文件、图片等),它们变动的频率很小,同一个客户端发起多次请求返回的都是同一个文件,这样就会对服务器的带宽造成浪费,同时也会加重 Web 服务器的负载,降低 Web 服务器的性能。如果在客户端首次获取到这些静态文件后,将这些变动频率很低的静态文件缓存到客户端,这样,客户端下次发起请求时,就可以直接从本地获取对应的缓存文件,不必每次都从服务器获取,就可以提高服务器的负载,进而提升服务器的性能,同时还会减少网络流量,降低客户端请求等待延迟,从而提升客户端用户的体验,这就是 HTTP 缓存的意义。

    HTTP 缓存的种类

    HTTP 缓存的种类有很多,但大致可以分为私有缓存和共享缓存两种:

    • 私有缓存:作用于单个用户,通常就是浏览器缓存;
    • 共享缓存:往往存放在可以被多个用户共享的代理之中,所以有时候也叫代理缓存。

    除此之外还有网关缓存也属于 HTTP 缓存,反向代理缓存、CDN 缓存都属于其范畴之内,这些更加复杂的缓存我们放到后续去讨论,而代理缓存往往存储在公司或 ISP 服务商假设的作为本地网络一部分的 Web 代理中,也不是我们本篇重点讨论的内容,所以接下来我们主要以客户端浏览器缓存为例来介绍 HTTP 缓存的工作机制和实现原理。

    HTTP 缓存的工作原理

    虽然 HTTP 缓存的种类繁多,构建机制也不尽相同,但基本工作原理是一致的,无外乎以下这几个步骤:

    • 接收:读取请求报文;
    • 解析:对请求报文进行解析,提取 URL 和各种首部字段;
    • 查询:查看是否有本地缓存可用,如果没有,则从服务器获取相应的资源并存储到本地;
    • 新鲜度检查:缓存不会一直有效,所谓的「新鲜度」指的是和食品的保质期类似,缓存是有有效期的,在有效期之内才可以使用,否则需要向服务器查询对应资源是否有更新;
    • 创建响应:缓存会用新的首部和缓存的响应主体来构建响应报文;
    • 发送:将响应发送给客户端;
    • 日志:缓存可以创建一条日志来记录这个 HTTP 事务。

    所以,总结下来,一个 HTTP 请求的缓存处理流程如下:

    另外,需要注意的是通常我们只会对 GET 请求资源进行缓存,因为只有 GET 请求不会对资源实体的状态进行改变,OPTIONS 请求不返回响应实体没有缓存的意义,而其他诸如 POST、PUT、DELETE、PATCH 这些会改变资源状态的请求则不能进行缓存。

    HTTP 缓存的实现机制

    基于 HTTP 协议的 HTTP 缓存是通过在请求头和响应头中设置相应的字段值来实现的,下面我们将详细介绍比较常见的缓存相关首部字段,比如 ExpiresCache-ControlLast-Modified/If-Modified-SinceEtag/If-None-Match 等。

    Expires

    Expires 字段的值为服务端返回的缓存资源到期时间(绝对时间),即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。

    不过 Expires 是 HTTP/1.0 的东西,现在浏览器均默认使用 HTTP/1.1,所以它的作用基本忽略。

    另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。所以 HTTP/1.1 使用 Cache-Control 替代该字段。而且如果在 Cache-Control 响应头设置了 max-age 或者 s-max-age 指令,那么 Expires 头也会被忽略。

    下面我们将继续探讨基于 Cache-Control、Last-Modified/If-Modified-Since、Etag/If-None-Match 这三种方式实现 HTTP 缓存。

    Cache-Control

    在 HTTP/1.0 中通过 Expires 首部字段来判断缓存是否过期,但是 Expires 字段值是一个绝对日期,有其局限性,在 HTTP/1.1 中我们统一通过 Cache-Control 字段来控制缓存的有效期及实现细节。可以说 Cache-Control 是 HTTP 缓存相关首部字段中最重要的一个字段,下面我们具体来看如果通过该字段设置 HTTP 缓存。

    Cache-Control 字段中可以设置多个属性值,不同属性值之间通过逗号分隔,作为一个通用首部字段,请求头和响应头中都可以出现这个字段,并且通过不同的属性值来定义 HTTP 缓存策略。常见的属性及其含义如下所示:

    • no-store:禁止进行缓存,缓存中不得存储任何关于客户端请求和服务端响应的内容,每次由客户端发起的请求都会从服务端下载完整的响应内容;
    • no-cache:这个属性值很具有迷惑性,它的含义并不是不使用缓存,而是强制确认缓存,每次有请求发出时,缓存会将此请求发到服务器(该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期,则缓存才使用本地缓存副本。该属性和 HTTP/1.0 中的 - Pragma: no-cache 等效;
    • public:用于共享缓存,任何中间代理都可以缓存响应;
    • private:用于私有缓存,只有客户端浏览器才可以缓存响应,没有指定 public 时,默认为 private;
    • max-age:用于设置缓存有效期,与 Expires 字段值不同,max-age 是距离请求发起时间的秒数,是一个相对值,从而可以避免客户端与服务端时间不一致导致的误差,如果在响应头中两者都存在,则以 max-age 为准,Expires 自动失效;
    • must-revalidate:使用该指令时,意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用。该属性与 no-cache 的区别在于,使用 no-cache 时,不管本地资源缓存副本是否过期,使用资源缓存副本前,一定要到源服务器进行副本有效性校验,而 must-revalidate 则不然,只有在本地资源缓存副本过期后,才去源服务器进行有效性检测。

    关于缓存有效性检测(或者叫做新鲜度检测、服务器再验证),在 HTTP/1.1 协议中可以通过两对首部字段来实现。

    Last-Modified/If-Modified-Since

    Last-Modified 字段常用于响应头中,告知客户端资源的最后修改时间,这样,当客户端再次请求该资源时,会在 If-Modified-Since 请求头字段中带上上次请求返回的最后修改时间,服务器收到请求报文后发现请求头包含 If-Modified-Since 字段,则与被请求资源的最后修改时间进行对比。如果资源的最后修改时间大于 If-Modified-Since 字段值,说明资源又被改动过,则返回完整的资源内容,对应响应状态码为 200;如果资源的最后修改时间小于或等于 If-Modified-Since 字段值,说明资源没有做新的修改,则返回状态码 304,告知浏览器使用本地保存的缓存作为响应实体。

    Etag/If-None-Match

    和上面那对首部字段类似,Etag 用于响应头中,告知客户端资源在服务器的唯一标识(生成规则由服务器指定,每当资源发生修改后 Etag 值会变化),当客户端再次请求该资源时,通过 If-None-Match 字段通知服务器客户段缓存资源数据的唯一标识。服务器收到请求报文后发现请求头包含 If-None-Match 字段,则与被请求资源的唯一标识进行对比,如果不同,说明资源又被改动过,则返回完整的资源内容,对应响应状态码为 200;如果相同,说明资源没有做新的修改,则返回状态码 304,告知浏览器使用本地保存的缓存作为响应实体。

    需要指出的是 Etag/If-None-Match 的优先级要高于 Last-Modified/If-Modified-Since,如果同时出现,以前者为准。

    综上,缓存有效性检测逻辑流程图如下所示:

    如果把范围再扩大到通过 Cache-Control 来定义 HTTP 缓存策略,则对应的流程图如下所示:

    最后一步「Add Etag Header」还可以改为「Add Last-Modified Header」。

    以上就是 HTTP 缓存的底层工作原理和实现机制。

    Laravel 项目中实现 HTTP 缓存:浏览器缓存

    在实际项目中,基于客户端浏览器的私有缓存并不是主流的实现方案,因为服务端页面更新后,往往需要用户主动刷新页面才能清空缓存,并不便于服务端去控制,所以针对 HTTP 缓存,使用网关缓存的实现更为主流,比如我们比较熟悉的 CDN 缓存、反向代理缓存都属于这一范畴,常见的反向代理服务器有 Nginx、Squid、Varnish 等,Nginx 更多用于高性能 Web 服务器,具备缓存静态资源的能力,Squid、Varnish 则多用于代理缓存服务器,用于实现 HTTP 静态缓存。我们这里强调「静态」是为了区别于 Memcached、Redis 之类的缓存服务器,后者更多用于存储从数据库获取的、动态变化的「动态」缓存。

    下面我们以基于 Laravel 框架的 PHP 项目为例,简单演示下如何通过浏览器缓存和网关缓存实现 HTTP 缓存。

    浏览器缓存

    首先我们来看通过 Expires 响应头实现 HTTP 缓存,这很简单,只需要在返回的响应实例上设置额外的 Expires 头即可,我们设置资源过期时间为 1 小时后:

    Route::get('expires', function () {
        return response('Test Expires Header')->setExpires(new DateTime(date(DATE_RFC7231, time() + 3600)));
    });
    

    在浏览器访问该路由,首次访问本地还没有缓存副本,会从服务器拉取资源并保存到本地,再次访问就可以通过浏览器缓存获取资源了:

    响应状态码仍然是 200,但是后面有一个提示,说明该资源是从本地缓存获取的。注意不要刷新页面,否则会在请求头中加上 Cache-Control: max-age=0,设置该请求头后,每次都会从服务器验证缓存是否已过期,只有在服务器返回 304 响应时才会应用缓存,否则会从服务器拉取最新资源。

    类似的,我们还可以通过在响应头设置 Cache-Control 字段来实现浏览器缓存:

    Route::get('cache_control', function () {
        return response('Test Cache-Control Header')->setClientTtl(3600);
    });
    

    这段代码会在响应头中设置 Cache-Control 的 max-age 属性值为 3600,表示缓存有效期为 1 个小时。同样,首次访问的时候,由于本地没有相应的缓存副本,会从服务器读取最新资源并保存到本地,第二次访问的时候,就会从缓存获取了:

    上述两种缓存策略都属于强制缓存,如果响应头 Cache-Control 中设置了 no-cache,则需要客户端发送相应的请求协商头(If-Modified-Since/If-None-Match),与服务端对应字段(Last-Modified/Etag)对比验证缓存是否过期来实现 HTTP 缓存,这种缓存策略我们称之为对比缓存。

    我们以 If-Modified-Since/Last-Modified 为例来演示这种浏览器缓存的实现,首先我们在 Laravel 项目中定义相应的路由如下:

    Route::get('no_cache', function (IlluminateHttpRequest $request) {
        $httpcache = false;
        $lastmodified = 'Thu, 09 May 2019 22:32:00 GMT';
        if ($request->hasHeader('If-Modified-Since')) {
            $time1 = new DateTime($request->header('If-Modified-Since'));
            $time2 = new DateTime($lastmodified);
            if ($time1->getTimestamp() >= $time2->getTimestamp()) {
                $httpcache = true;
            }
        }
        $response = response('');
        $response->headers->addCacheControlDirective('no-cache', true);
        $response->setClientTtl(3600);
        $response->setLastModified(new DateTime($lastmodified));
        if ($httpcache) {
            $response->setStatusCode(304);
            return $response;
        }
        $response->setContent('Test Cache-Control Header:no-cache');
        return $response;
    });
    

    我们需要设置响应头 Cache-Control 字段值为 no-cache,max-age=3600,private,同时还设置 Last-Modified 字段值为一个固定值,并且需要注意的是在对比缓存中,如果缓存有效,返回的响应状态码是 304,这一点和强制缓存不同,在浏览器中访问该路由,首次访问的时候会从服务器获取资源并缓存到本地,再次访问的时候,浏览器会自动加上 If-Modified-Since 请求头,如果缓存有效则返回 304 状态码,然后使用本地缓存作为响应实体在页面渲染:

    max-age=0、no-cache 与 no-store 的区别

    在浏览器访问该路由,会发现尽管设置了 Expires 头,但是浏览器并没有通过缓存获取资源,而是每次都从服务器获取资源,这是因为 Chrome 浏览器默认为在请求头中设置 Cache-Control 字段值为 max-age=0:

    Cache-Control: max-age=0
    

    即缓存过期时间为0,意思是不管响应头如何设置,客户端每次仍然会请求服务器判断资源是否过期,如果服务器返回 304 响应,则使用客户端缓存,否则使用服务端最新资源,效果等同于刷新(F5)浏览器页面。还有一个与之类似的请求头:

    Cache-Control: no-cache
    

    该请求头用于协商缓存,并不是不缓存的意思,而是每次都要去服务器验证资源有没有更新,如果更新了则使用服务器返回的最新资源,否则使用浏览器本地缓存。最后还有一个 no-store:

    Cache-Control: no-store
    

    意思是不管响应头如何设置,客户端都不会进行重新验证,服务器也不能返回缓存副本,每次请求都会从服务器获取最新资源,效果等同于强制刷新(Ctrl+F5)浏览器页面。

    浏览器这么做固然是为了让用户每次可以获取服务端最新资源,但是这个默认行为导致我们也没法做测试,毕竟这个是客户端行为,我们无法通过服务端代码来控制。为了方便测试,我们可以在 Chrome 中安装 Smart Header 扩展来修改请求头,不要带上 Cache-Control 字段,这样再访问上面定义的 expires 路由,就可以通过浏览器缓存获取资源了。

    Laravel 项目中实现 HTTP 缓存:网关缓存

    **比起浏览器缓存,网关缓存更易于通过服务端代码进行维护和控制,同时还可以被多个客户端共享,所以更推荐在实际项目中以这种方式实现 HTTP 缓存。
    **

    原理概述

    这里要介绍的网关缓存主要就是反向代理缓存,常见的反向代理服务器有 Nginx、Varnish、Squid 等,但是这里为了简化模型,将使用 Symfony 框架提供的 HTTP Cache 功能来做演示,Laravel 框架底层的 HTTP 模块是基于 Symfony 的,所以我们很容易在 Laravel 框架中基于 Symfony 的 HTTPCache 模块来实现 HTTP 缓存,更加方便的是,还有一个现成的 Laravel HTTP Cache 扩展包 barryvdh/laravel-httpcache 对 Symfony 的 HTTP 缓存功能进行了封装,以便我们在 Laravel 项目中快速接入以实现 HTTP 缓存。

    有关 Symfony 的 HTTP Cache 功能可以参考 Symfony 文档,这里我们将重点放在基于 laravel-httpcache 扩展包在 Laravel 项目中演示基于网关缓存实现 HTTP 缓存上。

    安装扩展包

    首先,我们通过 Composer 在 Laravel 项目根目录下安装这个扩展包:

    composer require barryvdh/laravel-httpcache
    

    然后,在 app/Http/Kernel.phpweb 中间件组中添加一个中间件:

    BarryvdhHttpCacheMiddlewareCacheRequests::class,
    

    这样,我们就可以在 routes/web.php 定义的路由中应用 HTTP 网关缓存了,之所以叫做网关缓存,是因为这个缓存存放在服务器网关而非客户端浏览器或中间代理中,这里的网关就是 Symfony 底层基于 PHP 实现的简单反向代理服务器了(工业级反向代理缓存服务器还是使用 Varnish 或 Squid)。

    网关缓存实现:

    Expires

    就是这么简单,接下来我们可以编写路由来演示基于底层 Symfony 网关实现的 HTTP 缓存了,首先我们来看基于 Expires 响应头的缓存:

    Route::get('expires', function () {
        return response('Test Expires Header')
            ->setPublic()
            ->setExpires(new DateTime(date(DATE_RFC7231, time() + 3600)));
    });
    

    由于缓存要存放在服务器网关中,所以 Cache-Control 响应头中需要设置 public 属性(默认是 private),我们可以通过 setPublic 方法来实现这一目的,然后通过 setExpires 方法设置缓存过期时间,这样,在浏览器访问该路由,首次访问的时候,会从服务器读取最新资源数据,同时 Symfony 网关会设置相应的响应头 X-Symfony-Cache: miss,store,表示缓存未命中,已存储:

    缓存记录默认存储在 Laravel 项目的 storage/httpcache 目录下,再次访问该路由,就会从网关缓存获取资源了,这可以通过 X-Symfony-Cache: fresh 响应头得知:

    Cache-Control

    接下来,我们来看下基于 Cache-Control 响应头实现的网关缓存,相应的路由定义如下:

    Route::get('cache_control', function () {
        return response('Test Cache-Control Header')->setTtl(3600);
    });
    

    max-age 用于设置本地缓存有效期,如果是代理缓存或网关缓存,则需要通过 s-maxage 属性来设置,所以在上述代码中我们使用了 setTtl 方法而不是 setClientTtl,该方法会同时设置 Cache-Control 响应头的 s-maxage 以及 public 属性,这样,我们在浏览器中访问该路由,首次访问当然还是不会命中,但缓存会被存储到网关:

    再次访问,就可以从缓存获取资源了:

    If-Modified-Since/Last-Modified

    下面我们再以 If-Modified-Since/Last-Modified 为例演示一个对比缓存的例子,编写路由定义如下:

    Route::get('no_cache', function () {
        $response = response('Test If-Modified-Since/Last-Modified Http Cache');
        $response->headers->addCacheControlDirective('no-cache', true);
        $response->setTtl(3600);
        $response->setLastModified(new DateTime(date(DATE_RFC7231)));
        return $response;
    });
    

    我们设置响应头 Cache-Control 字段值为:no-cache,public,s-maxage=3600,表示需要进行缓存新鲜度检测,如果缓存未过期,则返回 304 响应,否则返回最新资源,同样首次访问该路由的时候缓存未命中,但会保存下来:

    再次访问,则返回 304 响应,然后从网关缓存获取缓存副本作为响应实体返回给客户端:

    If-None-Match/Etag 实现思路也是类似,这里不再单独演示了。

    小结

    以上就是 Laravel 项目中实现通过浏览器缓存和网关缓存实现 HTTP 缓存的大致思路,有了 HTTP 缓存,就可以降低服务器负载和网络带宽,从而提高服务器性能,加快用户访问页面速度,在实际项目中,你可以将 Symfony 网关替换成 Varnish 之类的工业级软件,效果会更好,在 Laravel 中使用 Varnish 可以使用 spatie/laravel-varnish 这个扩展包。另外,需要注意的是,以上 HTTP 缓存都会存储完整的响应实体,即整个页面,所以 HTTP 缓存多适用于静态页面或文件的存储,如果你想要缓存数据片段的话,则更适合通过 Memcached 或 Redis 之类的缓存方案来解决。

  • 相关阅读:
    Android 解决小米手机Android Studio安装app 报错的问题It is possible that this issue is resolved by uninstalling an existi
    Android Unresolved Dependencies
    Android studio 自定义打包apk名
    Android Fragment与Activity交互的几种方式
    魅族和三星Galaxy 5.0webView 问题Android Crash Report
    Android几种常见的多渠道(批量)打包方式介绍
    Android批量打包 如何一秒内打完几百个apk渠道包
    上周热点回顾(9.30-10.6)团队
    上周热点回顾(9.23-9.29)团队
    上周热点回顾(9.16-9.22)团队
  • 原文地址:https://www.cnblogs.com/stringarray/p/12995827.html
Copyright © 2020-2023  润新知