中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

手寫Node靜態資源服務器的實現方法

發布時間:2020-10-14 20:18:10 來源:腳本之家 閱讀:182 作者:October 欄目:web開發

想寫靜態資源服務器,首先我們需要知道如何創建一個http服務器,它的原理是什么

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

http的原理是對請求和響應進行了包裝,當客戶端連接上來之后先觸發connection事件,然后可以多次發送請求,每次請求都會觸發request事件

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

  1. req 代表客戶端的連接,server服務器把客戶端的請求信息進行解析,然后放在req上面
  2. res 代表響應,如果希望向客戶端回應消息,需要通過 res
  3. req和res都是從socket來的,先監聽socket的data事件,然后等事件發生的時候,進行解析,解析出請頭對象,再創建請求對象,再根據請求對象創建響應對象
  4. req.url 獲取請求路徑
  5. req.headers 請求頭對象

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

深刻理解并實現壓縮和解壓

為什么要壓縮呢?有什么好處?

可以使用zlib模塊進行壓縮及解壓縮處理,壓縮文件以后可以減少體積,加快傳輸速度和節約帶寬代碼

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

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

實現壓縮和解壓

因為壓縮我文件可能很大也可能很小,所以為了提高處理速度,我們用流來實現

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"));
  1. gzip方法用于實現壓縮
  2. gunzip方法用于實現解壓
  3. 其中文件msg.txt是同級目錄
  4. 為什么需要這么寫:gzip(path.join(__dirname,'msg.txt'));
  5. 因為console.log(process.cwd());打印出當前工作目錄是根目錄,并不是文件所在目錄,如果這么寫gzip('msg.txt');找不到文件就會報錯
  6. basename 從一個路徑中得到文件名,包括擴展名的,可以傳一個擴展名參數,去掉擴展名
  7. extname 獲取擴展名
  8. 壓縮的格式和解壓的格式需要對上,否則會報錯

有些時候我們拿到的字符串不是一個流,那怎么解決呢

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中應用壓縮和解壓

下面實現這樣一個功能,如圖:

手寫Node靜態資源服務器的實現方法

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

  1. 服務器端需要根據Accept-Encoding顯示的格式進行壓縮,沒有的格式就不能壓縮,因為瀏覽器無法解壓
  2. 如果客戶端需要的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");
 }
}
  1. mime:通過文件的名稱、路徑拿到一個文件的內容類型, 可以根據不同的文件內容類型返回不同的Content-Type
  2. acceptEncoding:全部寫成小寫是為了兼容不同的瀏覽器,node把所有的請求頭全轉成了小寫
  3. filepath:得到文件的絕對路徑
  4. 啟動服務后,訪問http://localhost:8080/msg.txt 可看到結果

深刻理解并實現緩存

為什么要緩存呢,緩存有什么好處?

  1. 減少了冗余的數據傳輸,節省了網費。
  2. 減少了服務器的負擔, 大大提高了網站的性能
  3. 加快了客戶端加載網頁的速度

緩存的分類

強制緩存:

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

手寫Node靜態資源服務器的實現方法

手寫Node靜態資源服務器的實現方法

對比緩存:

瀏覽器第一次請求數據時,服務器會將緩存標識與數據一起返回給客戶端,客戶端將二者備份至緩存數據庫中

再次請求數據時,客戶端將備份的緩存標識發送給服務器,服務器根據緩存標識進行判斷,判斷成功后,返回304狀態碼,通知客戶端比較成功,可以使用緩存數據

手寫Node靜態資源服務器的實現方法

手寫Node靜態資源服務器的實現方法

兩類緩存的區別和聯系

強制緩存如果生效,不需要再和服務器發生交互,而對比緩存不管是否生效,都需要與服務端發生交互
兩類緩存規則可以同時存在,強制緩存優先級高于對比緩存,也就是說,當執行強制緩存的規則時,如果緩存生效,直接使用緩存,不再執行對比緩存規則

實現對比緩存

實現對比緩存一般是按照以下步驟:

第一次訪問服務器的時候,服務器返回資源和緩存的標識,客戶端則會把此資源緩存在本地的緩存數據庫中。

第二次客戶端需要此數據的時候,要取得緩存的標識,然后去問一下服務器我的資源是否是最新的。

如果是最新的則直接使用緩存數據,如果不是最新的則服務器返回新的資源和緩存規則,客戶端根據緩存規則緩存新的數據

實現對比緩存一般有兩種方式

通過最后修改時間來判斷緩存是否可用

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

這種方式有很多缺陷

  1. 某些服務器不能精確得到文件的最后修改時間, 這樣就無法通過最后修改時間來判斷文件是否更新了
  2. 某些文件的修改非常頻繁,在秒以下的時間內進行修改.Last-Modified只能精確到秒。
  3. 一些文件的最后修改時間改變了,但是內容并未改變。 我們不希望客戶端認為這個文件修改了
  4. 如果同樣的一個文件位于多個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-Control與Expires的作用一致,都是指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據還是重新發請求到服務器取數據,如果同時設置的話,其優先級高于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了

function list() {
  let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
  return handlebars.compile(tmpl);
}

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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

麻阳| 温泉县| 万源市| 鲜城| 开封县| 铁岭县| 鲁山县| 昌邑市| 富宁县| 呼图壁县| 简阳市| 炎陵县| 博爱县| 永和县| 保定市| 九寨沟县| 含山县| 新龙县| 遂川县| 阿拉善盟| 安顺市| 延寿县| 宿迁市| 班戈县| 保德县| 什邡市| 怀来县| 略阳县| 星座| 泽州县| 蛟河市| 平顶山市| 龙海市| 石棉县| 常德市| 中山市| 嵊泗县| 呼玛县| 渭源县| 布拖县| 垦利县|