您好,登錄后才能下訂單哦!
想寫靜態資源服務器,首先我們需要知道如何創建一個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'); });
接下來我們對一些核心功能進行講解
深刻理解并實現壓縮和解壓
為什么要壓縮呢?有什么好處?
可以使用zlib模塊進行壓縮及解壓縮處理,壓縮文件以后可以減少體積,加快傳輸速度和節約帶寬代碼
壓縮和解壓縮對象都是transform轉換流,繼承自duplex雙工流即可讀可寫流
實現壓縮和解壓
因為壓縮我文件可能很大也可能很小,所以為了提高處理速度,我們用流來實現
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"));
有些時候我們拿到的字符串不是一個流,那怎么解決呢
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中應用壓縮和解壓
下面實現這樣一個功能,如圖:
客戶端向服務器發起請求的時候,會通過accept-encoding(比如:Accept-Encoding:gzip,default)告訴服務器我支持的解壓縮的格式
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"); } }
深刻理解并實現緩存
為什么要緩存呢,緩存有什么好處?
緩存的分類
強制緩存:
強制緩存,在緩存數據未失效的情況下,可以直接使用緩存數據
在沒有緩存數據的時候,瀏覽器向服務器請求數據時,服務器會將數據和緩存規則一并返回,緩存規則信息包含在響應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); }
這種方式有很多缺陷
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); }
這樣一個簡單的靜態服務器就完成了,其中包含了靜態文件服務,實現緩存,實現斷點續傳,分塊獲取,實現壓縮的功能
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。