Node.js Stream(流)總結

Keely發表於2019-02-27

1. 流的概念

  • 流是一組有序的,有起點和終點的位元組資料傳輸手段,而且有不錯的效率。
    藉助事件和非阻塞I/O庫,流模組允許在其可用的時候動態處理,在其不需要的時候釋放掉。
  • 流(stream)在 Node.js 中是處理流資料的抽象介面(abstract interface)。 stream 模組提供了基礎的 API 。使用這些 API 可以很容易地來構建實現流介面的物件。比如HTTP 伺服器request和response物件都是流。
  • 流可以是可讀的、可寫的,或是可讀寫的。所有的流都是 EventEmitter 的例項

2. 為什麼使用流

如果讀取一個檔案,使用fs.readFileSync同步讀取,程式會被阻塞,然後所有資料被寫到記憶體中。使用fs.readFile讀取,程式不會阻塞,但是所有資料依舊會一次性全被寫到記憶體,然後再讓消費者去讀取。如果檔案很大,記憶體使用便會成為問題。
這種情況下流就比較有優勢。流相比一次性寫到記憶體中,它會先寫到到一個緩衝區,然後再由消費者去讀取,不用將整個檔案寫進記憶體,節省了記憶體空間。

  1. 不使用流時:會發現當檔案很大會導致記憶體佔用也非常大。
    不使用流時
  2. 使用流時:
    使用流時

    使用流會發現記憶體佔用會很小,流的過程還可以理解成如下圖:

    使用流時

3. 四種流型別

Node.js中有四種基本型別的流:

  • Readable – 可讀操作。 (如 fs.createReadStream())
  • Writable – 可寫操作。 (如 fs.createWriteStream())
  • Duplex – 可讀可寫操作。(如 net.Socket)
  • Transform – 在讀寫過程中可以修改和變換資料的 Duplex 流 (如 zlib.createDeflate())

4. 可讀流 createReadStream

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

var util = require(`util`);
var fs = require("fs")

fs.createReadStream = function(path, options) {
  return new ReadStream(path, options);
};
util.inherits(ReadStream, Readable);
複製程式碼

4.1 建立可讀流

var rs = fs.createReadStream(path,[options]);

1.path 讀取檔案的路徑
2.options 
    flags開啟檔案要做的操作,預設為`r`
    encoding預設為null
    start開始讀取的索引位置
    end結束讀取的索引位置(包括結束位置)
    highWaterMark讀取快取區預設的大小64kb

> 如果指定utf8編碼highWaterMark要大於3個位元組
複製程式碼

4.2 設定編碼

// 與指定{encoding:`utf8`}效果相同,設定編碼
rs.setEncoding(`utf8`);
複製程式碼

4.3 監聽data事件

// 一旦監聽data事件時,流就可以讀檔案的內容,並且發射data。
// 根據設定的讀取快取區預設大小來決定,讀一段發射一段,直到讀完。

// 預設情況下,監聽data事件後會不停的讀資料,然後出發data事件,觸發完data事件後,再次讀資料。不會停。
// 希望流有一個暫停和恢復觸發機制,見4.8 暫停和恢復觸發data

rs.on(`data`, function (data) {
    console.log(data);
});
複製程式碼

4.4 監聽end事件

// 檔案讀完了,會觸發end事件
rs.on(`end`, function () {
    console.log(`讀取完成`);
});
複製程式碼

4.5 監聽error事件

// 檔案讀取出錯了,會觸發error事件
rs.on(`error`, function () {
    console.log("error");
});
複製程式碼

4.6 監聽open事件

// 如果是檔案流還會涉及到open和close兩個事件
rs.on(`open`, function () {
    console.log("檔案開啟");
});
複製程式碼

4.7 監聽close事件

// 如果是檔案流還會涉及到open和close兩個事件
rs.on(`close`, function () {
    console.log("檔案關閉");
});
複製程式碼

4.8 暫停和恢復觸發data

// 通過pause()方法和resume()方法
rs.on(`data`, function (data) {
    console.log(data);
    rs.pause(); // 暫停讀取和發射data事件
    setTimeout(function () {
        rs.resume(); // 恢復讀取並觸發data事件
    },2000);
});
複製程式碼

以上可以看到:

  • opendata前,open 先開啟檔案,然後data讀取完內容發射。
  • endclose前,先發現檔案讀完了執行end,然後再關閉檔案close

4.9 可讀流的兩種模式

  1. 可讀流事實上工作在下面兩種模式之一:flowingpaused

  2. flowing 模式下, 可讀流自動從系統底層讀取資料,並通過 EventEmitter 介面的事件儘快將資料提供給應用。

  3. paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。

  4. 所有初始工作模式為 pausedReadable 流,可以通過下面三種途徑切換到 flowing 模式:

    1. 監聽 `data` 事件
    var rs = fs.createReadStream(path,[options]);
    複製程式碼
    1. 呼叫 stream.resume() 方法
    2. 呼叫 stream.pipe() 方法將資料傳送到 Writable
  5. 可讀流可以通過下面途徑切換到 paused 模式:

    1. 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
    2. 如果存在管道目標,可以通過取消 `data` 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。

如果 Readable 切換到 flowing 模式,且沒有消費者處理流中的資料,這些資料將會丟失。 比如, 呼叫了 readable.resume() 方法卻沒有監聽 `data` 事件,或是取消了 `data` 事件監聽,就有可能出現這種情況。

5. 可寫流 createWriteStream

createWriteStream實現了stream.Writable介面的物件將流資料寫入到物件中

fs.createWriteStream = function(path, options) {
  return new WriteStream(path, options);
};

util.inherits(WriteStream, Writable);
複製程式碼

5.1 建立可寫流

// 往可寫流裡寫資料時,不會立刻寫入檔案的,而會先寫入快取區,
快取區大小就是highWaterMark,預設16k。然後等快取區滿了之後再真正的寫入檔案裡。

var ws = fs.createWriteStream(path,[options]);

1. path寫入的檔案路徑
2. options
    flags 開啟檔案要做的操作,預設為`w`
    encoding 預設為utf8
    start 開始位置
    highWaterMark 寫入快取區的預設大小16kb
複製程式碼

5.2 write方法

// write方法有返回值flag,按理說返回false就不能往裡面寫了,但是真的寫了資料也不會丟失,會快取在記憶體裡。等快取區清空後再從記憶體讀取出來。

let flag = ws.write(chunk,[encoding],[callback]);

1. chunk寫入的資料buffer/string
2. encoding編碼格式chunk為字串時有用,可選
3. callback 寫入成功後的回撥
> 返回值為布林值,系統快取區滿時為false,未滿時為true(快取區不能接著寫返回false,能接著寫返回true複製程式碼

5.3 end方法

ws.end(chunk,[encoding],[callback]);

> 表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 `finish` 事件的回撥函式
複製程式碼

5.4 drain方法

  • 當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 `drain` 事件就會被觸發
  • 建議, 一旦 write() 返回 false, 在 `drain` 事件觸發前, 不能寫入任何資料塊
// 監聽可寫流快取區清空事件
// 快取區滿了後被清空了才會觸發drain
ws.on(`drain`, function () {
    console.log(`drain`);
});
複製程式碼

5.5 finish方法

  • 在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, `finish` 事件將被觸發
ws.end(`結束`);
ws.on(`finish`, function () {
    console.log("寫入完成");
});
複製程式碼

參考:

  1. Node.js v8.9.3 文件 stream (流)
  2. Node.js中流的使用
  3. Node.js Stream(流)
  4. Node Stream 流(一)流的基本介紹及流下載檔案

相關文章