NodeJs Stream的整理總結 (一) --可讀流與可寫流

辰辰沉沉大辰沉發表於2018-04-28

最近正好處於一個相對悠閒的當口,想到要整理一下關於Node的一點知識。首先本人專案中並沒有用過node,但是有時間就會關注一下學習一點。短短續續的學習導致的問題就是知識沒有連貫性,由於不注意總結,本來之前花力氣深入瞭解的知識過段時間又忘了。
痛定思痛,寧願在爛筆頭上花點時間,鞏固記憶同時也便於回溯。

以下是我看過的一些比較好的相關文章:

nodejs stream官方文件

stream-handbook英文版
stream-handbook中文版
Nodejs Stream

不扯淡了,主題如題,就是對Node中Stream知識點的一個總結。還是我自己的老習慣,先列個知識點圖譜,再來一項一項的展開:

  • 流是什麼
  • 流的種類
  • 自定義流
  • 流的工作模式

流是什麼

在理解這個問題前我們首先要知道流能幹什麼,解決了什麼問題

// node中的IO會涉及到對檔案的讀寫,以下程式碼中使用者在接收到內容之前首先需要等待程式將檔案
// 內容完全讀入到記憶體中,造成的問題就是如果檔案很大,不但消耗資源而且使使用者連線緩慢,
// 影響使用者體驗
var http = require('http');
var fs = require('fs');

var server = http.createServer(function (req, res) {
    fs.readFile(__dirname + '/large-data.txt', function (err, data) {
        res.end(data);
    });
});
server.listen(8000);

流能幹什麼?其實就是解決上述的問題:將資料分割成段,一段一段的讀取,佔用記憶體更小而且效率更高。

已上我們引出流(stream)的定義:流是對輸入輸出裝置的抽象,資料來源可理解為可讀流,資料目的地可以理解為可寫流。

呼叫node關於流的api對上面程式碼的改造就是:

var http = require('http');
var fs = require('fs');

var server = http.createServer(function (req, res) {
    var stream = fs.createReadStream(__dirname + '/large-data.txt');
    stream.pipe(res);
});
server.listen(8000);

流的種類

  1. 可讀流 readable streams
  2. 可寫流 writable streams
  3. 雙向流 包括transform以及duplex

篇幅有限,這篇文章中先只整理可讀流與可寫流。

可讀流栗子

Readable streams produce data that can be fed into a writable, transform, or duplex stream by calling .pipe()

var Readable = require('stream').Readable;

var rs = new Readable;
rs.push('beep ');
rs.push('boop\n');
rs.push(null);

rs.pipe(process.stdout);
$ node read0.js
beep boop

自定義可讀流 (來自之前看的文章,我覺得很形象)

在node中自定義可讀流的要點有兩個:

  • 繼承 sream 模組的 Readable 類
  • 重寫 _read 方法
const Readable = require('stream').Readable;

class RandomNumberStream extends Readable {
    constructor(max) {
        super()
        this.max = max;
    }

    _read() {
        const ctx = this;

        setTimeout(() => {
            if (ctx.max) {
                const randomNumber = parseInt(Math.random() * 10000);

                // 1) 只能 push 字串或 Buffer,不能是數字
                // 2) push(null)結束推送
                // 3when .push() to a readable stream, the chunks pushed are buffered until a consumer is ready to read them.

                ctx.push(`${randomNumber}\n`);
                ctx.max -= 1;
            } else {
                ctx.push(null);
            }
        }, 100);
    }
}

module.exports = RandomNumberStream;
const RandomNumberStream = require('./RandomNumberStream');

const rns = new RandomNumberStream();

rns.pipe(process.stdout); //輸出(列印)到控制檯

流的工作模式

流有兩種工作模式:流動模式暫停模式

當處於流動模式時,資料由底層系統讀出,並儘可能快地提供給程式;當處於暫停模式時,必須顯式呼叫 stream.read() 來取出若干資料塊。流預設處於暫停模式。

