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

原文地址在個人博客javascript

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

正文

二進制緩衝區Buffer

在前端,咱們只需作字符串級別的操做,不多接觸字節、進制等底層操做,一方面這足以知足平常需求,另外一方面Javascript這種應用層語言並非幹這個的;然而在後端,處理文件、網絡協議、圖片、視頻等時是很是常見的,尤爲像文件、網絡流等操做處理的都是二進制數據。爲了讓javascript可以處理二進制數據,node封裝了一個Buffer類,主要用於操做字節,處理二進制數據。前端

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

一個 Buffer 相似於一個整數數組,能夠取下標,有length屬性,有剪切複製操做等,不少API也相似數組,但Buffer的大小在被建立時肯定,且沒法調整。Buffer處理的是字節,兩位十六進制,所以在整數範圍就是0~255。java

能夠看到,Buffer能夠與string互相轉化,還能夠設置字符集編碼。Buffer用來處理文件I/O、網絡I/O傳輸的二進制數據,string用來呈現。在處理文件I/O、網絡I/O傳輸的二進制數據時,應該儘可能以Buffer形式直接傳輸,速度會獲得很好的提高,但操做字符串比操做Buffer仍是快不少的。node

Buffer內存分配與性能優化webpack

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

流Stream

前面講到,流類比水流形容數據的流動,在文件I/O、網絡I/O中數據的傳輸均可以稱之爲流,流是能統一描述全部常見輸入輸出類型的模型,是順序讀寫字節序列的抽象表示。數據從A端流向B端與從B端流向A端是不同的,所以,流是有方向的。A端輸入數據到B端,對B就是輸入流,獲得的對象就是可讀流;對A就是輸出端、獲得的對象是可寫流。有的流便可以讀又能夠寫,如TCP鏈接,Socket鏈接等,稱爲讀寫流(Duplex)。還有一種在讀寫過程當中能夠修改和變換數據的讀寫流稱爲Transform流。github

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

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

值得一提的是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.txt'
readable.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.txt
var 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架構以下:

fs主要操做

讀寫操做:

const fs = require('fs'); // 引入fs模塊
/* 讀文件 */

// 使用流
const read = fs.createReadStream('sam.js',{encoding:'utf8'});
read.on('data',(str)=>{
    console.log(str);
})
// 使用readFile
fs.readFile('test.txt', {}, function(err, data) {
    if (err) {
        throw err;
    }
    console.log(data);
});
// open + read
fs.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

相關文章
相關標籤/搜索