Node.js一行程式碼實現靜態檔案伺服器

windyfancy發表於2019-05-06

靜態檔案伺服器實現

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.js一行程式碼實現靜態檔案伺服器

我們再回頭審視一下程式碼,的確就只有這麼簡單,這要歸功於node Stream類 pipe方法的強大,fs.createReadStream讀取本地檔案建立一個可讀流(ReadStream類的例項),再使用pipe導流到res響應流,res是一個http.ServerResponse類的例項,是一個可寫流,繼承自 Stream類

http.ServerResponse類的繼承關係如下:

Node.js一行程式碼實現靜態檔案伺服器

安全性考慮

上述程式碼實現靜態檔案伺服器後,意味著專案根目錄下所有的檔案(遞迴)都可以通過瀏覽器直接訪問和下載了,這樣會帶來一些安全性的問題,想想看,你的伺服器端程式碼和配置檔案都能通過瀏覽器直接下載了,因此需要在程式碼里加一些限制,例如只能訪問特定的目錄下的檔案和特定副檔名的檔案,這樣還不夠,參考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(相對時間)兩個用於控制快取過期時間的引數,瀏覽器下次請求該檔案時,分為以下幾種情況:

  1. 如果沒到過期時間,瀏覽器不會請求檔案直接讀快取
  2. 如果已到過期時間,則會在請求頭中last-modified欄位攜帶檔案的最後修改日期,如果對比時間戳與伺服器檔案一致,則HTTP 返回 304: Not Modified
  3. 如果按下f5重新整理,會在請求頭中if-modified-since欄位中攜帶快取的過期時間,如果對比時間戳與伺服器檔案一致,則HTTP 返回 304: Not Modified
  4. 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/…

相關文章