网络协议-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 之类的缓存方案来解决。

原文地址:https://www.cnblogs.com/stringarray/p/12995827.html