Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浏览器缓存 #57

Open
lovelmh13 opened this issue May 19, 2021 · 0 comments
Open

浏览器缓存 #57

lovelmh13 opened this issue May 19, 2021 · 0 comments

Comments

@lovelmh13
Copy link
Owner

lovelmh13 commented May 19, 2021

说到浏览器的缓存,一下子就能想到两个词:「强缓存」和「协商缓存」。

但是这两个词比较有误导性,在 RFC 7234 中并没有出现这两个词,而是提到了「新鲜度」和「验证」。这其实就是常听到的「强缓存」和「协商缓存」。所以在后面的文章中,就尽量不使用「强缓存」和「协商缓存」了。

The goal of caching in HTTP/1.1 is to significantly improve performance by reusing a prior response message to satisfy a current request. A stored response is considered "fresh", as defined in Section 4.2, if the response can be reused without "validation" (checking with the origin server to see if the cached response remains valid for this request). A fresh response can therefore reduce both latency and network overhead each time it is reused. When a cached response is not fresh, it might still be reusable if it can be freshened by validation (Section 4.3) or if the origin is unavailable (Section 4.2.4).

在 HTTP/1.1 中,缓存通过复用一个之前的响应消息来满足当前的请求,其目的是显著提升性能。一个已存储的响应,如果它在不需要“验证”的情况就可以用来复用,那么,它被认为是“新鲜的fresh”(章节 4.2)。所谓“验证validation”,是指和源服务器一起检查,看看这个已存储的响应是否仍然有效于这个请求。因此,每一次复用一个新鲜的响应的时候,都可以减少双方的延迟以及网络开销。当一个已存储的响应不再新鲜,它可能仍旧可以获得复用,如果它可以通过验证(章节 4.3)而唤发新鲜freshened,或者原始的已不可用(章节 4.2.4)。

译注:本文所述的“验证”指的是“新鲜度验证”,也可以叫作“过期时间验证”。

1. 新鲜度 / Freshness

新鲜的响应,是指没有过保鲜期的响应。同理,肯定还会有过期的响应,就是超过了保鲜期的响应。但是过期的响应并不一定意味着这个响应就是无效的,我们可以在之后对其进行「验证」,来决定是否要重新获取还是利用缓存。这个后面会再说。

如何确定响应的保鲜期呢?

要确定新鲜度,肯定就要有过期时间。过期时间可以通过两种方式来规定。一是设置 Expires,二是设置 Cache-Control: max-age

如果源服务器希望强制让缓存每次都去源服务器上进行校验,那么只需要给一个过期的过期时间就可以了。表明这个响应是陈旧的,遵从规范的缓存将会在复用一个陈旧的已缓存的响应来满足后续请求之前,正常地验证它。

Expires

Expires 是在 HTTP 1.0 中提出的。描述的是一个绝对时间,由服务器返回。但是受限于本地时间,如果本地时间被修改了,那么缓存可能也就会失效了。

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

Cache-Control

Cache-ControlHTTP/1.1 中出现的,
max-age 表示的是相对于请求的时间。
s-maxage 会覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。

 Cache-Control: max-age=315360000

在 RFC7234 中提到:

主要的缓存 key 是由请求方法request method以及目标 URI 组成的。但是,因为如今普遍使用的 HTTP 缓存通常都被限制为只对 GET 的响应进行缓存,因此,许多缓存简单地拒绝其他方法,并只使用 URI 作为主要的缓存 key。

Cache-Control 被设置为 public 时,是可以响应 POST 等请求的。

public
表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST 。)

还有一个要注意的:no-cache。当设置为 no-cache 后,并不意味着就不缓存了,而是在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(也就是俗称的协商缓存)。个人理解就是绕过了「新鲜度」而直接走到了「验证」这一步。

只有当设置为 no-store,才是真正的不走缓存,直接请求资源。

其他的属性可以参考 MDN

Pragma

Expires 一样是 HTTP 1.0 提出的。为了向后兼容。现在看,就会向后兼容了 Cache-Control

当同时有 PragmaCache-Control: max-age时,会覆盖掉 max-age

过期时间头的优先级

如果是共享缓存,那么 s-maxage 会覆盖 max-ageExpires
如果不是共享缓存,如果出现 max-age ,那么 Expries 会被忽略。
当同时有 PragmaCache-Control: max-age时,会覆盖掉 max-age

启发式新鲜度 / Heuristic Freshness

并不是所有的源服务器都会设置过期时间,所以在没有明确的提供一个过期时间时,缓存可以指派一个「启发式过期时间」。原理是利用一种使用其他头字段值来估算出一个看似合理的过期时间的算法。RFC 7234 规范并没有提供具体的算法,但是对它们的计算结果的最差情况实施了约束。

如果响应带有一个 Last-Modified 头字段(【RFC7232】章节 2.2),规范鼓励缓存所使用的启发式过期时间的值不超过那个头字段的时间以后的某个比例,通常这个比例会设置为 10%。

启发式仅能用于没有明确新鲜度的响应,并且要求以下情况之一:
这些响应的状态码被定义为默认是可缓存的(见【RFC7231】章节 6.1);
这些响应被明确标记为可缓存的(比如,带有一个 public 响应指定)。

注:有些文章里还会看见 “启发式缓存“ 这个说法,其实就是指这个启发式新鲜度。

验证 / Validation

