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 Stream 跟 Duplex 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

參考資料:
node 官方網站
https://nodejs.org/api/stream.html
Backpressuring in Streams
https://nodejs.org/es/docs/guides/backpressuring-in-streams/
海納百川:Node.js Streams
https://eebreakdown.com/2016/10/nodejs-streams.html
打造自己的 Node.js Transform Stream
http://fred-zone.blogspot.com/2017/09/nodejs-transform-stream.html
Streams in Node.js
By luyunghsien
Streams in Node.js
- 588