手寫Node靜態資源服務器

想寫靜態資源服務器,首先咱們須要知道如何建立一個http服務器,它的原理是什麼javascript

http服務器是繼承自tcp服務器 http協議是應用層協議,是基於TCP的html

http的原理是對請求和響應進行了包裝,當客戶端鏈接上來以後先觸發connection事件,而後能夠屢次發送請求,每次請求都會觸發request事件java

let server = http.createServer();
let url = require('url');
server.on('connection', function (socket) {
    console.log('客戶端鏈接 ');
});
server.on('request', function (req, res) {
    let { pathname, query } = url.parse(req.url, true);
    let result = [];
    req.on('data', function (data) {
        result.push(data);
    });
    req.on('end', function () {
        let r = Buffer.concat(result);
        res.end(r);
    })
});
server.on('close', function (req, res) {
    console.log('服務器關閉 ');
});
server.on('error', function (err) {
    console.log('服務器錯誤 ');
});
server.listen(8080, function () {
    console.log('server started at http://localhost:8080');
});
複製代碼
  • req 表明客戶端的鏈接,server服務器把客戶端的請求信息進行解析,而後放在req上面
  • res 表明響應,若是但願向客戶端迴應消息,須要經過 res
  • reqres都是從socket來的,先監聽socketdata事件,而後等事件發生的時候,進行解析,解析出請頭對象,再建立請求對象,再根據請求對象建立響應對象
  • req.url 獲取請求路徑
  • req.headers 請求頭對象

接下來咱們對一些核心功能進行講解node

深入理解並實現壓縮和解壓

爲何要壓縮呢?有什麼好處?算法

  • 可使用zlib模塊進行壓縮及解壓縮處理,壓縮文件之後能夠減小體積,加快傳輸速度和節約帶寬代碼

壓縮和解壓縮對象都是transform轉換流,繼承自duplex雙工流便可讀可寫流數據庫

  • zlib.createGzip:返回Gzip流對象,使用Gzip算法對數據進行壓縮處理
  • zlib.createGunzip:返回Gzip流對象,使用Gzip算法對壓縮的數據進行解壓縮處理
  • zlib.createDeflate:返回Deflate流對象,使用Deflate算法對數據進行壓縮處理
  • zlib.createInflate:返回Deflate流對象,使用Deflate算法對數據進行解壓縮處理

實現壓縮和解壓promise

由於壓縮我文件可能很大也可能很小,因此爲了提升處理速度,咱們用流來實現瀏覽器

let fs = require("fs");
let path = require("path");
let zlib = require("zlib");
function gzip(src) {
  fs
    .createReadStream(src)
    .pipe(zlib.createGzip())
    .pipe(fs.createWriteStream(src + ".gz"));
}
gzip(path.join(__dirname,'msg.txt'));
function gunzip(src) {
  fs
    .createReadStream(src)
    .pipe(zlib.createGunzip())
    .pipe(
      fs.createWriteStream(path.join(__dirname, path.basename(src, ".gz")))
    );
}
gunzip(path.join(__dirname, "msg.txt.gz"));

複製代碼
  • gzip方法用於實現壓縮
  • gunzip方法用於實現解壓
  • 其中文件msg.txt是同級目錄
  • 爲何須要這麼寫:gzip(path.join(__dirname,'msg.txt'));
  • 由於console.log(process.cwd());打印出當前工做目錄是根目錄,並非文件所在目錄,若是這麼寫gzip('msg.txt');找不到文件就會報錯
  • basename 從一個路徑中獲得文件名,包括擴展名的,能夠傳一個擴展名參數,去掉擴展名
  • extname 獲取擴展名
  • 壓縮的格式和解壓的格式須要對上,不然會報錯

有些時候咱們拿到的字符串不是一個流,那怎麼解決呢緩存

let zlib=require('zlib');
let str='hello';
zlib.gzip(str,(err,buffer)=>{
    console.log(buffer.length);
    zlib.unzip(buffer,(err,data)=>{
        console.log(data.toString());
    })
});
複製代碼
  • 有可能壓縮後的內容比原來還大,要是內容太少的話,壓縮也沒什麼意義了
  • 文本壓縮的效果會好一點,由於有規律

在http中應用壓縮和解壓 下面實現這樣一個功能,如圖:bash

客戶端向服務器發起請求的時候,會經過accept-encoding(好比:Accept-Encoding:gzip,default)告訴服務器我支持的解壓縮的格式

  • 服務器端須要根據Accept-Encoding顯示的格式進行壓縮,沒有的格式就不能壓縮,由於瀏覽器沒法解壓
  • 若是客戶端須要的Accept-Encoding中的格式服務端沒有,也沒法實現壓縮
let http = require("http");
let path = require("path");
let url = require("url");
let zlib = require("zlib");
let fs = require("fs");
let { promisify } = require("util");
let mime = require("mime");
//把一個異步方法轉成一個返回promise的方法
let stat = promisify(fs.stat);
http.createServer(request).listen(8080);
async function request(req, res) {
  let { pathname } = url.parse(req.url); 
  let filepath = path.join(__dirname, pathname); 
  // fs.stat(filepath,(err,stat)=>{});如今不這麼寫了,異步的處理起來比較麻煩
  try {
    let statObj = await stat(filepath);
    res.setHeader("Content-Type", mime.getType(pathname));
    let acceptEncoding = req.headers["accept-encoding"];
    if (acceptEncoding) {
      if (acceptEncoding.match(/\bgzip\b/)) {
       
        res.setHeader("Content-Encoding", "gzip");
        fs
          .createReadStream(filepath)
          .pipe(zlib.createGzip())
          .pipe(res);
      } else if (acceptEncoding.match(/\bdeflate\b/)) {
        res.setHeader("Content-Encoding", "deflate");
        fs
          .createReadStream(filepath)
          .pipe(zlib.createDeflate())
          .pipe(res);
      } else {
        fs.createReadStream(filepath).pipe(res);
      }
    } else {
      fs.createReadStream(filepath).pipe(res);
    }
  } catch (e) {
    res.statusCode = 404;
    res.end("Not Found");
  }
}

複製代碼
  • mime:經過文件的名稱、路徑拿到一個文件的內容類型, 能夠根據不一樣的文件內容類型返回不一樣的Content-Type
  • acceptEncoding:所有寫成小寫是爲了兼容不一樣的瀏覽器,node把全部的請求頭全轉成了小寫
  • filepath:獲得文件的絕對路徑
  • 啓動服務後,訪問http://localhost:8080/msg.txt 可看到結果

深入理解並實現緩存

爲何要緩存呢,緩存有什麼好處?

  • 減小了冗餘的數據傳輸,節省了網費。
  • 減小了服務器的負擔, 大大提升了網站的性能
  • 加快了客戶端加載網頁的速度

緩存的分類

強制緩存:

  • 強制緩存,在緩存數據未失效的狀況下,能夠直接使用緩存數據
  • 在沒有緩存數據的時候,瀏覽器向服務器請求數據時,服務器會將數據和緩存規則一併返回,緩存規則信息包含在響應header中

對比緩存:

  • 瀏覽器第一次請求數據時,服務器會將緩存標識與數據一塊兒返回給客戶端,客戶端將兩者備份至緩存數據庫中
  • 再次請求數據時,客戶端將備份的緩存標識發送給服務器,服務器根據緩存標識進行判斷,判斷成功後,返回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, true);
    //D:\vipcode\201801\20.cache\index.html
    let filepath = path.join(__dirname, pathname);
    fs.stat(filepath, (err, stat) => {
        if (err) {
            return sendError(req, res);
        } else {
            let ifModifiedSince = req.headers['if-modified-since'];
            let LastModified = stat.ctime.toGMTString();
            if (ifModifiedSince == LastModified) {
                res.writeHead(304);
                res.end('');
            } else {
                return send(req, res, filepath, stat);
            }
        }
    });
}).listen(8080);
function sendError(req, res) {
    res.end('Not Found');
}
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);
}
複製代碼

這種方式有不少缺陷

  • 某些服務器不能精確獲得文件的最後修改時間, 這樣就沒法經過最後修改時間來判斷文件是否更新了
  • 某些文件的修改很是頻繁,在秒如下的時間內進行修改.Last-Modified只能精確到秒。
  • 一些文件的最後修改時間改變了,可是內容並未改變。 咱們不但願客戶端認爲這個文件修改了
  • 若是一樣的一個文件位於多個CDN服務器上的時候內容雖然同樣,修改時間不同

ETag

ETag是根據實體內容生成的一段hash字符串,能夠標識資源的狀態 資源發生改變時,ETag也隨之發生變化。 ETag是Web服務端產生的,而後發給瀏覽器客戶端

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);
    fs.stat(filepath, (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.digest('hex');
                let etag = `${stat.size}`;
                if (ifNoneMatch == etag) {
                    res.writeHead(304);
                    res.end('');
                } else {
                    return send(req, res, filepath, etag);
                }
            });

        }
    });
}).listen(8080);
function sendError(req, res) {
    res.end('Not Found');
}
function send(req, res, filepath, etag) {
    res.setHeader('Content-Type', mime.getType(filepath));
   
    res.setHeader('ETag', etag);
    fs.createReadStream(filepath).pipe(res);

}
複製代碼
  • 客戶端想判斷緩存是否可用能夠先獲取緩存中文檔的ETag,而後經過If-None-Match發送請求給Web服務器詢問此緩存是否可用。
  • 服務器收到請求,將服務器的中此文件的ETag,跟請求頭中的If-None-Match相比較,若是值是同樣的,說明緩存仍是最新的,Web服務器將發送304 Not Modified響應碼給客戶端表示緩存未修改過,可使用。
  • 若是不同則Web服務器將發送該文檔的最新版本給瀏覽器客戶端