当请求 URI 的时候,当缓存已经存储了这个 URI 对应的一个或多个响应,但缓存却不能使用它们之中的任何一个来满足这个请求(比如,因为它们都不再新鲜,或者没有一个可以选择的(Vary 头字段),这时缓存在转发这个请求的时候,用条件请求机制来给下一个入展服务器一个机会,来选择一个有效的已储存的响应来使用。在这个过程中会更新已经存储的元数据。或者干脆就用新的响应来替换这个缓存。这个过程称为对已存储的响应进行「验证」或者「重新验证」。也就是常听说的 “协商缓存”。

如何验证

通过验证器进行验证
在验证阶段,缓存会发送一个或多个前提条件头字段来包含「验证器元数据」。

验证器有两个:

Last-Modified / If-Modified-Since

Last-Modified 响应头里的时间戳就是其中一个验证器。被 If-Modified-Since 请求头来验证响应。

ETag / If-None-Match

ETag 响应头里的实体标签是另一个验证器。被 If-None-Match 请求头来验证。

image

优先级

ETag 的优先级要高于 Last-Modified

具体为什么要用ETag,主要出于下面几种情况考虑:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间。

处理接收到的验证请求(状态码)

一个请求,如果它包含有一个 If-None-Match 头字段(【RFC7232】章节 3.2),则表明:客户端想通过与缓存选定的任何已存储的响应相比较的方式,来验证客户端所拥有的一个或多个已存储的响应。如果它的字段值是 "*",或者它的字段值是一个实体标签列表并且至少其中之一个与已选的已存储的响应selected stored response相匹配,那么,缓存接收端 应当 生成一个 304 (Not Modified) 响应(使用那个已选的已存储的响应的元数据),而不是发送那个已存储的响应。

一个请求,其包含有一个 If-None-Match 头字段,字段值是实体标签的一个列表,当一个缓存决定为这个请求重新验证它所拥有的已存储的响应的时候,缓存 可以 将所接收到的列表与它所拥有的已存储的响应集合(新鲜的或陈旧的)的实体标签列表合并成一个列表,然后将这个联合列表代替到将要转发的请求的 If-None-Match 头字段值进行发送。如果某个已存储的响应包含的仅为表示形式的一部分内容partial content,那么,缓存 禁止 将这个响应的实体标签包含进联合列表中,除非这个请求是一个范围请求且请求的范围可以被那个已存储的部分响应完全满足。如果转发请求后返回的是一个 304 (Not Modified) 响应,并且这个响应有一个 ETag 头字段值,字段值有一个实体标签没有出现在客户端所请求的实体标签列表中,那么,客户端 必须 通过复用这个实体标签所对应的已存储的响应来为这个客户生成一个 200 (OK) 响应,同时将刚才返回的那个 304 响应的元数据更新到这个 200 响应中去(章节 4.3.4)。

一个请求,如果没有出现 If-None-Match 头字段,而是包含有一个 If-Modified-Since 头字段(【RFC7232】章节 3.3),则表明:客户端想通过修改日期modification date来验证客户端所拥有的一个或多个已存储的响应。缓存接收端 应当 生成一个 304 (Not Modified) 响应(使用那个已选的已存储的响应的元数据),如果以下其中一种情况为真的话:
已选的已存储的响应有一个 Last-Modified 头字段,其值早于或等于条件式(即 If-Modified-Since)的时间戳。
已选的已存储的响应没有出现 Last-Modified 头字段,但它有一个 Date 头字段,其值早于或等于条件式的时间戳。
已选的已存储的响应既没有出现 Last-Modified 头字段,也没有出现 Date 头字段,但缓存记录到接收到这个响应的时间早于或等于条件式的时间戳。

200: Expires / Cache-Control 存失效时,返回新的资源文件
200(from cache): Expires/Cache-Control 两者都存在,未过期,Cache-Control 优先 Expires时,浏览器从本地获取资源成功
304(Not Modified ):Last-modified/Etag没有过期时,服务端返回状态码304
但是!但是!
现在的200(from cache)已经变成了from disk cache(磁盘缓存)和from memory cache(内存缓存)两种
打开chrome控制台看一下网络请求就知道了
image

整体流程如下:

图来自参考 1 的文章:

image

疑问

用 node 写一个返回 html 的代码:

const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs')

http.createServer((req, res) => {
    let { pathname } = url.parse(req.url, true);
    console.log(pathname)
    let abs = path.join(__dirname, pathname);
    // res.setHeader('Expires', new Date(Date.now() + 10000).toGMTString());
    res.setHeader('Cache-Control', 'max-age=3')
    fs.stat(path.join(__dirname, pathname), (err, stat) => {
        if(err) {
            res.statusCode = 404;
            res.end('not found')
            return
        }
        if(stat.isFile()) {
            fs.createReadStream(abs).pipe(res)
        }
    })
}).listen(7711)

返回一个 html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  哈哈哈哈哈哈哈哈哈哈哈
  <img src="./1.png" alt="">
</body>
</html>

为什么每次刷新只有 img 走了缓存,html 没有走呢?
image

如果让 html 走缓存的话,需要另重新打开一个页签,才能命中 html 的缓存:
image

这是为什么呢?

参考:

缓存(二)——浏览器缓存机制:强缓存、协商缓存
RFC 7234

@lovelmh13 lovelmh13 changed the title 浏览器缓存 浏览器缓存(未写完) May 19, 2021
@lovelmh13 lovelmh13 changed the title 浏览器缓存(未写完) 浏览器缓存 May 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant