# HTTP的缓存机制

  缓存机制无处不在,有客户端缓存(cookie、localstorage等),服务端缓存(session),代理服务器缓存等。在HTTP中具有缓存功能的是浏览器缓存。 HTTP缓存作为web性能优化的重要手段,对于从事web开发的朋友有重要的意义。思维导图如下:

暂无图片

# 1、缓存的分类

  缓存分为强制缓存和协商缓存

# 1、 强制缓存

  当本地缓存中含有请求的数据且(及缓存时间还未过期)时,客户端直接从本地缓存中获取数据。当本地缓存没有所请求的数据时,客户端的才会从服务端获取数据。
  对于强制缓存,服务器响应的 header 中会用两个字段来表明——Expires和Cache-Control

# 1)Expires

Exprires 的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,ExpiresHTTP1.0 的产物,故现在大多数使用 Cache-Control 替代。

//在服务器端设置过期时间,表示10秒后到期
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString())
1
2

  通过res.setHeader(Expires,time) 会在响应头中添加一个 Expires字段,表示过期时间。在过期时间内,获取文件时响应头里的 Status Code都是 200 OK (from memory cache)。添加 Expires响应头的结果如下所示。

暂无图片
# 2)Cache-Control

Cache-Control 有很多属性,不同的属性代表的意义也不同。

字段 含义
private 客户端可以缓存
public 客户端和代理服务器都可以缓存
max-age=t 缓存内容将在t秒后失效
no-cache 需要使用协商缓存来验证缓存数据
no-store 所有内容都不会缓存。
must-revalidate 缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用
//在服务器端设置过期时间,表示20秒后过期
res.setHeader('Cache-Control', 'max-age=20')
1
2

  通过在响应头中添加Cache-Control,在过期时间内,获取文件时响应头里的 Status Code都是 200 OK (from memory cache)。添加Cache-Control后的结果如下图所示。

暂无图片

# 2、协商缓存

  又称对比缓存,客户端会先从本地缓存中获取到一个缓存数据的标识(ETag), 然后服务器检查该ETag 证是否失效,如果没有失效服务端会返回 304(只有响应头,没有响应体),此时客户端直接从缓存中获取所请求的数据,如果标识失效,服务端会返回更新后的数据。
协商缓存又分两种情况。

# 情况1:根据Last-Modified来进行协商缓存(HTTP 1.0)

Last-Modified: 服务器在响应请求时,会告诉浏览器资源的最后修改时间

if-Modified-Since: 浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有 If-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回304和响应报文头,浏览器只需要从缓存中获取信息即可。
  从字面上看,就是说:从某个时间节点算起,是否文件被修改了。
  1、如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
  2、如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified(没有响应体)。 测试demo如下

