Streams in Node.js

微介紹

要讀檔案的時候,如果要一次全部讀出來...

import fs from 'fs';

fs.readFile('./file', (err, data) => {
  if (err) throw err;
  
  console.log(data);
});

// <Buffer 68 65 6c 6c 6f 0a>

假如檔案很大,就會耗時、一次佔用很多記憶體

data 是 Buffer 物件

Buffer 可以想成一段記憶體空間

上面會有一段位元組序列

換成用 Stream 來讀...

import fs from 'fs';

const fileStream = fs.createReadStream('./file');

// 在 fileStream 的 'data' 事件設置 listener ( callback )
fileStream.on('data', (chunk) => {
  console.log(chunk);
});

就可以分很多次讀

stream 是 EventEmitter

on 是 addListener 的別名

要寫檔案的時候,如果要一次全部寫進去...

import fs from 'fs';

const data = 'hello world';

fs.writeFile('./file', data, (err) => {
  if (err) throw err;

  console.log('file saved');
});

假如內容很多,就會耗時、一次佔用很多記憶體

換成用 Stream 來寫...

import fs from 'fs';

const fileStream = fs.createWriteStream('./file');

const chunks = ['hello ', 'world'];

chunks.forEach((chunk) => {
  fileStream.write(chunk);
});

fileStream.end();

就可以分很多次寫

用 Stream 讀一個檔案的內容,然後寫到另一個檔案

import fs from 'fs';

const fileReadStream = fs.createReadStream('./fileToRead');
const fileWriteStream = fs.createWriteStream('./fileToWrite');

// 在 fileReadStream 的 'data' 事件設置 listener ( callback )
fileReadStream.on('data', (chunk) => {
  fileWriteStream.write(chunk);
});

// 在 fileReadStream 的 'end' 事件設置 listener ( callback )
fileReadStream.on('end', () => {
  fileWriteStream.end();
});

或是直接用 pipe 方法

import fs from 'fs';

const fileReadStream = fs.createReadStream('./fileToRead');
const fileWriteStream = fs.createWriteStream('./fileToWrite');

fileReadStream.pipe(fileWriteStream);

這個是 Readable Stream ( 產生資料 )

這個是 Writable Stream ( 消耗資料 )

import fs from 'fs';

const fileStream = fs.createReadStream('./file');

import fs from 'fs';

const fileStream = fs.createWriteStream('./file');

基本上就是這兩種

readableStream.pipe(writableStream)

 

  pipe 主要在做的事(極度簡略版)

平常的時候

readable.on('data', (chunk) => {
  writable.write(chunk);
});

readable.on('end', () => {
  writable.close();
});

流程和這樣有點像

Readable Stream 產生 data 太慢的時候

就處理得慢一點

Writable Stream 消耗 data 太慢的時候

readable.on('data', (chunk) => {
  if (!writable.write(chunk)) {
    readable.pause();
  }
});

writable.on('drain', () => {
  readable.resume();
});

1. writable.write(chunk) 會回傳 false

2. 知道來不及,於是叫 readable 暫停

 

 

3. 等 writable 說 ok

4. 再叫 readable 繼續

流程和這樣有點像

Readable Stream 有分 flowing mode

non-flowing mode

flowing mode 的時候會自己一直發送 'data' 事件

non-flowing mode 的時候不會

 

Readable Stream 發現有人在監聽 'data' 事件

就會自己進入 flowing mode

pipe 方法會在 readable 跟 writable 的一些事件上放 listener ( callback )

    readable.on('data', ondata);

    readable.on('end', unpipe);

    readable.on('end', onend);

    ...

 

    writable.on('drain', ondrain);

    writable.on('error', onerror);

    writable.on('unpipe', onunpipe);

    writable.on('close', onclose);

    writable.on('finish', onfinish);

    ...

readable stream 跟 writable stream 內部都有緩衝區

Readable

Writable

BufferList

BufferList

Buffer

(highWaterMark)

(highWaterMark)

如果加入 Buffer 之後總和超過 highWaterMark,

writable.write(chunk) 就會回傳 false,意思就是最好可以暫停,

但是那一塊 chunk (Buffer 物件) 還是會加到 writable 的 BufferList

