http强制缓存、协商缓存、指纹ETag详解

目录

每个浏览器都有一个自己的缓存区,使用缓存区的数据有诸多好处,减少冗余的数据传输,节省网络传输。减少服务器负担, 提高网站的性能。加快客户端加载网页的速度等,而这里指的缓存,指代的静态文件的缓存,动态数据缓存需要走redis。今天我们使用node搭建服务,简单演示一下几种缓存的设置及配合使用。

缓存分为disk cachememory cache两种,浏览器自行处理,代码层面无法控制。而我们一般在用的时候都是在nginx层做处理,但核心是一样的,都是设置header

简单说一下,Chrome浏览器的缓存文件位置在哪,感兴趣的同学可以自己找一找:

  1. chrome浏览器地址栏中输入:chrome://version/

  2. 找到个人资料路径(我的是):C:\Users\Lenovo\AppData\Local\Google\Chrome\User Data\Default

  3. 计算机中找到对应的目录,可以在这个目录下查看到CacheCode Cache目录,这个就是缓存文件目录进入对应的目录,可以进行手动删除

实操目录及步骤

初始化package.jsonnpm init -y

下载第三方模块:npm i mime

.
│
└─cache                
    ├─node_modules             
    ├─public            // 静态文件目录              
        ├─1.js          // 请求的文件资源  
        ├─index.html    
    ├─1.cache.js          // 强制缓存 完整代码案例
    ├─2.cache.js          // 协商缓存 完整代码案例
    ├─3.cache.js          // 指纹对比 完整代码案例

缓存分类

  1. 强制缓存:直接缓存至浏览器中,不会再次向服务器发送请求;
  2. 对比缓存:也叫协商缓存,客服各执一份文件修改时间,相互对比,若相同用客户端缓存
  3. 指纹Etag:为解决对比缓存存在的一些问题,客服各执一份文件签名,相互对比,若相同用客户端缓存

强制缓存

  1. 服务器与浏览器约定一个缓存的最大存活时间,如10s,那么10s内,浏览器请求相同的资源便不会在请求服务器,会默认走浏览器的缓存区,并且响应码依然为200

  2. 如果返回的是一个html,其中又引用了其他资源,还会继续向服务器发送请求。

  3. 不对首次访问的路径做处理,也就是第一次访问时,不走强制缓存的,必然会请求到服务器端,因为如果连首页都走缓存了,那么在断网或服务器宕机的情况下也可以访问该网站,显然是不合理的

  4. 可以根据不同的文件后缀,设置不同的强制缓存的时间

  5. 在缓存数据生效期间,可以直接使用缓存数据,在没有缓存数据时,浏览器向服务器请求数据,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应头中。

  6. 强制缓存常用的两种响应头设置

    // 10s 表示当前时间 + 10s,属于相对时间 (用于新版浏览器)
    res.setHeader('Cache-Control', 'max-age=10'); 
    
    // 设置 绝对时间 (用于旧版浏览器或IE老版本 或 http1.0)
    // 设置header的值只能是数字,字符串或数组,不能为对象,new Date()返回的是对象,所以需要转一下。
    res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString()); 
    
    
  1. 完整代码:

    const http = require('http')
    const url = require('url')
    const path = require('path');
    const fs = require('fs');
    const mime = require('mime');
    
    const server = http.createServer((req, res) => {
      let { pathname } = url.parse(req.url, true)
      let filepath = path.join(__dirname, 'public', pathname);   // 访问路径拼接 public
    
      // 10s 表示当前时间 + 10s,属于相对时间 (用于新版浏览器)
      res.setHeader('Cache-Control', 'max-age=10'); 
      // 设置 绝对时间 (用于旧版浏览器或IE老版本 或 http1.0)
      // 设置header的值只能是数字,字符串或数组,不能为对象,new Date()返回的是对象,所以需要转一下。
      res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString()); 
    
      fs.stat(filepath, function (err, statObj) {
        if (err) {    // 获取文件信息报错,则则响应 404
          res.statusCode = 404;
          res.end('Not Found!')
        } else {
          // 如果是文件,设置对应类型的响应头,并返响应文件内容
          if (statObj.isFile()) {
            res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
            fs.createReadStream(filepath).pipe(res);
          } else {
            // 如果是目录,需要找目录下的 index.html
            let htmlPath = path.join(filepath, 'index.html')   // 拼接路径
            fs.access(htmlPath, function (err) {
              if (err) {    // 查看文件的可访问性,如不能访问则响应 404
                res.statusCode = 404;
                res.end('Not Found!')
              } else {
                res.setHeader('Content-Type', 'text/html;charset=utf-8');
                fs.createReadStream(htmlPath).pipe(res)
              }
            })
          }
        }
      })
    });
    
    // 服务监听 3000 端口
    server.listen(3000, function () {
      console.log('server is running....');
    })
    
    
