靜態檔案伺服器實現
nodejs不僅僅可以用來寫服務端介面,用來做靜態檔案伺服器替代nginx的功能, 也是分分鐘可以搞定的。 話不多說,先上程式碼:
var server=http.createServer(function (req,res){
fs.createReadStream(Path.resolve(__dirname,"."+req.url)).pipe(res);
})
複製程式碼
在專案根目錄建一個hello.html檔案測試一下 hello.html內容如下:
<h1>hello,world</h1>
複製程式碼
node app.js執行,開啟瀏覽器訪問一下:http://localhost/hello.html
我們再回頭審視一下程式碼,的確就只有這麼簡單,這要歸功於node Stream類 pipe方法的強大,fs.createReadStream讀取本地檔案建立一個可讀流(ReadStream類的例項),再使用pipe導流到res響應流,res是一個http.ServerResponse類的例項,是一個可寫流,繼承自 Stream類
http.ServerResponse類的繼承關係如下:
安全性考慮
上述程式碼實現靜態檔案伺服器後,意味著專案根目錄下所有的檔案(遞迴)都可以通過瀏覽器直接訪問和下載了,這樣會帶來一些安全性的問題,想想看,你的伺服器端程式碼和配置檔案都能通過瀏覽器直接下載了,因此需要在程式碼里加一些限制,例如只能訪問特定的目錄下的檔案和特定副檔名的檔案,這樣還不夠,參考OWasp Top 10安全風險(第4條-不安全的物件直接引用),攻擊者仍然可以通過../../目錄回溯的方法訪問到其它目錄,對於訪問路徑中包含..的也要全部過濾掉。
實現mine type
mime type是指http 響應頭中的content-type欄位,它決定了瀏覽器如何解析檔案,是直接當做純檔案顯示(text/plain),還是做為html檔案渲染(text/html),或者當做二進位制檔案下載,沒有輸出正確的mine type,可能導致圖片檔案無法顯示,字型檔案無效,視訊檔案無法播放的問題。要實現起來也十分簡單,只需要做一個對映表,不同副檔名,在響應頭的content-type欄位中輸出對應的mine type就行了。
完整程式碼如下:
const http=require("http");
const Path=require("path");
const fs=require("fs");
var server=http.createServer(function (req,res){
const fileName=Path.resolve(__dirname,"."+req.url);
const extName=Path.extname(fileName).substr(1);
if (fs.existsSync(fileName)) { //判斷本地檔案是否存在
var mineTypeMap={
html:'text/html;charset=utf-8',
htm:'text/html;charset=utf-8',
xml:"text/xml;charset=utf-8",
png:"image/png",
jpg:"image/jpeg",
jpeg:"image/jpeg",
gif:"image/gif",
css:"text/css;charset=utf-8",
txt:"text/plain;charset=utf-8",
mp3:"audio/mpeg",
mp4:"video/mp4",
ico:"image/x-icon",
tif:"image/tiff",
svg:"image/svg+xml",
zip:"application/zip",
ttf:"font/ttf",
woff:"font/woff",
woff2:"font/woff2",
}
if (mineTypeMap[extName]) {
res.setHeader('Content-Type', mineTypeMap[extName]);
}
var stream=fs.createReadStream(fileName);
stream.pipe(res);
}
})
server.listen(80);
複製程式碼
實現gzip
對於文字型別的檔案,如html,js,css,採用gzip壓縮可以大幅減少傳輸量,提升伺服器傳輸效能,當然這會損耗一點伺服器的cpu效能做為代價,如果客戶端瀏覽器支援gzip壓縮,則會在請求頭的accept-encoding中攜帶gzip關鍵字,用node自帶的zlib類就可以實現gzip壓縮了,只要在stream.pip實多加一層,先導流到gzip流,再匯出到res流,當然,還要在響應頭中新增Content-Encoding為gzip,這樣瀏覽器才能正確識別到http body是採用gzip演算法壓縮的,並進行自動解壓縮。
程式碼如下:
const zlib = require('zlib');
if (req.headers["accept-encoding"].indexOf("gzip")>=0 && (extName=="js" || extName=="css" || extName=="html"))) {
res.setHeader('Content-Encoding', "gzip");
const gzip = zlib.createGzip();
stream.pipe(gzip).pipe(res);
}
複製程式碼
客戶端快取
http協議的快取協商流程比較長,最終在響應頭中生成expire(絕對時間)和cache-control(相對時間)兩個用於控制快取過期時間的引數,瀏覽器下次請求該檔案時,分為以下幾種情況:
- 如果沒到過期時間,瀏覽器不會請求檔案直接讀快取
- 如果已到過期時間,則會在請求頭中last-modified欄位攜帶檔案的最後修改日期,如果對比時間戳與伺服器檔案一致,則HTTP 返回 304: Not Modified
- 如果按下f5重新整理,會在請求頭中if-modified-since欄位中攜帶快取的過期時間,如果對比時間戳與伺服器檔案一致,則HTTP 返回 304: Not Modified
- ctrl+f5重新整理,請求頭中攜帶 cache-control: no-cache,強制禁用快取。重新下載檔案
邏輯分支較多,但都是日期比對,搞清楚快取協商過程比較容易寫出來,有興趣的同學可以自行實現
高效能靜態檔案伺服器優化
如果要做一個高效能的靜態檔案伺服器僅實現gzip和快取協商是不夠的,涉及到本地檔案的頻繁讀取,高併發下I/O必定成為瓶頸,考慮到伺服器上的檔案是很少更新的, 可以用Buffer把檔案流快取到記憶體中,每次請求時先在記憶體中查詢匹配項,如果命中了直接從記憶體中返回,避免了讀取磁碟,gzip也不用壓縮了,直接用壓縮好的檔案流返回,可以成倍的大幅提升效能。當然如果檔案太多了,記憶體也會飆升,需要考慮淘汰演算法,只快取訪問次數高的檔案,剔除低訪問量的檔案。
採用fs.watch監控目錄檔案的變化,如果檔案有更新,則刪掉快取。
小結
Node.js 內建的pipe方法可以非常簡便的實現將伺服器本地檔案輸出到http 響應流中,gzip壓縮也同樣可以通過pipe實現,再配合輸出mine type 實現的靜態伺服器已經可以滿足一般業務的使用。如果要實現高效能的靜態檔案伺服器,還需要實現客戶端快取、服務端快取功能(本文提供了思路,按圖索驥也非難事)。
最後,推薦一下個人的開源專案, node.js web開發框架,已包含本文靜態檔案伺服器的功能 webcontext: github.com/windyfancy/…