用代碼來實踐Web緩存

Web緩存是能夠自動保存常見文檔副本的HTTP設備。當Web請求抵達緩存時,若是本地有「已緩存的副本」,就能夠從本地存儲設備而不是原始服務器中提取這個文檔。javascript

上面是《HTTP權威指南》中對Web緩存的定義,緩存的好處主要有如下幾點:html

  1. 減小了冗餘數據的傳輸;
  2. 減小了客戶端的網絡請求,也下降了原始服務器的壓力;
  3. 下降了時延,頁面加載更快。

總結一下就是省流量,省帶寬,還賊快。那麼緩存是如何工做的呢?客戶端和服務端是如何協調緩存的時效性的呢?下面咱們用代碼來一步一步揭曉緩存的工做原理。java

1、瀏覽器緩存

當咱們在瀏覽器地址欄敲入localhost:8080/test.txt並回車時,咱們是向指定的服務端發起對text.txt文件的請求,git

服務端在接收到這個請求以後,找到了這個文件並準備返回給客戶端,並經過設置Cache-ControlExpires兩個response header告訴客戶端這個文件要緩存下來,在過時以前別跟我要了。github

首先咱們看一下項目目錄:瀏覽器

|-- Cache
    |-- index.js
    |-- assets
        |-- index.html
        |-- test.txt

具體實現代碼以下:緩存

<!-- index.html -->
...
<a href="./test.txt">test.txt</a>
...
// index.js
const http = require('http');
const path = require('path');
const fs = require('fs');

http.createServer((req, res) => {
    const requestUrl = path.join(__dirname, '/assets', path.normalize(req.url));
    fs.stat(requestUrl, (err, stats) => {
        if (err || !stats.isFile) {
            res.writeHead(404, 'Not Found');
            res.end();
        } else {
            const readStream = fs.createReadStream(requestUrl);
            const maxAge = 10;
            const expireDate = new Date(
                new Date().getTime() + maxAge * 1000 
            ).toUTCString();
            res.setHeader('Cache-Control', `max-age=${maxAge}, public`);
            res.setHeader('Expires', expireDate);
            readStream.pipe(res);
        }
    });
}).listen(8080);

Cache-ControlExpires這個兩個response header又表明什麼意思呢?Cache-Control:max-age=500表示設置緩存存儲的最大週期爲500秒,超過這個時間緩存被認爲過時。Expires:Tue, 23 Feb 2021 01:23:48 GMT表示在Tue, 23 Feb 2021 01:23:48 GMT這個日期以後文檔過時。服務器

啓動server後,在瀏覽器訪問localhost:8080/index.html,這時是第一次訪問,沒有緩存,因此服務器返回完整的資源。網絡

咱們點擊超連接訪問test.txt測試

由於是第一次訪問,因此沒有緩存,這個時候咱們點擊返回按鈕回到index.html

發現不一樣了嗎?這個時候NetWork中Size已經變成了disk cache,說明命中了瀏覽器緩存,也就是強緩存,這個時候再點擊超連接訪問test.txt,若是在設置的過時時間10s之內,就能看到命中瀏覽器緩存,若是超過10s,就會從新從服務器獲取資源。

這裏說明一點,瀏覽器的前進後退按鈕會一直從緩存中讀取資源,而忽略設置的緩存規則。也就是說剛纔若是我從localhost:8080/test.txt頁面經過瀏覽器返回按鈕回到localhost:8080/index.html頁面,會發現無論過多久Network都是disk cache,一樣再點擊瀏覽器前進按鈕進入localhost:8080/test.txt頁面,哪怕超過設置的過時時間也仍是from disk cache。

注意Cache-Control的優先級大於Expires,由於時差緣由還有服務端時間和客戶端時間可能不一致會致使Expires判斷緩存有效性不許確。可是Expires兼容http1.0,Cache-Control兼容到http1.1,因此通常仍是兩個都設置。

2、協商緩存

上面咱們設置過緩存時限後,若是緩存過時了怎麼辦呢?你可能會說,過時了就從新從服務端獲取資源啊。可是也有可能緩存時間過時了,可是資源並無變化,因此咱們還要引入其餘的策略來處理這種狀況,那就是協商緩存也就是弱緩存。

咱們梳理一下協商緩存的流程:

