node中的快取機制

Macchiato發表於2019-03-04

快取是node開發中一個很重要的概念,它應用在很多地方,例如:瀏覽器有快取、DNS有快取、包括伺服器也有快取。

一、快取作用

那快取是為了做什麼呢?

1.為了提高速度,提高效率。

2.減少資料傳輸,節省網費。

3.減少伺服器的負擔,提高網站效能。

4.加快客戶端載入網頁的速度。

二、快取分類

那快取有幾種策略呢?

強制快取:

1、概念:

客戶端訪問伺服器請求資源,請求成功之後客戶端會快取到本地,快取到本地之後,如果以後客戶端再請求該資源此時不需要請求伺服器了,直接訪問本地的就可以。

2、特點:

強制快取不需要與伺服器發生互動

3、客戶端訪問強制快取的流程圖解
node中的快取機制

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)客戶端第一次發請求

node中的快取機制

客戶端第一次請求資料,發現本地快取中沒有,就向伺服器發起請求,然後伺服器把請求的資料返回給客戶端,並和客戶端商量你要儲存到本地快取中的規則,即是否快取 快取時間 有沒有標示 最後修改時間等資訊。
2)客戶端第二次發請求

node中的快取機制

客戶端發起請請求
—>檢視本地的快取資料庫中是否有快取—> 沒有—> 向伺服器發起請求—>伺服器返回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`);
`);
// 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`)
}
複製程式碼
存在問題

都需要向伺服器端發請求與伺服器端發生互動

相關文章