每个浏览器都有一个自己的缓存区,使用缓存区的数据有诸多好处,减少冗余的数据传输,节省网络传输。减少服务器负担, 提高网站的性能。加快客户端加载网页的速度等,而这里指的缓存,指代的静态文件的缓存,动态数据缓存需要走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());
  7. 完整代码:

    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....');
    })

对比缓存

  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)
  8. Last-Modify & If-Modified-Since

  9. 完整代码:

    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....');
    })

指纹 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)
  9. ETag & If-None-Match

  10. 完整代码:

    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();

最新文章

  1. 297. Serialize and Deserialize Binary Tree
  2. #技塑人生# windows2008无法远程— 注册表缺失键值导致高级防火墙服务异常
  3. 史上最全的Excel数据编辑处理技巧(转)
  4. linux虚拟主机wdcp系列教程之四
  5. [设计模式]NetworkManagementService中的观察者模式
  6. XML中 添加或修改时 xmlns="" 怎么删除
  7. OPPO通过AWS节约大量成本提供海外服务
  8. Android 关于 OnScrollListener 事件顺序次数的简要分析
  9. PAT (Advanced Level) 1014. Waiting in Line (30)
  10. 13. Roman to Integer【leetcode】
  11. 浅谈Java接口
  12. The Apache Tomcat installation at this directory is version 8.5.40. A Tomcat 8.0 installation is expected.
  13. php使用root用户启动
  14. BZOJ1007: [HNOI2008]水平可见直线(单调栈)
  15. Go-day07
  16. CTAP: Complementary Temporal Action Proposal Generation (ECCV2018)
  17. 操作系统学习笔记(三) windows内存管理
  18. What To Do When MySQL Runs Out of Memory: Troubleshooting Guide
  19. UVA 10976 分数拆分【暴力】
  20. 学习mysql replication

热门文章

  1. 测试报告$\beta$
  2. 通过SQL注入获得网站后台用户密码
  3. 《前端运维》一、Linux基础--03Shell基础及补充
  4. java并发编程:深入了解synchronized
  5. MegaRAID BIOS设置阵列
  6. 二进制部署K8S-1基本概念
  7. Ansible_使用Ansible galaxy部署角色
  8. shell初学之nginx(域名)
  9. mysql基础之mysql双主(主主)架构
  10. Mybatis Mapper 映射文件(xxxMapper.xml)