0%

HTTP缓存机制详解

什么是HTTP缓存

当我们访问一个Web页面时,浏览器会从服务器下载对应的css、js等资源。由于这些资源不会经常发生改变,所以浏览器可以将它们缓存到本地,这样在下次需要这些资源的时候就可以直接从本地加载,而不必从HTTP服务器请求和下载。从而节省了宽带,降低了服务器压力。


HTTP缓存控制方法

HTTP缓存通过HTTP头部字段进行控制,常见的的HTTP响应头如下:

  • Expires (RFC7234
    其值为一个GMT日期(时间戳),设置资源将在此时间后过期。
    示例:

    1
    Expires: Thu, 01 Dec 1994 16:00:00 GMT

    需要注意的是,如果一个响应头中包含有 Cache-Control,且此 Cache-Control 包含 max-age 指令,则浏览器必须忽略掉 Expires 头。详见 RFC7345#section-5.3

    If a response includes a Cache-Control field with the max-age
    directive (Section 5.2.2.8), a recipient MUST ignore the Expires
    field.

  • Cache-Control (RFC7234
    该头部用于指定缓存相关的指令。其常用的指令如下(注意:以下描述中的“可以”表示可以这样做,但不强制这样做):

    public:
    该响应可以被任何对象缓存,该对象可以是中间代理或者是发送本请求的客户端。

    private:
    该响应可以被单个用户缓存,不可以被任何中间对象(如中间代理器)缓存。

    no-cache:
    当客户缓存了此响应,然后在再次使用此缓存之前,必须向服务器验证此缓存的有效性。关于如何向服务器验证资源有效性,请参考本文 EtagIf-None-Match 以及 If-Modified-Since 部分。
    值得注意的是,此指令也可以出现在请求头中。当此指令出现在请求头中时,表示客户端强制重新请求最新的服务器资源,而不使用缓存。

    no-store:
    任何对象(代理器和个人浏览器)不可以缓存该响应。

    no-store与no-cache的区别:
    no-store 意味着浏览器和任何中间代理不可以存储该响应。
    no-cache 则表明你可以存储该响应,但是在下次再请求同样的资源时,不能直接将之前缓存的内容用于该请求。而应该先向服务发送一个HTTP请求对资源有效性进行验证,验证通过后才能使用该缓存,验证不通过则需要重新从服务器获取对应的资源.

    max-age:
    在指定的时间内,客户端不应该再向浏览器请求该响应,而应该使用本地缓存.
    示例:

    1
    max-age=3600

    以上示例值表示在第一次请求一个资源后的3600秒内,客户端不应该再向服务器请求该资源。
    Cache-Control示例:

    1
    Cache-Control: public, max-age=3600
  • Last-Modified(RFC7232
    其值为一个GMT日期(时间戳),指示请求的资源最后更改时间
    示例:

    1
    Last-Modified: Tue, 29 Oct 2019 13:15:46 GMT
  • ETag(RFC7232
    ETag 全称为 entity-tag,是请求的文件的一个标识符,当文件被修改时,该标识符也会发生变化(关于如何生成该标识符,这里不进行展开描述)。
    当响应头中返回此头部时,浏览器可以先将其保存起来,下次请求同样的资源时,可以先将之前保存的 ETag 发送给服务器进行验证。如果服务器上对应的资源没有发生改变,则返回304状态码,客户端收到304状态码,则继续使用该资源的缓存。如果服务器上对应的资源确实发生了变化,则服务器会返回200状态码并重新发送新的资源到客户端。
    ETag 可以被认为是 Last-Modified 的补充。后者只能以修改时间来检测请求的文件是否被更改了,而前者则可以检测请求的文件的内容是否被更改了,更加精确。
    示例:

    1
    ETag: "xyzzy"

常见的HTTP请求头如下:

  • If-None-Match(RFC7232
    验证缓存的资源的 ETag 是否和服务器上的资源的 ETag 相匹配。匹配时,服务器返回304状态码。否则服务器返回200状态码,并向客户端发送请求的资源文件。
    示例:

    1
    If-None-Match: "xyzzy"

    这里的 “xyzzy” 对应首次请求时响应头中的 ETag 的值
    只有在之前的响应头中返回了 ETag 时,请求头中才可能出现 If-None-Match

  • If-Modified-Since(RFC7232
    验证缓存的资源在一个给定的时间后是否被修改过,此给定的时间来自服务器返回的 Last-Modified 响应头。如果资源在此时间后被修改过了,服务器会返回304状态码。否则服务器将返回200状态码,并向客户端发送请求的资源文件。
    示例:

    1
    If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

    只有在之前的响应头中返回了 Last-Modified 时,请求头中才可能出现 If-Modified-Since

  • If-Modified-Since与If-None-Match的优先级
    参考 RFC7232#section-3.3 中的一段话:

    A recipient MUST ignore If-Modified-Since if the request contains an
    If-None-Match header field; the condition in If-None-Match is
    considered to be a more accurate replacement for the condition in
    If-Modified-Since

    翻译一下就是:当一个请求中包含一个 If-None-Match 请求头时,必须忽略掉 If-Modified-Since 请求头。因为 If-None-Match 中指定的条件被认为比 If-Modified-Since 中指定的条件更加精确。
    即:If-None-Match 优先级是高于 If-Modified-Since 的。


实际应用

在Web应用中,js、css、图片文件一般被认为是不会经常发生变更的资源。

因此我们会在它们对应的HTTP请求中返回一个 Cache-Control: public, max-age=n 响应头。表示希望浏览器缓存这些资源,并在指定的时间 n 以内不要去请求服务器,而是使用本地缓存。因而达到降低服务器负载,节约宽带的目的。

同时还应设置 ETag 响应头,此时 ETag 响应头的作用是:当超过 max-age 指定的时间后,浏览器就会从服务器获取最新的文件。然而此时如果该文件存在 ETag 时,浏览器会先发送一个验证性请求,即在请求头中设置 If-None-Match 请求头,如果服务器检测发现请求的文件的 ETag 并没有发生改变时,就会返回304状态码,避免传输整个没有更改过的文件。