import fs from 'fs';

const fileReadStream = fs.createReadStream('./fileToRead');
const fileWriteStream = fs.createWriteStream('./fileToWrite');

fileReadStream.on('data', (chunk) => {
  fileWriteStream.write(chunk);
});

fileReadStream.on('end', () => {
  fileWriteStream.end();
});

雖然這樣寫不好

但可能也不會出錯

只是有可能會挪用很多記憶體

簡單實作 Readable Stream

主要是要實作它內部的 _read 方法,

方法裡面要呼叫 this.push(chunk),

這個 this.push(chunk) 會把 chunk 放到緩衝區 (BufferList) 裡面

 

這個 _read 方法會在適當的時候

被 Readable Stream 自己呼叫

這樣緩衝區裡面就有資料可以取了

Readable

BufferList

this.push(chunk)

chunk

簡單實作 Readable Stream

const fs = require('fs');
const { Readable } = require('stream');

const fileWriteStream = fs.createWriteStream('./file.txt');

const content = ['World', 'Hello '];

const readable = new Readable({
  read(size) {
    const data = content.pop();

    if (data) {
      this.push(data);
    } else {
      this.push(null);
    }
  }
});

readable.pipe(fileWriteStream);

function Readable(options) {

    ...

    this._read = options.read;

    ...

}

簡單實作 Writable Stream

主要是要實作它內部的 _write 方法,

寫說你要怎麼用那一塊 data

 

這個 _write 方法會在適當的時候

被 Writable Stream 自己呼叫

這樣緩衝區裡面的資料就被消耗掉了

Writable

BufferList

_write(chunk)

chunk

簡單實作 Writable Stream

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

const writable = new Writable({
  write(chunk, encoding, callback) {
    console.log(chunk.toString());

    callback();
  }
});

function Writable(options) {

    ...

    this._write = options.write;

    ...

}

 

( 不要和 writable.write 搞混 )

另外還有兩種 Stream

Transform StreamDuplex Stream

他們都既是 Readable Stream 也是 Writable Stream

Transform Stream

Duplex Stream

Writable side

Writable side

Readable side

Readable side

readable.pipe(writable) 會 return writable

如果那個 writable 也是一個 readable

就可以再 pipe

Text

Transform Stream <------

簡單實作 Transform Stream

主要是要實作它內部的 _transform 方法,

裡面要對 chunk 做一些事然後再 this.push 它

就像是 _write 跟 _read 合起來成為一個 _transform 方法

 

這個 _transform 方法會在適當的時候被

Transfrom Stream 自己呼叫

這樣 writable 那邊的緩衝區的資料

就被轉換到 readable 那邊的緩衝區了

簡單實作 Transform Stream

http://fred-zone.blogspot.com/2017/09/nodejs-transform-stream.html

沒對資料做轉換就直接推出去了

 

如果要做一些轉換

就 push 轉換過的 data

簡單實作 Duplex Stream

就當作分別實作 Readable Stream 跟 Writable Stream 的部份

也就是要分別實作 _read 方法跟 _write 方法

stdin 和 stdout (還有 stderr)

stdin -> standard input 標準輸入串流
stdout -> standard output 標準輸出串流

在 Node.js 裡可以用

process.stdin

process.stdout

來叫它們

node.js 的 console.log

檔案內容直接 pipe 到 stdout

const fs = require('fs');

const fileReadStream = fs.createReadStream('./file.txt');

fileReadStream.pipe(process.stdout);

或是去 child_process 繞一下

const fs = require('fs');
const child_process = require('child_process');

const fileReadStream = fs.createReadStream('./file.txt');

const cp = child_process.spawn('cat');

fileReadStream.pipe(cp.stdin);

cp.stdout.pipe(process.stdout);

child_process 的 stdin

對我們來說是 Writable Stream

 

child_process 的 stdout

對我們來說是 Readable Stream

Node.js 的 Stream 有一個 objectMode

本來是傳一段一段 Buffer (或 string)

可以改成傳一個一個 object

 

objectMode 下 highWaterMark 的意義是 object 的個數

而不是 BufferList 的總長

監控一連串的 pipe

Streams in Node.js

By luyunghsien

Streams in Node.js

  • 588