當服務端第一次返回資源時,除了設置Cache-ControlExpires響應頭以外,還會設置Last-Modified(資源更新時間)和ETag(資源摘要或資源版本)兩個響應頭,分別表明資源的最近一次變動時間和實體標籤。當客戶端沒有命中強緩存時,會從新像服務端發起請求,並攜帶If-modified-SinceIf-None-Match兩個請求頭,服務端拿到這兩個請求頭會跟以前設置的Last-ModifiedETag做比較,若是不匹配,說明緩存不可用,從新返回資源,反之說明緩存有效,返回304響應碼,告知緩存能夠繼續使用,並更新緩存有效時間。

下面咱們看一下具體代碼實現:

const http = require('http');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

// 生成entity digest
function generateDigest(requestUrl) {
    let hash = '2jmj7l5rSw0yVb/vlWAYkK/YBwk';
    let len = 0;
    fs.readFile(requestUrl, (err, data) => {
        if (err) {
            console.error(error);
            throw new Error(err);
        } else {
            len = Buffer.byteLength(data, 'utf8');
            hash = crypto
                .createHash('sha1')
                .update(data, 'utf-8')
                .digest('base64')
                .substring(0, 27);
        }
    });
    return '"' + len.toString(16) + '-' + hash + '"';
}

// 響應文件
function responseFile(requestUrl, stats, res) {
    const readStream = fs.createReadStream(requestUrl);
    const maxAge = 10;
    const expireDate = new Date(
        new Date().getTime() + maxAge * 1000
    ).toUTCString();
    res.setHeader('Cache-Control', `max-age=${maxAge}, public`);
    res.setHeader('Expires', expireDate);
    res.setHeader('Last-Modified', stats.mtime);
    res.setHeader('ETag', generateDigest(requestUrl));
    readStream.pipe(res);
}

// 判斷新鮮度
function isFresh(requestUrl, stats, req) {
    const ifModifiedSince = req.headers['if-modified-since'];
    const ifNoneMatch = req.headers['if-none-match'];

    if (!ifModifiedSince && !ifNoneMatch) {
        //若是沒有相應的請求頭,應該返回全新的資源
        return false;
    } else if (ifNoneMatch && ifNoneMatch !== generateDigest(requestUrl)) {
        //若是ETag不匹配(資源內容發生改變),表示緩存不新鮮
        return false;
    } else if (ifModifiedSince && ifModifiedSince !== stats.mtime.toString()) {
        //若是資源更新時間不匹配,表示緩存不新鮮
        return false;
    }
    return true;
}

http.createServer((req, res) => {
    const requestUrl = path.join(__dirname, '/assets', path.normalize(req.url));

    fs.stat(requestUrl, (err, stats) => {
        if (err || !stats.isFile) {
            res.writeHead(404, 'Not Found');
            res.end();
        } else {
            if (isFresh(requestUrl, stats, req)) {
                // 緩存新鮮,告知客戶端沒有緩存可用,不返回響應實體
                res.writeHead(304, 'Not Modified');
                res.end();
            } else {
                // 緩存不新鮮,從新返回資源
                responseFile(requestUrl, stats, res);
            }
        }
    });
}).listen(8080);

從代碼中能夠看到ETagLast-Modified都是用於協商緩存的校驗的,ETag基於實體標籤,通常能夠經過版本號,或者資源摘要來指定;Last-Modified則是基於資源的最後修改時間。

這時訪問localhost:8080/test.txt文件,當命中強緩存後,等待10s鍾,再次訪問,服務器返回304,而非200,代表協商緩存生效。

此時修改test.txt文件,再次訪問,服務器返回200,頁面展現最新的test.txt文件內容。

總結一下:

  1. ETag能更精確地判斷資源到底有沒有變化,且優先級高於Last-Modified
  2. 基於摘要實現的ETag相對較慢,更佔資源;
  3. Last-Modified精確到秒,對亞秒級的資源更新的緩存新鮮度判斷無能爲力;
  4. ETag兼容到http1.1Last-Modified兼容到http1.0

注意:本文中經過超連接訪問test.txt是由於,若是直接在地址欄訪問該資源,瀏覽器會在request headers中設置cache-control:max-age=0,這樣永遠不會命中瀏覽器緩存。

本文測試瀏覽器:Chrome 版本 88.0.4324.192

參考:

  1. 《HTTP權威指南》
  2. HTTP緩存
  3. etag
相關文章
相關標籤/搜索