![](https://upload-images.jianshu.io/upload_images/26303134-c8aae1659079bcec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

对比缓存

  1. 浏览器首次请求资源时,服务器会将缓存标识(文件修改时间)与资源一同返回给浏览器。

  2. 再次请求时,客户端请求头会携带缓存标识(If-Modified-Since),并在服务端对比两个时间

  3. 若相等,直接返回304状态码,读取浏览器的缓存中对应缓存文件;

  4. 若不相等,返回最新内容,并给文件设置新的修改时间。

  5. 对比缓存不管是否生效,都需要与服务端发生交互

  6. 强制缓存和对比缓存可以配合使用,如10s内强制缓存,超过10s走对比缓存,同时在设置10s的强制缓存

  7. 响应头设置

    // no-cache: 需要使用对比缓存验证数据,会向服务器发送请求,且数据会存到浏览器的缓存中 
    res.setHeader('Cache-Control', 'no-cache'); 
    
    // 设置响应头,文件的最后修改时间
    res.setHeader('Last-Modified',ctime)
    
    
  1. Last-Modify & If-Modified-Since

  1. 完整代码:

    const http = require('http')
    const url = require('url')
    const path = require('path');
    const fs = require('fs');
    const mime = require('mime');
    
    const server = http.createServer((req, res) => {
      let { pathname } = url.parse(req.url, true)
      let filepath = path.join(__dirname, 'public', pathname);
      // 强制缓存和对比缓存配合使用,10s内走强制缓存,超过10s会走对比缓存,同时在设置10s的强制缓存
      // res.setHeader('Cache-Control', 'max-age=10');  
    
      res.setHeader('Cache-Control', 'no-cache'); 
    
      fs.stat(filepath, function (err, statObj) {
        if (err) {
          res.statusCode = 404;
          res.end('Not Found!')
        } else {
          // 如果是文件
          if (statObj.isFile()) {
            const ctime = statObj.ctime.toGMTString();
            // 判断请求头存储的时间与服务器端文件的最后修改时间是否相等
            if(req.headers['if-modified-since'] === ctime){
              res.statusCode = 304;  // 设置响应状态码,浏览器默认会自动解析,从缓存中读取对应文件
              res.end();  // 表示此时服务器没有响应结果
            }else{
              // 设置响应头,文件的最后修改时间
              res.setHeader('Last-Modified',ctime)
              // 设置对应类型的响应头,并返响应文件内容
              res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
              fs.createReadStream(filepath).pipe(res);
            }
          } else {
            // 如果是目录,需要找目录下的index.html
            let htmlPath = path.join(filepath, 'index.html')    // 拼接路径
            fs.access(htmlPath, function (err) {
              if (err) {  // 查看文件的可访问性,如不能访问则响应 404
                res.statusCode = 404;
                res.end('Not Found!')
              } else {
                res.setHeader('Content-Type', 'text/html;charset=utf-8');
                fs.createReadStream(htmlPath).pipe(res)
              }
            })
          }
        }
      })
    });
    
    // 服务监听 3000 端口
    server.listen(3000, function () {
      console.log('server is running....');
    })
    
    
![](https://upload-images.jianshu.io/upload_images/26303134-9218c65fbab0aa60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

指纹 Etag

在讲指纹之前,还需要介绍一下摘要算法加密算法crypto 是node中提供好的用于加密的模块,各种摘要算法和加密算法。

摘要及加密算法

MD5:常见的MD5算法,也叫hash算法或者摘要算法,具有以下特点:

  • 不能反解,不可逆,
  • 相同的内容,摘要出的结果相同
  • 不同的内容,摘要出长度是相同的
  • 不同的内容,摘要的结果完全不同 (也称雪崩效应,有一点不一样,结果就完全不一样)
  • 网上在线解密MD5其实只是通常意义的撞库
  • 撞库不叫解密,为了安全,可以将一个md5值多次加密,一般三次以上就无法破解了md5(md5(md5(xxx))),

sah1/sha256:加盐算法,是真正的加密算法,设定一个盐值(秘钥)内容一致,盐值不同,结果不同

const crypto = require('crypto');
/** md5*/
//                                    摘要的内容     摘要的格式
let r1 = crypto.createHash('md5').update('abcd').digest('base64');
//                                   分开摘要, 如果内部使用了流,可以读一点摘要一点
let r2 = crypto.createHash('md5').update('a').update('b').update('cd').digest('base64');
console.log(r1, r2);

/** sha256 */
const crypto = require('crypto');
let r3 = crypto.createHmac('sha256','n').update('ab')..update('cd').digest('base64');
let r4 = crypto.createHmac('sha256','h').update('a')..update('bcd').digest('base64');
console.log(r3, r4);

进入正题,对比缓存使用的最后修改时间方案也存在一定的问题:

  • 某些服务器不能精确得到文件的最后修改时间, 这样就无法通过最后修改时间来判断文件是否更新了。
  • 某些文件的修改非常频繁,在秒以下的时间内进行多次修改,而Last-Modified只能精确到秒。
  • 一些文件的最后修改时间改变了,但是内容并未改变(典型吃了吐)。 因此不希望被认为是修改。
  • 如果同样的一个文件位于多个CDN服务器,内容虽然一样,修改时间不一样。

Etag的出现,可以在一定程度上解决这个问题,但不能说完全解决,他也存在他的问题,接下来分析一下他的实现原理:

  1. ETag(实体标签),根据摘要算法将实体内容生成的一段hash字符串,文件改变,ETag也随之改变

  2. 但是对于大文件,不会直接全量比对,可以用文件的大小,开头、或某一段生成一个指纹

  3. 浏览器首次请求资源时,服务器会将ETag与资源一同返回给浏览器。

  4. 再次请求时,客户端请求头会携带签名标识(If-None-Match),并在服务端对比两个签名

  5. 若相等,直接返回304状态码,读取浏览器的缓存中对应缓存文件;

  6. 若不相等,返回最新内容,并给文件设置新的修改时间。

  7. ETag不管是否生效,都需要与服务端发生交互

  8. 响应头设置

    // no-cache: 需要使用对比缓存验证数据,会向服务器发送请求,且数据会存到浏览器的缓存中 
    res.setHeader('Cache-Control', 'no-cache'); 
    
    // 设置响应头,文件的最后修改时间
    res.setHeader('Last-Modified',ctime)
    
    
  1. ETag & If-None-Match

  1. 完整代码:
```
const http = require('http')
const url = require('url')
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const crypto = require('crypto');

const server = http.createServer((req, res) => {
  let { pathname } = url.parse(req.url, true)
  let filepath = path.join(__dirname, 'public', pathname);

  fs.stat(filepath, function (err, statObj) {
    if (err) {
      res.statusCode = 404;
      res.end('Not Found!')
    } else {
    // 如果是文件
      if (statObj.isFile()) {
        let content = fs.readFileSync(filepath);
        let etag = crypto.createHash('md5').update(content).digest('base64');
        // 判断请求头存储的签名与服务端文件的生成的签名是否相等
        if(req.headers['if-none-match'] === etag){
          res.statusCode = 304; // 设置响应状态码,浏览器默认会自动解析,从缓存中读取对应文件
          res.end()  // 表示此时服务器没有响应结果
        }else{
          // 设置响应头,签名
          res.setHeader('Etag',etag)
          // 设置对应类型的响应头,并返响应文件内容
          res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
          fs.createReadStream(filepath).pipe(res);
        }
      } else {
        // 如果是目录,需要找目录下的index.html
        let htmlPath = path.join(filepath, 'index.html')   // 拼接路径
        fs.access(htmlPath, function (err) {  
          if (err) {   // 查看文件的可访问性,如不能访问则响应 404
            res.statusCode = 404;
            res.end('Not Found!')
          } else {
            res.setHeader('Content-Type', 'text/html;charset=utf-8');
            fs.createReadStream(htmlPath).pipe(res)
          }
        })
      }
    }
  })
});

// 服务监听 3000 端口
server.listen(3000, function () {
  console.log('server is running....');
})

```

缓存总结

  • 强制缓存如果生效,不会再和服务器发生交互而对比缓存不管是否生效,都需要与服务端发生交互

  • 缓存规则可以同时存在,强制缓存优先级高于对比缓存,也就是说,当强制缓存规则生效时,直接使用缓存,不再执行对比缓存规则

  • 可以设置不同的匹配规则,采用不同的缓存方式

  • 重要代码:

    // 第一次发送文件,先设置强制缓存,在执行强制缓存时,默认不会执行对比缓存,因为不走服务器
    res.setHeader('Cache-Control','max-age=10');
    res.setHeader('Expires',new Date(Date.now() + 10 * 1000).toGMTString());
    
    // 每次强制缓存时间到了,就会走对比缓存,然后在变成强制缓存
    const lastModified = statObj.ctime.toGMTString();
    const etag = crypto.createHash('md5').update(readFileSync(requestFile)).digest('base64');
    res.setHeader('Last-Modified',lastModified);
    res.setHeader('Etag',etag);
    
    let ifModifiedSince = req.headers['if-modified-since'];
    let ifNoneMatch = req.headers['if-none-match'];
    // 如果文件修改时间不一样,就直接返回最新的
    if(lastModified !== ifModifiedSince){    // 有可能时间一样,但是内容不一样
        return createReadStream(requestFile).pipe(res);;
    }
    if(etag !== ifNoneMatch){ // 一般情况,指纹生成不会是根据文件全量生成,有可能只是根据文件大小等
        return createReadStream(requestFile).pipe(res);;
    }
    res.statusCode = 304;
    return res.end();
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 在工作中,前端代码打包之后的生成的静态资源就要发布到静态服务器上,这时候就要做对这些静态资源做一些运维配置,其中,...
    0月阅读 109,095评论 35 132
  • 前言 http的缓存是老生常谈的问题,基本面试专用的,看到的文章挺多的,但都是一些原理性的文章,基本没有真正实践过...
    mongofeng阅读 227评论 0 0
  • 现代的浏览器以及服务器都支持压缩技术,唯一需要协商的是采用的压缩算法。 为了选择采用的压缩算法,浏览器和服务器之间...
    编程小世界阅读 560评论 0 0
  • 浏览器的强缓存和协商缓存 原文链接:https://github.com/yiliang114/Blog/issu...
    易良同学阅读 738评论 0 0
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,095评论 2 7