Node.js Stream モジュール
Stream(ストリーム)は、Node.js でストリーミングデータを処理するための抽象インターフェースです。ストリームを水流のように想像できます。データは水のように、ある場所から別の場所へ流れます。この処理方法は、大きなファイルや連続的なデータソースの処理に特に適しています。すべてのデータを一度にメモリに読み込む必要がないからです。
なぜ Stream が必要なのか?
- メモリ効率:大きなファイルを処理する場合、ファイル全体を一度にメモリに読み込む必要がない
- 時間効率:データを受け取りながら処理できるため、すべてのデータが準備できるまで待つ必要がない
- 合成性:複数のストリームをパイプのように接続して、データ処理パイプラインを形成できる
Stream の4つの基本タイプ
1. Readable Stream(読み取り可能ストリーム)
読み取り可能ストリームはデータの供給源であり、そこからデータを読み取ることができます。例えば、ファイルからコンテンツを読み取ったり、HTTP リクエストからデータを取得したりします。
例
const fs = require('fs');
const readableStream = fs.createReadStream('example.txt');
readableStream.on('data', (chunk) => {
console.log(`受け取った ${chunk.length}バイトのデータ`);
});
readableStream.on('end', () => {
console.log('これ以上のデータはありません');
});
const readableStream = fs.createReadStream('example.txt');
readableStream.on('data', (chunk) => {
console.log(`受け取った ${chunk.length}バイトのデータ`);
});
readableStream.on('end', () => {
console.log('これ以上のデータはありません');
});
2. Writable Stream(書き込み可能ストリーム)
書き込み可能ストリームはデータの宛先であり、そこにデータを書き込むことができます。例えば、ファイルに書き込んだり、HTTP レスポンスを送信したりします。
例
const fs = require('fs');
const writableStream = fs.createWriteStream('output.txt');
writableStream.write('1行目のデータ\n');
writableStream.write('2行目のデータ\n');
writableStream.end('最後の行のデータ');
const writableStream = fs.createWriteStream('output.txt');
writableStream.write('1行目のデータ\n');
writableStream.write('2行目のデータ\n');
writableStream.end('最後の行のデータ');
3. Duplex Stream(双方向ストリーム)
双方向ストリームは読み取り可能であり、書き込み可能でもあります。例えば TCP ソケットです。
例
const { Duplex } = require('stream');
const myDuplex = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
},
read(size) {
this.push('双方向ストリームから読み取ったデータ');
this.push(null); // データの終了を示す
}
});
const myDuplex = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
},
read(size) {
this.push('双方向ストリームから読み取ったデータ');
this.push(null); // データの終了を示す
}
});
4. Transform Stream(変換ストリーム)
変換ストリームは特別な双方向ストリームであり、読み書きの過程でデータを変更または変換できます。例えば、データの圧縮や暗号化です。
例
const { Transform } = require('stream');
const upperCaseTr = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
process.stdin.pipe(upperCaseTr).pipe(process.stdout);
const upperCaseTr = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
process.stdin.pipe(upperCaseTr).pipe(process.stdout);
Stream の一般的なメソッド
pipe() メソッド
pipe()pipe() メソッドは Stream の最も強力な機能の1つであり、複数のストリームを接続してパイプラインを形成できます。
例
const fs = require('fs');
// 読み取りストリームと書き込みストリームを作成する
const readStream = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
// pipe を使用して読み取りストリームと書き込みストリームを接続する
readStream.pipe(writeStream);
writeStream.on('finish', () => {
console.log('データの書き込みが完了しました');
});
// 読み取りストリームと書き込みストリームを作成する
const readStream = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
// pipe を使用して読み取りストリームと書き込みストリームを接続する
readStream.pipe(writeStream);
writeStream.on('finish', () => {
console.log('データの書き込みが完了しました');
});
イベント監視
Stream は EventEmitter のインスタンスであり、イベントを監視することでさまざまな状況を処理できます。
data- 読み取り可能なデータがあるときに発生しますend- 読み取るデータがなくなったときに発生しますerror- エラーが発生したときに発生しますfinish- すべてのデータが基盤システムにフラッシュされたときに発生します
実際の応用例
例1:ファイルコピー
例
const fs = require('fs');
function copyFile(source, target, cb) {
// 読み書きストリームを作成
const rd = fs.createReadStream(source);
const wr = fs.createWriteStream(target);
// エラーを処理
rd.on('error', err => cb(err));
wr.on('error', err => cb(err));
// 完了コールバック
wr.on('finish', cb);
// コピーを開始
rd.pipe(wr);
}
// 使用
copyFile('source.txt', 'target.txt', err => {
if (err) console.error(err);
else console.log('コピーが完了しました');
});
function copyFile(source, target, cb) {
// 読み書きストリームを作成
const rd = fs.createReadStream(source);
const wr = fs.createWriteStream(target);
// エラーを処理
rd.on('error', err => cb(err));
wr.on('error', err => cb(err));
// 完了コールバック
wr.on('finish', cb);
// コピーを開始
rd.pipe(wr);
}
// 使用
copyFile('source.txt', 'target.txt', err => {
if (err) console.error(err);
else console.log('コピーが完了しました');
});
例2:HTTP サーバーでのファイル圧縮
例
const fs = require('fs');
const http = require('http');
const zlib = require('zlib');
http.createServer((req, res) => {
// 読み取りストリームを作成
const readStream = fs.createReadStream('./largeFile.txt');
// レスポンスヘッダーを設定
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
});
// パイプライン:読み取り -> 圧縮 -> レスポンス
readStream.pipe(zlib.createGzip()).pipe(res);
}).listen(3000);
const http = require('http');
const zlib = require('zlib');
http.createServer((req, res) => {
// 読み取りストリームを作成
const readStream = fs.createReadStream('./largeFile.txt');
// レスポンスヘッダーを設定
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
});
// パイプライン:読み取り -> 圧縮 -> レスポンス
readStream.pipe(zlib.createGzip()).pipe(res);
}).listen(3000);
Stream のベストプラクティス
- エラー処理:常にストリームにエラー処理リスナーを追加する
- メモリ管理:大きなファイルの場合は、一括読み込みではなくストリームを使用する
- パイプライン:pipe() メソッドを適切に使用して複数のストリームを組み合わせる
- 背圧処理:書き込み速度が読み取り速度に追いつかない場合、背圧を適切に処理する必要があります
エラー処理の例
例
const fs = require('fs');
const readStream = fs.createReadStream('not-exist-file.txt');
readStream.on('error', (err) => {
console.error('エラーが発生しました:', err.message);
});
const readStream = fs.createReadStream('not-exist-file.txt');
readStream.on('error', (err) => {
console.error('エラーが発生しました:', err.message);
});
Node.js の Stream モジュールを習得することで、さまざまな I/O 操作を効率的に処理できます。特に大きなファイルやリアルタイムデータを扱う際に、ストリームは強力なツールとなるでしょう。
その他の拡張
Node.js 組み込みモジュール