手寫Node靜態(tài)資源服務(wù)器的實(shí)現(xiàn)方法
想寫靜態(tài)資源服務(wù)器,首先我們需要知道如何創(chuàng)建一個(gè)http服務(wù)器,它的原理是什么
http服務(wù)器是繼承自tcp服務(wù)器 http協(xié)議是應(yīng)用層協(xié)議,是基于TCP的
http的原理是對(duì)請(qǐng)求和響應(yīng)進(jìn)行了包裝,當(dāng)客戶端連接上來(lái)之后先觸發(fā)connection事件,然后可以多次發(fā)送請(qǐng)求,每次請(qǐng)求都會(huì)觸發(fā)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('服務(wù)器關(guān)閉 '); }); server.on('error', function (err) { console.log('服務(wù)器錯(cuò)誤 '); }); server.listen(8080, function () { console.log('server started at http://localhost:8080'); });
- req 代表客戶端的連接,server服務(wù)器把客戶端的請(qǐng)求信息進(jìn)行解析,然后放在req上面
- res 代表響應(yīng),如果希望向客戶端回應(yīng)消息,需要通過 res
- req和res都是從socket來(lái)的,先監(jiān)聽socket的data事件,然后等事件發(fā)生的時(shí)候,進(jìn)行解析,解析出請(qǐng)頭對(duì)象,再創(chuàng)建請(qǐng)求對(duì)象,再根據(jù)請(qǐng)求對(duì)象創(chuàng)建響應(yīng)對(duì)象
- req.url 獲取請(qǐng)求路徑
- req.headers 請(qǐng)求頭對(duì)象
接下來(lái)我們對(duì)一些核心功能進(jìn)行講解
深刻理解并實(shí)現(xiàn)壓縮和解壓
為什么要壓縮呢?有什么好處?
可以使用zlib模塊進(jìn)行壓縮及解壓縮處理,壓縮文件以后可以減少體積,加快傳輸速度和節(jié)約帶寬代碼
壓縮和解壓縮對(duì)象都是transform轉(zhuǎn)換流,繼承自duplex雙工流即可讀可寫流
- zlib.createGzip:返回Gzip流對(duì)象,使用Gzip算法對(duì)數(shù)據(jù)進(jìn)行壓縮處理
- zlib.createGunzip:返回Gzip流對(duì)象,使用Gzip算法對(duì)壓縮的數(shù)據(jù)進(jìn)行解壓縮處理
- zlib.createDeflate:返回Deflate流對(duì)象,使用Deflate算法對(duì)數(shù)據(jù)進(jìn)行壓縮處理
- zlib.createInflate:返回Deflate流對(duì)象,使用Deflate算法對(duì)數(shù)據(jù)進(jìn)行解壓縮處理
實(shí)現(xiàn)壓縮和解壓
因?yàn)閴嚎s我文件可能很大也可能很小,所以為了提高處理速度,我們用流來(lái)實(shí)現(xiàn)
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方法用于實(shí)現(xiàn)壓縮
- gunzip方法用于實(shí)現(xiàn)解壓
- 其中文件msg.txt是同級(jí)目錄
- 為什么需要這么寫:gzip(path.join(__dirname,'msg.txt'));
- 因?yàn)閏onsole.log(process.cwd());打印出當(dāng)前工作目錄是根目錄,并不是文件所在目錄,如果這么寫gzip('msg.txt');找不到文件就會(huì)報(bào)錯(cuò)
- basename 從一個(gè)路徑中得到文件名,包括擴(kuò)展名的,可以傳一個(gè)擴(kuò)展名參數(shù),去掉擴(kuò)展名
- extname 獲取擴(kuò)展名
- 壓縮的格式和解壓的格式需要對(duì)上,否則會(huì)報(bào)錯(cuò)
有些時(shí)候我們拿到的字符串不是一個(gè)流,那怎么解決呢
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()); }) });
有可能壓縮后的內(nèi)容比原來(lái)還大,要是內(nèi)容太少的話,壓縮也沒什么意義了
文本壓縮的效果會(huì)好一點(diǎn),因?yàn)橛幸?guī)律
在http中應(yīng)用壓縮和解壓
下面實(shí)現(xiàn)這樣一個(gè)功能,如圖:
客戶端向服務(wù)器發(fā)起請(qǐng)求的時(shí)候,會(huì)通過accept-encoding(比如:Accept-Encoding:gzip,default)告訴服務(wù)器我支持的解壓縮的格式
- 服務(wù)器端需要根據(jù)Accept-Encoding顯示的格式進(jìn)行壓縮,沒有的格式就不能壓縮,因?yàn)闉g覽器無(wú)法解壓
- 如果客戶端需要的Accept-Encoding中的格式服務(wù)端沒有,也無(wú)法實(shí)現(xiàn)壓縮
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"); //把一個(gè)異步方法轉(zhuǎn)成一個(gè)返回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)=>{});現(xiàn)在不這么寫了,異步的處理起來(lái)比較麻煩 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:通過文件的名稱、路徑拿到一個(gè)文件的內(nèi)容類型, 可以根據(jù)不同的文件內(nèi)容類型返回不同的Content-Type
- acceptEncoding:全部寫成小寫是為了兼容不同的瀏覽器,node把所有的請(qǐng)求頭全轉(zhuǎn)成了小寫
- filepath:得到文件的絕對(duì)路徑
- 啟動(dòng)服務(wù)后,訪問http://localhost:8080/msg.txt 可看到結(jié)果
深刻理解并實(shí)現(xiàn)緩存
為什么要緩存呢,緩存有什么好處?
- 減少了冗余的數(shù)據(jù)傳輸,節(jié)省了網(wǎng)費(fèi)。
- 減少了服務(wù)器的負(fù)擔(dān), 大大提高了網(wǎng)站的性能
- 加快了客戶端加載網(wǎng)頁(yè)的速度
緩存的分類
強(qiáng)制緩存:
強(qiáng)制緩存,在緩存數(shù)據(jù)未失效的情況下,可以直接使用緩存數(shù)據(jù)
在沒有緩存數(shù)據(jù)的時(shí)候,瀏覽器向服務(wù)器請(qǐng)求數(shù)據(jù)時(shí),服務(wù)器會(huì)將數(shù)據(jù)和緩存規(guī)則一并返回,緩存規(guī)則信息包含在響應(yīng)header中
對(duì)比緩存:
瀏覽器第一次請(qǐng)求數(shù)據(jù)時(shí),服務(wù)器會(huì)將緩存標(biāo)識(shí)與數(shù)據(jù)一起返回給客戶端,客戶端將二者備份至緩存數(shù)據(jù)庫(kù)中
再次請(qǐng)求數(shù)據(jù)時(shí),客戶端將備份的緩存標(biāo)識(shí)發(fā)送給服務(wù)器,服務(wù)器根據(jù)緩存標(biāo)識(shí)進(jìn)行判斷,判斷成功后,返回304狀態(tài)碼,通知客戶端比較成功,可以使用緩存數(shù)據(jù)
兩類緩存的區(qū)別和聯(lián)系
強(qiáng)制緩存如果生效,不需要再和服務(wù)器發(fā)生交互,而對(duì)比緩存不管是否生效,都需要與服務(wù)端發(fā)生交互
兩類緩存規(guī)則可以同時(shí)存在,強(qiáng)制緩存優(yōu)先級(jí)高于對(duì)比緩存,也就是說(shuō),當(dāng)執(zhí)行強(qiáng)制緩存的規(guī)則時(shí),如果緩存生效,直接使用緩存,不再執(zhí)行對(duì)比緩存規(guī)則
實(shí)現(xiàn)對(duì)比緩存
實(shí)現(xiàn)對(duì)比緩存一般是按照以下步驟:
第一次訪問服務(wù)器的時(shí)候,服務(wù)器返回資源和緩存的標(biāo)識(shí),客戶端則會(huì)把此資源緩存在本地的緩存數(shù)據(jù)庫(kù)中。
第二次客戶端需要此數(shù)據(jù)的時(shí)候,要取得緩存的標(biāo)識(shí),然后去問一下服務(wù)器我的資源是否是最新的。
如果是最新的則直接使用緩存數(shù)據(jù),如果不是最新的則服務(wù)器返回新的資源和緩存規(guī)則,客戶端根據(jù)緩存規(guī)則緩存新的數(shù)據(jù)
實(shí)現(xiàn)對(duì)比緩存一般有兩種方式
通過最后修改時(shí)間來(lái)判斷緩存是否可用
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)); //發(fā)給客戶端之后,客戶端會(huì)把此時(shí)間保存起來(lái),下次再獲取此資源的時(shí)候會(huì)把這個(gè)時(shí)間再發(fā)回服務(wù)器 res.setHeader('Last-Modified', stat.ctime.toGMTString()); fs.createReadStream(filepath).pipe(res); }
這種方式有很多缺陷
- 某些服務(wù)器不能精確得到文件的最后修改時(shí)間, 這樣就無(wú)法通過最后修改時(shí)間來(lái)判斷文件是否更新了
- 某些文件的修改非常頻繁,在秒以下的時(shí)間內(nèi)進(jìn)行修改.Last-Modified只能精確到秒。
- 一些文件的最后修改時(shí)間改變了,但是內(nèi)容并未改變。 我們不希望客戶端認(rèn)為這個(gè)文件修改了
- 如果同樣的一個(gè)文件位于多個(gè)CDN服務(wù)器上的時(shí)候內(nèi)容雖然一樣,修改時(shí)間不一樣
ETag
ETag是根據(jù)實(shí)體內(nèi)容生成的一段hash字符串,可以標(biāo)識(shí)資源的狀態(tài)
資源發(fā)生改變時(shí),ETag也隨之發(fā)生變化。 ETag是Web服務(wù)端產(chǎn)生的,然后發(fā)給瀏覽器客戶端
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發(fā)送請(qǐng)求給Web服務(wù)器詢問此緩存是否可用。
服務(wù)器收到請(qǐng)求,將服務(wù)器的中此文件的ETag,跟請(qǐng)求頭中的If-None-Match相比較,如果值是一樣的,說(shuō)明緩存還是最新的,Web服務(wù)器將發(fā)送304 Not Modified響應(yīng)碼給客戶端表示緩存未修改過,可以使用。
如果不一樣則Web服務(wù)器將發(fā)送該文檔的最新版本給瀏覽器客戶端
實(shí)現(xiàn)強(qiáng)制緩存
把資源緩存在客戶端,如果客戶端再次需要此資源的時(shí)候,先獲取到緩存中的數(shù)據(jù),看是否過期,如果過期了。再請(qǐng)求服務(wù)器
如果沒過期,則根本不需要向服務(wù)器確認(rèn),直接使用本地緩存即可
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); }
瀏覽器會(huì)將文件緩存到Cache目錄,第二次請(qǐng)求時(shí)瀏覽器會(huì)先檢查Cache目錄下是否含有該文件,如果有,并且還沒到Expires設(shè)置的時(shí)間,即文件還沒有過期,那么此時(shí)瀏覽器將直接從Cache目錄中讀取文件,而不再發(fā)送請(qǐng)求
Expires是服務(wù)器響應(yīng)消息頭字段,在響應(yīng)http請(qǐng)求時(shí)告訴瀏覽器在過期時(shí)間前瀏覽器可以直接從瀏覽器緩存取數(shù)據(jù)
Cache-Control與Expires的作用一致,都是指明當(dāng)前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數(shù)據(jù)還是重新發(fā)請(qǐng)求到服務(wù)器取數(shù)據(jù),如果同時(shí)設(shè)置的話,其優(yōu)先級(jí)高于Expires
下面開始寫靜態(tài)服務(wù)器
首先創(chuàng)建一個(gè)http服務(wù),配置監(jiān)聽端口
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)}`); });
下面寫個(gè)靜態(tài)文件服務(wù)器
先取到客戶端想說(shuō)的文件或文件夾路徑,如果是目錄的話,應(yīng)該顯示目錄下面的文件列表
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); } }
支持?jǐn)帱c(diǎn)續(xù)傳
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 }); }
支持對(duì)比緩存,通過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; } }
編譯模板,得到一個(gè)渲染的方法,然后傳入實(shí)際數(shù)據(jù)數(shù)據(jù)就可以得到渲染后的HTML了
function list() { let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8'); return handlebars.compile(tmpl); }
這樣一個(gè)簡(jiǎn)單的靜態(tài)服務(wù)器就完成了,其中包含了靜態(tài)文件服務(wù),實(shí)現(xiàn)緩存,實(shí)現(xiàn)斷點(diǎn)續(xù)傳,分塊獲取,實(shí)現(xiàn)壓縮的功能
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 實(shí)戰(zhàn)node靜態(tài)文件服務(wù)器的示例代碼
- Node.js靜態(tài)服務(wù)器的實(shí)現(xiàn)方法
- nodejs構(gòu)建本地web測(cè)試服務(wù)器 如何解決訪問靜態(tài)資源問題
- 用Nodejs搭建服務(wù)器訪問html、css、JS等靜態(tài)資源文件
- 在windows上用nodejs搭建靜態(tài)文件服務(wù)器的簡(jiǎn)單方法
- Node.js靜態(tài)文件服務(wù)器改進(jìn)版
- Nodejs實(shí)現(xiàn)的一個(gè)靜態(tài)服務(wù)器實(shí)例
- 使用nodejs、Python寫的一個(gè)簡(jiǎn)易HTTP靜態(tài)文件服務(wù)器
相關(guān)文章
如何讓Nodejs支持H5 History模式(connect-history-api-fallback源碼分析)
這篇文章主要介紹了如何讓Nodejs支持H5 History模式(connect-history-api-fallback源碼分析),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2019-05-05Node.js中文件系統(tǒng)fs模塊的使用及常用接口
fs是filesystem的縮寫,該模塊提供本地文件的讀寫能力,基本上是POSIX文件操作命令的簡(jiǎn)單包裝。這篇文章主要介紹了Node.js中的文件系統(tǒng)fs模塊的使用,需要的朋友可以參考下2020-03-03Node.js使用多進(jìn)程提高任務(wù)執(zhí)行效率
在Node.JS中使用多進(jìn)程非常簡(jiǎn)單,合理使用多進(jìn)程,可以解放硬件的能力,讓軟件的運(yùn)行效率得到肉眼可見的提升。本文詳細(xì)講解了Node.js使用多進(jìn)程提高任務(wù)執(zhí)行效率的方法,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09利用node.js如何創(chuàng)建子進(jìn)程詳解
之前看多進(jìn)程這一章節(jié)時(shí)發(fā)現(xiàn)這塊東西挺多,寫Process模塊的時(shí)候也有提到,今天下午午休醒來(lái)靜下心來(lái)好好的看了一遍,發(fā)現(xiàn)也不是太難理解。所以下面這篇文章主要給大家介紹了關(guān)于利用node.js如何創(chuàng)建子進(jìn)程的相關(guān)資料,需要的朋友可以參考下。2017-12-12NodeJS基礎(chǔ)API搭建服務(wù)器詳細(xì)過程記錄
本文將以一個(gè)超小型web項(xiàng)目,來(lái)詳細(xì)介紹如何使用NodeJS基礎(chǔ)的http, fs, path, url等模塊提供的API來(lái)搭建一個(gè)簡(jiǎn)單的web服務(wù)器。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-04-04node.js使用 http-proxy 創(chuàng)建代理服務(wù)器操作示例
這篇文章主要介紹了node.js使用 http-proxy 創(chuàng)建代理服務(wù)器,結(jié)合實(shí)例形式分析了node.js使用 http-proxy 創(chuàng)建代理服務(wù)器原理、具體步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2020-02-02node.js程序作為服務(wù)并在windows下開機(jī)自啟動(dòng)(用forever)
這篇文章主要介紹了node.js程序作為服務(wù)并在windows下開機(jī)自啟動(dòng)的相關(guān)資料,因?yàn)閷?shí)現(xiàn)的功能比較簡(jiǎn)單,沒有選擇功能比較強(qiáng)大的pm2,文中選擇利用了forever,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-03-03如何用node.js作為后臺(tái),讀寫xml文件,Node.js的文件系統(tǒng)的Api
這篇文章主要介紹了如何用node.js作為后臺(tái),讀寫xml文件,Node.js的文件系統(tǒng)的Api,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08