認識node核心模塊--從Buffer、Stream到fs

title: 認識node核心模塊--從Buffer、Stream到fsdate: 2017-11-10 21:03:07tags: [node,node基礎]

node中的Buffer和Stream會給剛接觸Node的前端工程師們帶來困惑,原因是前端並沒有類似概念(or 有我們也沒意識到)。然而,在後端,在node中,Buffer和Stream處處體現。Buffer是緩衝區的意思,Stream是流的意思。在計算機中,緩衝區是存儲中間變數,方便CPU讀取數據的一塊存儲區域;流是類比水流形容數據的流動。Buffer和Stream一般都是位元組級操作。本文將介紹這兩個模塊的具體細節後再介紹文件模塊,以讓讀者有更清晰的認識。

正文

二進位緩衝區Buffer

在前端,我們只需做字元串級別的操作,很少接觸位元組、進位等底層操作,一方面這足以滿足日常需求,另一方面Javascript這種應用層語言並不是干這個的;然而在後端,處理文件、網路協議、圖片、視頻等時是非常常見的,尤其像文件、網路流等操作處理的都是二進位數據。為了讓javascript能夠處理二進位數據,node封裝了一個Buffer類,主要用於操作位元組,處理二進位數據。

// 創建一個長度為 10、且用 30 填充的 Buffer。const buf1 = Buffer.alloc(10, 30)console.log(buf1)// <Buffer 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e>// 字元串轉Bufferconst buf2 = Buffer.from(javascript)console.log(buf2)// <Buffer 6a 61 76 61 73 63 72 69 70 74>// 字元串轉 bufferconsole.log(buf2.toString())// javascriptconsole.log(buf2.toString(hex)) //6a617661736372697074

一個 Buffer 類似於一個整數數組,可以取下標,有length屬性,有剪切複製操作等,很多API也類似數組,但Buffer的大小在被創建時確定,且無法調整。Buffer處理的是位元組,兩位十六進位,因此在整數範圍就是0~255。

可以看到,Buffer可以與string互相轉化,還可以設置字符集編碼。Buffer用來處理文件I/O、網路I/O傳輸的二進位數據,string用來呈現。在處理文件I/O、網路I/O傳輸的二進位數據時,應該盡量以Buffer形式直接傳輸,速度會得到很好的提升,但操作字元串比操作Buffer還是快很多的。

Buffer內存分配與性能優化

Buffer是一個典型的javascript與C++結合的模塊,與性能有關的用C++來實現,javascript 負責銜接和提供介面。Buffer所佔的內存不是V8分配的,是獨立於V8堆內存之外的內存,通過C++層面實現內存申請、javascript 分配內存。值得一提的是,每當我們使用Buffer.alloc(size)請求一個Buffer內存時,Buffer會以8KB為界限來判斷分配的是大對象還是小對象,小對象存入剩餘內存池,不夠再申請一個8KB的內存池;大對象直接採用C++層面申請的內存。因此,對於一個大尺寸對象,申請一個大內存比申請眾多小內存池快很多。

流Stream

前面講到,流類比水流形容數據的流動,在文件I/O、網路I/O中數據的傳輸都可以稱之為流,流是能統一描述所有常見輸入輸出類型的模型,是順序讀寫位元組序列的抽象表示。數據從A端流向B端與從B端流向A端是不一樣的,因此,流是有方向的。A端輸入數據到B端,對B就是輸入流,得到的對象就是可讀流;對A就是輸出端、得到的對象是可寫流。有的流即可以讀又可以寫,如TCP連接,Socket連接等,稱為讀寫流(Duplex)。還有一種在讀寫過程中可以修改和變換數據的讀寫流稱為Transform流。

在node中,這些流中的數據就是Buffer對象,可讀、可寫流會將數據存儲到內部的緩存中,等待被消費;Duplex 和 Transform 則是都維護了兩個相互獨立的緩存用於讀和寫。 在維持了合理高效的數據流的同時,也使得對於讀和寫可以獨立進行而互不影響。

