前陣子,我在「海納百川:Node.js Streams」這篇文章的範例中,在使用了 fs.readFile() 這支 API 時,有提到它是 Bulk I/O 這件事,然後再跟 fs.createReadStream() 來比較兩者之間的差異。為了讓你不用再回去那篇文章重看一次,我這裡把程式碼貼過來再稍微整理一下
我們當時是為了讀取一支 2 GB 這麼大的影音檔:
[使用 fs.readFile() 來實作 ]
我們建立一個 http 伺服器 server.js,它使用 fs.readFile() 來讀檔,當有 request 進來的時候,我們把檔案送過去給瀏覽器。我們順便檢測一下錯誤,若有錯誤就把它印出來。當執行 server.js 並開啟瀏覽器到 http://locahost:3000 來下載檔案時,我們的 server 可能會因此爆炸。 (可能爆炸的原因詳見 海納百川:Node.js Streams 內文說明)
var http = require('http'); var fs = require('fs'); var server = http.createServer(function (req, res) { // 利用 fs.readFile() 來讀取 xxx.avi 這支大檔案 fs.readFile('xxx.avi', function (err, data) { if (err) { // 如果發生錯誤就把它列印在 console, 並傳回失敗響應給 Client 端 console.log(err); res.writeHead(500); res.end(err.message); } else { // 如果成功, 就使用 res.end() 將檔案資料送給 Client 端 res.writeHead(200, { 'Content-Type': 'video/avi' }); res.end(data); } }); }); server.listen(3000, function () { console.log('Secret server is up'); });
[ 改用 Stream 來實作 ]
同樣的讀檔功能,改用 Stream 的方式來實作,一切都沒問題!
var server = http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'video/avi' }); // 將數據來源變成 ReadableStream, 然後 pipe 給 res fs.createReadStream('xxx.avi').pipe(res) .on('finish', function () { console.log('Sending done.'); }); });
上面兩種作法的差異
[使用 fs.readFile()]
使用 fs.readFile(pathToFile, function (err, data) {}) 這支 Bulk I/O API (Bulk 是一大坨的意思),底層會將檔案內容先讀進系統緩衝之中,然後在 callback 透過 data 一次給你一大包,就像右邊這張圖的感覺。
這意味著,當你使用這樣的 API 存取 I/O 時,你的 memory footprint 將有那麼一時半刻會衝到很高。如果剛剛好你的系統記憶體負載很繁重,那就只好眼睜睜看著它爆炸了 XDDD....
[使用 fs.createStream()]
Node.js API 手冊
以上是一點簡單的回顧,接下來我們稍微來看一下官方 API 手冊的說明,這裡做一下摘要:- fs 模組幫你把標準 POSIX 的 File I/O 操作包裝成一支支的方法
- File I/O 的方法有 asynchronous 與 synchronous 兩種形式
- Asynchronous 方法的 callback,一律遵照 Node.js 的 err-back style 慣例 (callback 函式簽署的第一個參數為 error 物件)
Asynchronous 與 Synchronous APIs
以 chmod 為例,fs 提供了非同步的 fs.chmod() 與同步的 fs.chmodSync() 兩種形式:在 API 說明中的 chmod(2) 連結會指向 Linux Programmer's Manual,你可以在那裡閱讀關於此操作的功能說明。另外,chmod(2) 中的 (2) 用於說明該方法的分類,我們可以看一下 man 的說明如下圖,(2) 是屬於 System Calls。
我們在 fs 模組的手冊中,會看到很多檔案操作的方法都有非同步與同步兩種版本:
這裡附帶說明一下,同步 API 大多為 Node 內部使用,另一種情況則是在我們自己的初始化程式碼中會使用到他們。當我們的程式完成初始化,運行起來之後,整個程式就進到了事件迴圈的模式,除了有特殊需求之外,我們大部分會使用到的都是 fs 提供的非同步 APIs。
Streams
除了上面看到的兩種 API 的形式之外,fs 還有兩個 Stream Class:ReadStream 與 WriteStream。在實作上會更常使用 createReadStream() 與 createWriteStream() 這兩支工廠方法來將目標檔案實例化為 Streams,這在我們最上面提供的範例就有看到 createReadStream() 的用法。當然,Stream 也是屬於「非同步」的介面。結語
這邊多補充兩個小點子。如果覺得 fs 的 mkdir 不好用,推薦大家使用 substack 寫的 mkdirp,它也是一個被爆量使用的小模組。然後,我常看到有些範例在讀取 json 檔時,會使用 readFile() 或 readFileSync() 將資料讀入後,再 JSON.parse() 來產生 JS 物件。對於 json 檔,在 Node.js 中只要用 var foo = require('path/to/xxx/json') 就可以將 json 讀入成為物件了(這應該蠻多人都知道啦),這是因為 Node 的 require 系統原始碼很佛心幫我們處理掉了。最後來個總結,Node.js 的 fs 模組提供給我們的三種不同類型的 APIs 有:
- 同步的:結果(或資料) 以 return 傳回
- 非同步的:結果(或資料) 傳回給 callback,或說我們使用 callback 來接回結果
- Stream:結果(或資料) 以事件的方式傳回,或說我們使用 event handler 來接回結果