node中的缓存机制

node中的缓存机制

缓存是node开发中一个很重要的概念,它应用在很多地方,例如浏览器有缓存、DNS有缓存、包括服务器也有缓存。

一、缓存作用

那缓存是为了做什么呢?

1.为了提高速度,提高效率。
2.减少数据传输,节省网费。
3.减少服务器的负担,提高网站性能。
4.加快客户端加载网页的速度。

二、缓存分类

那缓存有几种策略呢?

强制缓存:

1、概念:

客户端访问服务器请求资源,请求成功之后客户端会缓存到本地,缓存到本地之后,如果以后客户端再请求该资源此时不需要请求服务器了,直接访问本地的就可以。

2、特点:

强制缓存不需要与服务器发生交互

3、客户端访问强制缓存的流程图解
image

1)缓存命中
客户端请求数据,现在本地的缓存数据库中查找,如果本地缓存数据库中有该数据,且该数据没有失效。则取缓存数据库中的该数据返回给客户端。

2)缓存未命中
客户端请求数据,现在本地的缓存数据库中查找,如果本地缓存数据库中有该数据,且该数据失效。则向服务器请求该数据,此时服务器返回该数据和该数据的缓存规则返回给客户端,客户端收到该数据和缓存规则后,一起放到本地的缓存数据库中留存。以备下次使用。

4、如何实现强制缓存?

1、浏览器会将文件缓存到Cache目录,第二次请求时浏览器会先检查Cache目录下是否含有该文件,如果有,并且还没到Expires设置的时间,即文件还没有过期,那么此时浏览器将直接从Cache目录中读取文件,而不再发送请求
2、Expires是服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求,这是HTTP1.0的内容,现在浏览器均默认使用HTTP1.1,所以基本可以忽略
3、Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据,如果同时设置的话,其优先级高于Expires

把资源缓存在客户端,如果客户端再次需要此资源的时候,先获取到缓存中的数据,看是否过期,如果过期了。再请求服务器
如果没过期,则根本不需要向服务器确认,直接使用本地缓存即可

Cache-Control
private 客户端可以缓存
public 客户端和代理服务器都可以缓存
max-age=60 缓存内容将在60秒后失效
no-cache 需要使用对比缓存验证数据,强制向源服务器再次验证. 禁用强制缓存
no-store 所有内容都不会缓存,强制缓存和对比缓存都不会触发。兼用强制缓存和对比缓存å

/**
* 1. 第一次访问服务器的时候,服务器返回资源和缓存的标识,客户端则会把此资源缓存在本地的缓存数据库中。
* 2. 第二次客户端需要此数据的时候,要取得缓存的标识,然后去问一下服务器我的资源是否是最新的。
* 如果是最新的则直接使用缓存数据,如果不是最新的则服务器返回新的资源和缓存规则,客户端根据缓存规则缓存新的数据。
*/
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let crypto = require('crypto');
/**
* 强制缓存
* 把资源缓存在客户端,如果客户端再次需要此资源的时候,先获取到缓存中的数据,看是否过期,如果过期了。再请求服务器
* 如果没过期,则根本不需要向服务器确认,直接使用本地缓存即可
*/
http.createServer(function (req, res) {
   let { pathname } = url.parse(req.url, true);
   let filepath = path.join(__dirname, pathname);
   console.log(filepath);
   fs.stat(filepath, (err, stat) => {
       if (err) {
           return sendError(req, res);
       } else {
           send(req, res, filepath);
}
});
}).listen(8080);
function sendError(req, res) {
   res.end('Not Found');
}
function send(req, res, filepath) {
   res.setHeader('Content-Type', mime.getType(filepath));
   //expires指定了此缓存的过期时间,此响应头是1.0定义的,在1.1里面已经不再使用了
   res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());
   res.setHeader('Cache-Control', 'max-age=30');
   fs.createReadStream(filepath).pipe(res);
}