實現強制緩存

  • 把資源緩存在客戶端,若是客戶端再次須要此資源的時候,先獲取到緩存中的數據,看是否過時,若是過時了。再請求服務器
  • 若是沒過時,則根本不須要向服務器確認,直接使用本地緩存便可
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));
    res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());
    res.setHeader('Cache-Control', 'max-age=30');
    fs.createReadStream(filepath).pipe(res);
}
複製代碼
  • 瀏覽器會將文件緩存到Cache目錄,第二次請求時瀏覽器會先檢查Cache目錄下是否含有該文件,若是有,而且還沒到Expires設置的時間,即文件尚未過時,那麼此時瀏覽器將直接從Cache目錄中讀取文件,而再也不發送請求
  • Expires是服務器響應消息頭字段,在響應http請求時告訴瀏覽器在過時時間前瀏覽器能夠直接從瀏覽器緩存取數據
  • Cache-ControlExpires的做用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據仍是從新發請求到服務器取數據,若是同時設置的話,其優先級高於Expires

下面開始寫靜態服務器 首先建立一個http服務,配置監聽端口

let http = require('http');
 let server = http.createServer();
        server.on('request', this.request.bind(this));
        server.listen(this.config.port, () => {
            let url = `http://${this.config.host}:${this.config.port}`;
            debug(`server started at ${chalk.green(url)}`);
        });
複製代碼

下面寫個靜態文件服務器 先取到客戶端想說的文件或文件夾路徑,若是是目錄的話,應該顯示目錄下面的文件列表

async request(req, res) {
        let { pathname } = url.parse(req.url);
        if (pathname == '/favicon.ico') {
            return this.sendError('not found', req, res);
        }
        let filepath = path.join(this.config.root, pathname);
        try {
            let statObj = await stat(filepath);
            if (statObj.isDirectory()) {
                let files = await readdir(filepath);
                files = files.map(file => ({
                    name: file,
                    url: path.join(pathname, file)
                }));
                let html = this.list({
                    title: pathname,
                    files
                });
                res.setHeader('Content-Type', 'text/html');
                res.end(html);
            } else {
                this.sendFile(req, res, filepath, statObj);
            }
        } catch (e) {
            debug(inspect(e));
            this.sendError(e, req, res);
        }
    }
    
    sendFile(req, res, filepath, statObj) {
        if (this.handleCache(req, res, filepath, statObj)) return;
        res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
        let encoding = this.getEncoding(req, res);
        let rs = this.getStream(req, res, filepath, statObj);

        if (encoding) {
            rs.pipe(encoding).pipe(res);
        } else {
            rs.pipe(res);
        }
    }
複製代碼

支持斷點續傳

getStream(req, res, filepath, statObj) {
        let start = 0;
        let end = statObj.size - 1;
        let range = req.headers['range'];
        if (range) {
            res.setHeader('Accept-Range', 'bytes');
            res.statusCode = 206;
            let result = range.match(/bytes=(\d*)-(\d*)/);
            if (result) {
                start = isNaN(result[1]) ? start : parseInt(result[1]);
                end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
            }
        }
        return fs.createReadStream(filepath, {
            start, end
        });
    }
複製代碼

支持對比緩存,經過etag的方式

handleCache(req, res, filepath, statObj) {
        let ifModifiedSince = req.headers['if-modified-since'];
        let isNoneMatch = req.headers['is-none-match'];
        res.setHeader('Cache-Control', 'private,max-age=30');
        res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString());
        let etag = statObj.size;
        let lastModified = statObj.ctime.toGMTString();
        res.setHeader('ETag', etag);
        res.setHeader('Last-Modified', lastModified);
        if (isNoneMatch && isNoneMatch != etag) {
            return fasle;
        }
        if (ifModifiedSince && ifModifiedSince != lastModified) {
            return fasle;
        }
        if (isNoneMatch || ifModifiedSince) {
            res.writeHead(304);
            res.end();
            return true;
        } else {
            return false;
        }
    }
複製代碼

支持文件壓縮

getEncoding(req, res) {
        let acceptEncoding = req.headers['accept-encoding'];
        if (/\bgzip\b/.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'gzip');
            return zlib.createGzip();
        } else if (/\bdeflate\b/.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'deflate');
            return zlib.createDeflate();
        } else {
            return null;
        }
    }
複製代碼

編譯模板,獲得一個渲染的方法,而後傳入實際數據數據就能夠獲得渲染後的HTML了

編譯模板,獲得一個渲染的方法,而後傳入實際數據數據就能夠獲得渲染後的HTML了
function list() {
    let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
    return handlebars.compile(tmpl);
}
複製代碼

這樣一個簡單的靜態服務器就完成了,其中包含了靜態文件服務,實現緩存,實現斷點續傳,分塊獲取,實現壓縮的功能

相關文章
相關標籤/搜索