nodejs的stream模組

Eline發表於2018-04-06

Stream 是Node.js中最重要的元件和模式之一,在構建較複雜的系統時,通常將其拆解為功能獨立的若干部分。這些部分的介面遵循一定的規範,通過某種方式相連,以共同完成較複雜的任務。nodejs的核心模組,基本上都是stream的的例項,比如process.stdout、http.clientRequest

什麼是流?

  • 流是一組有序的,有起點和終點的位元組資料傳輸手段
  • 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理
  • 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。

簡單的理解,流就是將大塊的東西,分小塊依次處理。就像你需要從水龍頭上接一杯水,那麼當你擰開水龍頭,水管就會一點點的源源不斷的流出來給你。

image.png

那麼流這種方式在程式當中又有什麼優勢呢?先看如下程式碼:

let fs = require('fs');
fs.readFile('./1.txt', 'utf8', function(err, data){
    // 1.txt 已經讀取完成
    console.log(data);
    fs.writeFile('/2.txt', data); // 將內容寫入2.txt中
});
複製程式碼

以上兩個方法是實現的功能是將1.txt檔案讀取到記憶體當中,再將它寫入到2.txt檔案中。但是如果檔案過大就會出現問題了,記憶體容易爆掉。那麼這裡比較合適的方式應該是讀寫交替進行,也就是使用流的方式讀寫檔案,這樣不管檔案有多大,都不會一下子耗盡記憶體,可以安全的執行完。

如下:

let fs = require('fs');
let readStream = fs.createReadStream('./1.txt');
let writeStream = fs.createWriteStream('./2.txt');
readStream.on('data', function(chunk) { // 當有資料流出時,寫入資料,chunk的型別為Buffer
    writeStream.write(chunk);
});
readStream.on('end', function() { // 當沒有資料時,關閉資料流
    writeStream.end();
});	
複製程式碼

流的四種型別

在nodejs中,有四種stream型別:

  • Readable - 可讀的流,用來讀取資料 (例如 fs.createReadStream()).
  • Writable - 可寫的流,用來寫資料 (例如 fs.createWriteStream()).
  • Duplex - 可讀寫的流(雙工流),可讀+可寫 (例如 net.Socket).
  • Transform - 轉換流,在讀寫過程中可以修改和變換資料的 Duplex 流 (比如 zlib.createDeflate()(資料壓縮/解壓)).

1、可讀流(Readable streams)

nodejs中常見的可讀流有:fs.createReadStream()、http.IncomingRequest、process.stdin

可讀流createReadStream用法如下:
// 建立可讀流
let rs = fs.createReadStream(path,[options]);

// 設定編碼格式
rs.setEncoding('utf8');

// 監聽open事件,開啟檔案時觸發
rs.on('open', function () {
    console.log(err);
});

//流切換到流動模式,資料會被儘可能快的讀出
rs.on("data",function(data){
    console.log(data); //讀取到的資料
});

// 該事件會在讀完資料後被觸發
rs.on("end",function(data){
    console.log("資料已經讀取完畢");
});

//如果讀取檔案出錯了,會觸發error事件
rs.on("error",function(err){
    console.log("something is wrong during processing");
})

//檔案關閉觸發
rs.on('close', function () {
     console.log('檔案關閉');
});
複製程式碼

1、path讀取檔案的路徑 2、options

  • flags開啟檔案要做的操作,預設為'r'
  • encoding預設為null
  • start開始讀取的索引位置
  • end結束讀取的索引位置(包括結束位置)
  • highWaterMark讀取快取區預設的大小64kb 如果指定utf8編碼highWaterMark要大於3個位元組

2、可寫流(Writable streams)

可寫流createReadStream

實現了stream.Readable介面的物件,將物件資料讀取為流資料,當監聽data事件後,開始發射資料

 let  fs = require("fs");
// 建立一個可以寫入的流,寫入到檔案 1.txt 中
let  ws= fs.createWriteStream('1.txt');
let  data = '寫入流資料';
 
// 使用 utf8 編碼寫入資料
ws.write(data,'UTF8');
 
// 表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 'finish' 事件的回撥函式
ws.end("最後寫入的資料","utf8",function(){
   console.log(" 我是'finish' 事件的回撥函式")
});
 
// 在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發。
ws.on('finish', function() {
  console.log("寫入完成。");
});

// 寫入時發生錯誤觸發
ws.on('error', function(err){
  console.log(err.stack);
});
 
複製程式碼
// 建立可寫流
let  ws = fs.createWriteStream(path,[options]);
複製程式碼

1、path讀取檔案的路徑 2、options

  • flags開啟檔案要做的操作,預設為'w'
  • encoding預設為utf8
  • highWaterMark寫入快取區的預設大小16kb

管道流pipe用法 將資料的滯留量限制到一個可接受的水平,以使得不同速度的來源和目標不會淹沒可用記憶體。 linux經典的管道的概念,前者的輸出是後者的輸入 pipe是一種最簡單直接的方法連線兩個stream,內部實現了資料傳遞的整個過程,在開發的時候不需要關注內部資料的流動

用法:

var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to); // 就是從1.txt中讀一點就往2.txt中寫一點
複製程式碼

3、雙工流(Duplex streams)

Duplex實際上就是繼承了Readable和Writable。 有了雙工流,我們可以在同一個物件上同時實現可讀和可寫,就好像同時繼承這兩個介面。 重要的是雙工流的可讀性和可寫性操作完全獨立於彼此。這僅僅是將兩個特性組合成一個物件

const {Duplex} = require('stream');
const inoutStream = new Duplex({
    write(chunk, encoding, callback) {
        console.log(chunk.toString());
        callback();
    },
    read(size) {
        this.push((++this.index)+'');
        if (this.index > 3) {
            this.push(null);
        }
    }
});

inoutStream.index = 0;
process.stdin.pipe(inoutStream).pipe(process.stdout);
複製程式碼

最常見的Duplex stream應該就是net.Socket例項了。

4、轉換流(Transform streams)

轉換流的輸出是從輸入中計算出來的,Transform stream是Duplex stream的特例。也就是說,Transform stream也同時可讀可寫,它可以用來修改或轉換資料。然它跟Duplex stream的區別在於,Transform stream的輸出與輸入是存在相關性的。你可以認為轉換流就是一個函式,這個函式的輸入是一個可寫流,輸出是一個可讀流。

對於轉換流,我們不必實現read或write的方法,我們只需要實現一個transform方法,將兩者結合起來。它有write方法的意思,我們也可以用它來push資料。

例如:希望將輸入的內容轉化成大寫在輸出出來

const {Transform} = require('stream');

const upperCase = new Transform({
    transform(chunk, encoding, callback) {
        this.push(chunk.toString().toUpperCase()); // 將輸入的內容放入到可讀流中
        callback();
    }
});
// 希望將輸入的內容轉化成大寫在輸出出來
process.stdin.pipe(upperCase).pipe(process.stdout);
複製程式碼

常見的Transform stream包括zlib、crypto,這裡有個簡單例子:檔案的gzip壓縮。

let fs = require('fs');
let zlib = require('zlib');

let gzip = zlib.createGzip();

// 將1.txt檔案的內容,打包壓縮成compress.txt.gz
let inFile = fs.createReadStream('./file/1.txt');
let outGz = fs.createWriteStream('./file/compress.txt.gz');

inFile .pipe(gzip).pipe(outGz);
複製程式碼

相關文章