对比缓存:

1、概念:

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

2、特点:需要进行比较判断是否可以使用缓存
3、对比缓存流程图解

1)客户端第一次发请求


image

客户端第一次请求数据,发现本地缓存中没有,就向服务器发起请求,然后服务器把请求的数据返回给客户端,并和客户端商量你要保存到本地缓存中的规则,即是否缓存 缓存时间 有没有标示 最后修改时间等信息。
2)客户端第二次发请求


image

客户端发起请请求
--->查看本地的缓存数据库中是否有缓存---> 没有---> 向服务器发起请求--->服务器返回200和响应内容--->显示

--->查看本地的缓存数据库中是否有缓存---> 有 ---> 缓存没有过期(本地)---> 缓存中读取--->显示

--->查看本地的缓存数据库中是否有缓存---> 有 ---> 缓存已过期(本地)---> 本地的缓存中有没有Etag和Last-Modified --->有--->发给服务器对应的字段 if-none-match 和if-modified-since ---> 服务器策略。如果这两个字段和服务器上的这两个字段相同 ---> 说明数据没有更新--->返回304--->服务器从它的缓存库中获取到数据给客户端--->显示

--->查看本地的缓存数据库中是否有缓存---> 有 ---> 缓存已过期(本地)---> 本地的缓存中有没有Etag和Last-Modified --->有--->发给服务器对应的字段 if-none-match 和if-modified-since ---> 服务器策略。如果这两个字段和服务器上的这两个字段相同 --->说明数据有更新--->返回200--->重新获取--->显示

4、如何实现对比缓存?

/**

    1. 第一次访问服务器的时候,服务器返回资源和缓存的标识,客户端则会把此资源缓存在本地的缓存数据库中。
    1. 第二次客户端需要此数据的时候,要取得缓存的标识,然后去问一下服务器我的资源是否是最新的。
  • 如果是最新的则直接使用缓存数据,如果不是最新的则服务器返回新的资源和缓存规则,客户端根据缓存规则缓存新的数据。
    */
    我们通过标示字段来判断缓存中的数据是否有效
    这个标示有两种形式:
第一种是最后修改时间,Last-Modified

1、Last-Modified:响应时告诉客户端此资源的最后修改时间
2、If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向服务器请求时带上头If-Modified-Since。
3、服务器收到请求后发现有头If-Modified-Since则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应最新的资源内容并返回200状态码;
4、若最后修改时间和If-Modified-Since一样,说明资源没有修改,则响应304表示未更新,告知浏览器继续使用所保存的缓存文件。

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
// http://localhost:8080/index.html
http.createServer(function (req, res) {
    let {pathname} = url.parse(req.url);
    let filepath = path.join(__dirname,pathname);
    console.log(filepath);
    fs.stat(filepath,function (err, stat) {
          if (err) {
              return sendError(req,res)
          } else {
              // 再次请求的时候会问服务器自从上次修改之后有没有改过
              let ifModifiedSince = req.headers['if-modified-since'];
              console.log(req.headers);
              let LastModified = stat.ctime.toGMTString();
              console.log(LastModified);
              if (ifModifiedSince == LastModified) {
                  res.writeHead('304');
                  res.end('')
              } else {
                  return send(req,res,filepath,stat)
              }
          }
    })

}).listen(8080)

function send(req,res,filepath,stat) {
    res.setHeader('Content-Type', mime.getType(filepath));
    // 发给客户端之后,客户端会把此时间保存下来,下次再获取此资源的时候会把这个时间再发给服务器
    res.setHeader('Last-Modified', stat.ctime.toGMTString());
    fs.createReadStream(filepath).pipe(res)
}

function sendError(req,res) {
    res.end('Not Found')
}
最后修改时间存在问题