在node中,這四種流都是EventEmitter的實例,它們都有close、error事件,可讀流具有監聽數據到來的data事件等,可寫流則具有監聽數據已傳給低層系統的finish事件等,Duplex 和 Transform 都同時實現了 Readable 和 Writable 的事件和介面 。

值得一提的是writable的drain事件,這個事件表示緩存的數據被排空了。為什麼有這個事件呢?起因是調用可寫流的write和可讀流的read都會有一個緩存區用來緩存寫/讀的數據,緩存區是有大小的,一旦寫的內容超過這個大小,write方法就會返回false,表示寫入停止,這時如果繼續read完緩存區數據,緩存區被排空,就會觸發drain事件,可以這樣來防止緩存區爆倉:

var rs = fs.createReadStream(src);var ws = fs.createWriteStream(dst);rs.on(data, function (chunk) { if (ws.write(chunk) === false) { rs.pause(); }});rs.on(end, function () { ws.end();});ws.on(drain, function () { rs.resume();});

一些常見流分類:

  • 可寫流:HTTP requests, on the client、HTTP responses, on the server、fs write streams、zlib streams、crypto streams、TCP sockets、child process stdin、process.stdout, process.stderr
  • 可讀流:HTTP responses, on the client、HTTP requests, on the server、fs read streams、zlib streams、crypto streams、TCP sockets、child process stdout and stderr、process.stdin
  • 可讀可寫流:TCP sockets、zlib streams、crypto streams
  • 變換流:zlib streams、crypto streams

另外,提到流就不得不提到管道的概念,這個概念也非常形象:水流從一端到另一端流動需要管道作為通道或媒介。流也是這樣,數據在端之間的傳送也需要管道,在node中是這樣的:

// 將 readable 中的所有數據通過管道傳遞給名為 file.txt 的文件const readable = getReadableStreamSomehow();const writable = getWritableStreamSomehow(file.txt);// readable 中的所有數據都傳給了 file.txtreadable.pipe(writable);// 對流進行鏈式地管道操作const r = fs.createReadStream(file.txt);const z = zlib.createGzip();const w = fs.createWriteStream(file.txt.gz);r.pipe(z).pipe(w);

注意,只有可讀流才具有pipe能力,可寫流作為目的地。

pipe不僅可以作為通道,還能很好的控制管道里的流,控制讀和寫的平衡,不讓任一方過度操作。另外,pipe可以監聽可讀流的data、end事件,這樣就可以構建快速的響應:

// 一個文件下載的例子,使用回調函數的話需要等到伺服器讀取完文件才能向瀏覽器發送數據var http = require(http) ;var fs = require(fs) ;var server = http.createServer(function (req, res) { fs.readFile(__dirname + /data.txt, function (err, data) { res.end(data); }) ;}) ;server.listen(8888) ;// 而採用流的方式,只要建立連接,就會接受到數據,不用等到伺服器緩存完data.txtvar http = require(http) var fs = require(fs) var server = http.createServer(function (req, res) { var stream = fs.createReadStream(__dirname + /data.txt) stream.pipe(res) }) server.listen(8888)

因此,使用pipe即可解決上面那個爆倉問題。

fs文件模塊

fs文件模塊是高階模塊,繼承了EventEmitter、stream、path等底層模塊,提供了對文件的操作,包括文件的讀取、寫入、更名、刪除、遍歷目錄、鏈接POSIX文件系統等操作。與node設計思想和其他模塊不同的是,fs模塊中的所有操作都提供了非同步和同步兩個版本。fs模塊主要由下面幾部分組成:

  • 對底層POSIX文件系統的封裝,對應於操作系統的原生文件操作
  • 繼承Stream的文件流 fs.createReadStream和fs.createWriteStream
  • 同步文件操作方法,如fs.readFileSync、fs.writeFileSync
  • 非同步文件操作方法, fs.readFile和fs.writeFile

模塊API架構如下:

讀寫操作:

const fs = require(fs); // 引入fs模塊/* 讀文件 */// 使用流const read = fs.createReadStream(sam.js,{encoding:utf8});read.on(data,(str)=>{ console.log(str);})// 使用readFilefs.readFile(test.txt, {}, function(err, data) { if (err) { throw err; } console.log(data);});// open + readfs.open(test.txt,r,(err, fd) => { fs.fstat(fd,(err,stat)=>{ var len = stat.size; //檢測文件長度 var buf = new Buffer(len); fs.read(fd,buf,0,len,0,(err,bw,buf)=>{ console.log(buf.toString(utf8)); fs.close(fd); }) });});/* 寫文件與讀取文件API形式類似 */

讀/寫文件都有三種方式,那麼區別是什麼呢?

  • createReadStream/createWriteStream創建一個將文件內容讀取為流數據的ReadStream對象,這個方法主要目的就是把數據讀入到流中,得到是可讀流,方便以流進行操作
  • readFile/writeFile:Node.js會將文件內容視為一個整體,為其分配緩存區並且一次性將文件內容讀/寫取到緩存區中,在這個期間,Node.js將不能執行任何其他處理,所以當讀寫大文件的時候,有可能造成緩存區「爆倉」
  • read/write讀/寫文件內容是不斷地將文件中的一小塊內容讀/寫入緩存區,最後從該緩存區中讀取文件內容

同步API也是如此。其中最常用的是readFile,讀取大文件則採取用,read則提供更為細節、底層的操作,而且read要配合open。

獲取文件的狀態:

fs.stat(eda.txt, (err, stat) => { if (err) throw err console.log(stat)})/* Stats { dev: 16777220, mode: 33279, nlink: 1, uid: 501, gid: 20, rdev: 0, blksize: 4194304, ino: 4298136825, size: 0, blocks: 0, atimeMs: 1510317983760.94, - 文件數據最近被訪問的時間 mtimeMs: 1510317983760.94, - 文件數據最近被修改的時間。 ctimeMs: 1510317983777.8538, - 文件狀態最近更改的時間 birthtimeMs: 1509537398000, atime: 2017-11-10T12:46:23.761Z, mtime: 2017-11-10T12:46:23.761Z, ctime: 2017-11-10T12:46:23.778Z, birthtime: 2017-11-01T11:56:38.000Z }*/

監聽文件:

const FSWatcher = fs.watch(eda.txt, (eventType, filename) => { console.log(`${eventType}`)})FSWatcher.on(change, (eventType, filename) => { console.log(`${filename}`)})// watch和返回的FSWatcher實例的回調函數都綁定在了 change 事件上fs.watchFile(message.text, (curr, prev) => { console.log(`the current mtime is: ${curr.mtime}`); console.log(`the previous mtime was: ${prev.mtime}`);})

監聽文件仍然有兩種方法:

  • watch 調用的是底層的API來監視文件,很快,可靠性也較高
  • watchFile 是通過不斷輪詢 fs.Stat (文件的統計數據)來獲取被監視文件的變化,較慢,可靠性較低,另外回調函數的參數是 fs.Stat 實例

因此儘可能多的使用watch,watchFile 用於需要得到文件更多信息的場景。

其他

創建、刪除、複製、移動、重命名、檢查文件、修改許可權...

總結

由Buffer到Stream,再到fs文件模塊,將它們串聯起來能對整塊知識有更清晰的認識,也對webpack、gulp等前端自動化工具構建工作流的機制和實現有了更深的了解。學習其他知識亦是如此——知道來龍去脈,知道為什麼會存在,知道它們之間的聯繫,就能讓碎片化的知識串聯起來,能讓它們make sense,能夠讓自己「上的廳堂、下得廚房」。

參考:

nodeJs高階模塊--fs

deep into node


推薦閱讀:

WEB前端開發人員須知的常見瀏覽器兼容問題及解決技巧
你是如何去組織項目中的Less/Sass代碼的?
我對vuex的理解(二) 之 mapGetters取值和mapMutations的傳參
前端日刊-2018.01.24
前端日刊-2018.02.01

TAG:前端開發 | Nodejs | 後端技術 |