兩種模式可以互相切換:

暫停模式 >> 流動模式:

  1. 監聽’data’事件。
  2. 呼叫resume()方法。
  3. 呼叫pipe()方法將資料轉接到另一個可寫流。(這也正是為什麼在上面自定義可讀流的_read方法中用了setTimeout而非setInterval,卻能夠源源不斷地讀取資料)

流動模式 >> 暫停模式:

  1. 如果呼叫了pipe(),移除data事件的監聽,再呼叫unpipe()。
  2. 沒有呼叫過pipe(),直接顯式呼叫pause()方法。

暫停模式下的read()

When data is available, the ‘readable’ event fires and you can call .read() to fetch some data from the buffer.
When the stream is finished, .read() returns null because there are no more bytes to fetch.

readable event 解決問題就是讀操作在可讀流資料available的時候觸發,避免客戶讀到空資料或者輪詢可讀流是否ready

const rns = new RandomNumberStream(5);

rns.on('readable', () => {
  let chunk;
  while((chunk = rns.read()) !== null){
    console.log(chunk);
  }
});

可寫流栗子

A writable stream is a stream you can .pipe() to but not from

使用方法主要是在可讀流的data事件回撥函式中呼叫writeStream.write方法

const fs = require('fs');
const readStream = fs.createReadStream('./source.js');
const writeStream = fs.createWriteStream('./dis.js');

readStream.setEncoding('utf-8');
readStream.on('data', chunk => {
  writeStream.write(chunk);
});

自定義可寫流

  • 繼承 stream 模組的 Writable 類
  • 重寫 _write() 方法

write() 方法要點

  • highWaterMark 每次最多寫入快取區的資料量,預設值為 16kb
  • pipe的背壓反饋機制(back pressure):

In unix, streams are implemented by the shell with | pipes. In node, the built-in stream module is used by the core libraries and can also be used by user-space modules. Similar to unix, the node stream module’s primary composition operator is called .pipe() and you get a backpressure mechanism for free to throttle writes for slow consumers.
Using .pipe() has other benefits too, like handling backpressure automatically so that node won’t buffer chunks into memory needlessly when the remote client is on a really slow or high-latency connection.

pipe背壓反饋機制的原理: ‘drain’ event

pipe方法每次寫資料的時候,都會判斷是否寫成功,如果失敗,會等待可寫流觸發”drain”事件,表示可寫流可以繼續寫資料了,然後pipe才會繼續寫資料。

所以以下程式碼在寫入巨量資料的時候可能會發生記憶體洩露問題:

const rs = fs.createReadStream(filename);
rs.on('data', function (chunk) {
        res.write(chunk);
});
rs.on('end', function () {
        res.end();
});

改進方法如下:

const rs = fs.createReadStream(filename);
rs.pipe(res)

如果不用pipe的話,我們也可以利用“drain”事件的機制來手動控制讀寫流之間的平衡來避免記憶體洩漏:

const http = require("http");
const fs = require("fs");
const filename = "large-file.txt";

const serv = http.createServer(function (req, res) {
    const stat = fs.statSync(filename);
    res.writeHeader(200, {"Content-Length": stat.size});
    const readStream = fs.createReadStream(filename);
    readStream.on('data', function (chunk) {
        if(!res.write(chunk)){//判斷寫緩衝區是否寫滿
            readStream.pause();//如果寫緩衝區已滿,暫停讀取資料
        }
    });
    readStream.on('end', function () {
        res.end();
    });
    res.on("drain", function () {//寫緩衝區一度堵塞,經過一段時間寫後緩衝區恢復可寫狀態,觸發"drain"事件
        readStream.resume();//重新啟動讀取資料
    });
});

serv.listen(8888);

先暫時到這裡,關於雙工流Duplex和Transform下篇文章再總結。

相關文章