1、某些服务器不能精确得到文件的最后修改时间, 这样就无法通过最后修改时间来判断文件是否更新了。
2、某些文件的修改非常频繁,在秒以下的时间内进行修改. Last-Modified只能精确到秒。
3、一些文件的最后修改时间改变了,但是内容并未改变。 我们不希望客户端认为这个文件修改了。
4、如果同样的一个文件位于多个CDN服务器上的时候内容虽然一样,修改时间不一样。

第二种是Etag

ETag是实体标签的缩写,根据实体内容生成的一段hash字符串,可以标识资源的状态。当资源发生改变时,ETag也随之发生变化。 ETag是Web服务端产生的,然后发给浏览器客户端。

1、客户端想判断缓存是否可用可以先获取缓存中文档的ETag,然后通过If-None-Match发送请求给Web服务器询问此缓存是否可用。
2、服务器收到请求,将服务器的中此文件的ETag,跟请求头中的If-None-Match相比较,如果值是一样的,说明缓存还是最新的,Web服务器将发送304 Not Modified响应码给客户端表示缓存未修改过,可以使用。
3、如果不一样则Web服务器将发送该文档的最新版本给浏览器客户端

let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let mime = require('mime');
let crypto = require('let crypto = require(\'mime\');\n');
// http://localhost:8080/index.html
http.createServer(function (req, res) {
    let {pathname} = url.parse(req.url);
    let filepath = path.join(__dirname,pathname);
    console.log(filepath);
    fs.stat(filepath,function (err, stat) {
        if (err) {
            return sendError(req,res)
        } else {

            let ifNoneMatch = req.headers['if-none-match'];
            // 一、显然当我们的文件非常大的时候通过下面的方法就行不通来,这时候我们可以用流来解决,可以节约内存
            let out = fs.createReadStream(filepath);
            let md5 = crypto.createHash('md5');
            out.on('data',function (data) {
                md5.update(data)
            });
            out.on('end',function () {
                let etag = md5.update(content).digest('hex');
                // md5算法的特点 1. 相同的输入相同的输出 2.不同的输入不通的输出 3.不能根据输出反推输入 4.任意的输入长度输出长度是相同的
                if (ifNoneMatch == etag) {
                    res.writeHead('304');
                    res.end('')
                } else {
                    return send(req,res,filepath,stat, etag)
                }
            });
            
            // 二、再次请求的时候会问服务器自从上次修改之后有没有改过
            // fs.readFile(filepath,function (err, content) {
            //     let etag = crypto.createHash('md5').update(content).digest('hex');
            //     // md5算法的特点 1. 相同的输入相同的输出 2.不同的输入不通的输出 3.不能根据输出反推输入 4.任意的输入长度输出长度是相同的
            //     if (ifNoneMatch == etag) {
            //         res.writeHead('304');
            //         res.end('')
            //     } else {
            //         return send(req,res,filepath,stat, etag)
            //     }
            // };
            // 但是上面的一方案也不是太好,读一点缓存一点,文件非常大的话需要好长时间,而且我们的node不适合cup密集型,即不适合来做大量的运算,所以说还有好多其他的算法
            // 三、通过文件的修改时间减去文件的大小
            // let etag = `${stat.ctime}-${stat.size}`; // 这个也不是太好
            // if (ifNoneMatch == etag) {
            //     res.writeHead('304');
            //     res.end('')
            // } else {
            //     return send(req,res,filepath,stat, etag)
            // }
        }
    })

}).listen(8080)

function send(req,res,filepath,stat, etag) {
    res.setHeader('Content-Type', mime.getType(filepath));
    // 第一次服务器返回的时候,会把文件的内容算出来一个标示发送给客户端
    //客户端看到etag之后,也会把此标识符保存在客户端,下次再访问服务器的时候,发给服务器
    res.setHeader('Etag', etag);
    fs.createReadStream(filepath).pipe(res)
}

function sendError(req,res) {
    res.end('Not Found')
}
存在问题

都需要向服务器端发请求与服务器端发生交互

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容