const updateTime = () => {
    return new Date().toUTCString();
}
const http = require('http');
let lastModified
http.createServer((req, res) => {
    const { url } = req;
    if ("/" === url) {
        res.setHeader('Content-Type', 'text/html')
        res.end(`
            <html>
                html file load time${updateTime()}
                <script src="main.js"></script>
            </html>

        `)
    } else if (url === "/main.js") {
        const content = `document.writeln('<br> js file load time${updateTime()}')`;
        //判断文件是否被修改,即
        if (req.headers['if-modified-since'] && (new Date(req.headers['if-modified-since']).getTime() === new Date(lastModified).getTime())) {
            console.log('缓存命中');
            res.statusCode = 304;
            res.end();
            return
        } else {
            res.setHeader('Cache-Control', 'no-cache')
            lastModified = new Date().toUTCString()
            res.setHeader('Last-modified', lastModified)
            res.statusCode = 200
            res.end(content)
        }

    } else if (url === "favicon.icon") {
        res.end("")
    }
}).listen(3000, () => {
    console.log("服务器启动在3000端口")
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

if-Unmodified-Since: 从字面上看, 就是说: 从某个时间点算起, 是否文件没有被修改。
  1、如果没有被修改:则开始`继续'传送文件: 服务器返回: 200 OK。
  2、如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误) 。

首次请求时,会在响应头里添加 last-modified,表示最后修改时间。第二次请求时,会在请求头中添加 If-Modified-Since,用于跟服务端的 last-modified的值进行对比。

暂无图片 暂无图片

# 情况2:根据ETag来进行协商缓存(HTTP 1.1)

Etag: EtagURLEntity Tag,用于标示 URL 对象是否改变,区分不同语言和Session等等。具体内部含义是使服务器控制的,就像Cookie那样。Etag由服务器端生成,然后服务器通过客户端发送过来的 (If-Match/If-None-Match) 这个条件判断请求来验证资源是否修改。
第一次请求
  1.客户端发起 HTTP GET 请求一个文件;
  2.服务器处理请求,返回文件内容和一堆Header,当然包括Etag(例如"2e681a-6-5d0448402")(假设服务器支持Etag生成和已经开启了Etag),状态码200。
第二次请求
  客户端发起 HTTP GET 请求一个文件,注意这个时候客户端同时发送一个If-None-Match 头,这个头的内容就是第一次请求时服务器返回的 Etag:2e681a-6-5d0448402
  服务器检查该ETag,并判断出该页面自上次客户端请求之后是否被修改,如果If-None-Match 为跟后台生成的Etag相同,则证明请求的文件没有修改,则响应header和空的body,浏览器直接从缓存中获取数据信息。返回状态码304。如果ETag被修改了,说明资源被改动过,则响应整个资源内容,返回状态码200。
  但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了。

  第一次请求时会在响应头里添加 Etag(4b1f0259b32390df6baf991c917efe716945af29),如下所示。在第二次请求时,请求头里会添加 If-None-Match: 4b1f0259b32390df6baf991c917efe716945af29,用于跟服务器端的Etag比较,看是否一样。

暂无图片 暂无图片

测试demo如下。

const updateTime = () => {
    return new Date().toUTCString();
}
const http = require('http');
let hash//定义全局的hash值
http.createServer((req, res) => {
    const { url } = req;
    if ("/" === url) {
        res.setHeader('Content-Type', 'text/html')
        res.end(`
            <html>
                html file load time${updateTime()}
                <script src="main.js"></script>
            </html>

        `)
    } else if (url === "/main.js") {
        const content = `document.writeln('<br> js file load time${updateTime()}')`;
        const crypto = require('crypto');
        //判断是否设置了Etag,如果设置了,且文件没有修改时
        if (req.headers['if-none-match'] && req.headers['if-none-match'] === hash) {
            res.statusCode = 304;
            res.end();
            return;
        } else {
            //这是没有设置Etag或者 文件修改了的情况
            hash = crypto.createHash('sha1').update(content).digest('hex');
            res.setHeader("Etag", hash);
            res.statusCode = 200
            res.end(content)
        }
    } else if (url === "favicon.icon") {
        res.end("")
    }
}).listen(3000, () => {
    console.log("服务器启动在3000端口")
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

判断缓存是否过期可以用以下一张图来概括 暂无图片

# 2、如果服务器同时设置了Cache-Control:max-age和Expires以及ETag(If-None-Match)、If-Modified-Since(Last Modified)时,怎么办?

  具体判断过程如下所示。

  • 当发送一个服务器请求时,浏览器首先会进行缓存过期判断。浏览器根据缓存过期时间判断缓存文件是否过期。
    情景一(Cache-Control等浏览器本地判断):
      若没有过期,则不向服务器发送请求,直接使用缓存中的结果,此时我们在浏览器控制台中可以看到 200 OK(from cache) ,此时的情况就是完全使用缓存,浏览器和服务器没有任何交互的
    情景二(服务器端判断):
      若已过期,则向服务器发送请求,此时请求中会带上设置的文件修改时间和Etag,然后进行资源更新判断。这要分两种情形进行判断。
    情形一: 若两种判断的结论都是文件没有被修改过,则服务器就不给浏览器发index.html的内容了,直接告诉它,文件没有被修改过,你用你那边的缓存吧—— 304 Not Modified, 此时浏览器就会从本地缓存中获取 index.html 的内容。
    情形二: 若文件修改时间和ETag判断有任意一个没有通过,则服务器会受理此次请求。并从服务器加载数据。

总结: 两类缓存机制可以同时存在,强制缓存的优先级高于协商缓存,当执行强制缓存时,如果Expires 字段对应的时间还未过期,则直接使用本地缓存数据,过期了再进行缓存协商 。 总而言之: 先在浏览器端判断缓存是否过期,没有过期则使用本地缓存(状态码为200 OK (from memory cache))。过期了再进行协商缓存,如果通过判断ETag 和文件最后修改时间,发现请求文件都没被修改过,则直接从本地缓存中获取数据(状态码为304 Not Modified )。如果有一个修改了都会从服务器重新加载数据(状态码为 200 Ok

Last Updated: 6/11/2024, 11:35:27 AM