Node.js v0.10 Stream2 のきほん
Stream2 ハッカソン
IIJ 大津繁樹
2013年5月18日
Stream は大事なクラス
events.EventEmitter
stream.Stream
net.Socket
tls.CleartextStream
http.ServerRequest http.ClientResponse
http.ServerResponse http.ClientRequest
http.IncomingMessage http.outgoingMessage
node-v0.8
Stream は大事なクラス
events.EventEmitter
stream.Stream
node-v0.10
stream.Readable stream.Writable
http.outgoingMessagestream.Duplex
stream.Transform
net.Socket tls.CleartextStream
その他crypto系クラス
crypto
sign/verify
http.IncomingMessage
Stream1 おさらい
Readable
Stream
src
データの流
れ
dataイベントから
データの読み込み
pause() による読
み込み停止
resume() による読
み込み再開
Stream1 おさらい
Writable
Stream
dst
データの流
れ
.write() による
データの書き込み drainイベントによ
る
書き込み待ちの解
消通知
Stream1 おさらい
Readable
Stream
Writable
Stream
pipe
dataイベント
で受けたデー
タを write()
dstsrc
write() が false
なら pause()
drain イベント
が発生したら
resume()
Stream1 の問題
node-v0.8 のマニュアル
なんだろ?
Stream1 の問題
var server = http.createServer(function(req, res) {
setTimeout(function() {
var data = '';
req.on('data', function(chunk) {
data += chunk;
});
req.on('end', function() {
console.log(data);
});
res.writeHead(200);
res.end();
}, 1000);
}).listen(8080);
POSTデータをちゃんと取れる?
Stream1 の問題
リスナ
Readable
Stream
データ
Readable Stream が 'data' イベントを生成
した時にリスナが存在しなければ、 デー
タは失われることに注意してください
Stream1 の問題
開け→閉め→開け→閉め
開け→閉め→開け→閉め
開け→閉め→開け→閉め
開け→閉め→開け→閉め
不安定な流れに弱い
Stream2とは?
内部タンク付きストリーム
ストリーム内部にデータバッファを持つことによっ
て、
• データの取りこぼしをなくす
• 不安定な流れに影響されにくくする
• ただし上限値(highWaterMark: default 16kB)が決まっ
ている
• オブジェクト一つをストリームバッファとして扱
ReadableStream
きちんと理解す
るのは大変よ
ReadableStream のプレイヤー
ソース 実装者 消費者
データの源
ストリーム本体。
ソースからデー
タを読み込み内
部バッファに蓄
える
ストリームから
データを受け取
り利用する人
push() read()
_read()
readable
Readable 返り値
実
装
者
用
_read(n) Streamから呼ばれる。 n は hwm を指す。 N/A
push(chunk) 内部バッファの最後にchunkを加える。 (chunkはBufferか文
字列、ただし objectMode じゃない時)
chunk を null にすると endイベントが発生
true hwm以下
false hwm 以上
消
費
者
用
setEncoding(en
c)
StringDecoderを指定する。内部バッファにはdecodeしたも
のが加えられる。
undefined
read(n) 内部バッファから n バイトデータを読み込む。 n が
undefined の場合は内部データ全部
バッファ
unshift(chunk) 内部バッファの先頭にchunkを加える。
chunk を null にすると endイベントが発生
true hwm以下
false hwm 以上
パ
イ
プ
pipe(dest,
pipOpts)
dest(WritableStream)とつなげて書き込む。 {end: false} のオ
プションで src の end イベントに伴って自動的に dst.end
しない。
複数 dest をつなげることが可能
dest
unpipe(dest) dest との pipe を外す。destが指定されてない場合は全ての
pipe を外す。
src
旧
モ
ー
ド
用
resume() 省略 (このメソッドがされると旧モード互換になる。)
pause() 省略 (このメソッドがされると旧モード互換になる。)
wrap(stream) stream1 のオブジェクトを stream2 に変換するラッパー self
ReadableStream API methods
ReadableStream API events
イベント名 説明 引数
readable 内部バッファのデータが消費できる時に発生するイ
ベント。(でも実は発生条件が結構複雑)
N/A
end ソースから読み込むデータの終わりを通知された時
に発生するイベント。実質的には Readable.pull(null)
されたタイミングで発生するものと考えていいだろ
う。(例外有)
データを消費(Readable.read) されていないと発生し
ないので注意。
N/A
error 省略 エラーオブ
ジェクト
data 旧モードでデータを受けるイベント。このリスナが
登録されていると旧モード互換になる。
データバッ
ファ
覚えておこう大切なこと
Readable.read(0)
• ソースからデータを読み込む最初のトリ
ガー
• 常に null を返す
• 消費者が利用するもの
• Readable._read() を実行する。(でもストリーム内
部状態によっては実行されない時も…)
• 一番最初の readable イベントのリスナ登
録に合わせて裏で実行される。
覚えておこう大切なこと
Readable.read(n) ただし n>0
• ストリーム内部バッファのデータを消費者が読み
込む(消費する)関数
• 消費者が利用する
• n バイトを取得。読み込みデータが空だと nullが
返る。
• n を指定しない場合はストリーム内部の全データ
を取得。通常はこれ。
• n が highWaterMark以上 だとストリームの最大値
が上がるので注意。
• 通常 readable イベントに合わせて実行する。(他
でも使えるけど)
覚えておこう大切なこと
Readable._read(n)
• ソースからのデータを読み込み操作を開始する関数。
ストリーム内部から実行される。
• 実装者が利用する関数
• read(nn)がトリガーだが、実行されるにはいくつか条
件がある。大概は満タンチェックの 以下の条件。
(内部バッファ量 – nn) < highWaterMark
• 通常この中で ソースから取得したデータをストリーム
に(同期/非同期で) push するように実装する。
• _read()を実装してないとストリームがエラーを throw
する。空の関数でもいいので書いておく。
• 渡される n は highWaterMark。参考程度で使わない。
• push()した後に先読み機能でめちゃめちゃ _read() が呼
ばれるので気を付けよう。(後述)
覚えておこう大切なこと
Readable.push(chunk)
• ソースからストリームへデータ(chunk)を送り込む関数。
実装者が利用する。
• pushされた chunk はストリームの内部バッファの最後に
加えられる。
• (おおよそ)push に合わせて readable イベントが発火す
ると考えていいだろう。(例外あり)
• ストリームが満タン(内部バッファの容量が
highWaterMark以上)の時は push は false を返す。(でも
無理やり詰め込める)
• 何も渡すデータが無い時、 _read() が呼ばれたら push(‘’)
をしておきましょう。
• データ送りの終りは push(null) を実行。これがないと end
イベントが発生しないので必ずやろう。
• push() をするとストリームが先読み機能で内部に読み込
めるだけ read(0) を繰り返してきます。無駄な _read() に
覚えておこう大切なこと
Readable.unshift(chunk)
• ストリームの内部バッファの先頭にデー
タを追加する関数。
• 消費者が読み込み過ぎたデータをもとに
戻す時に利用する。
• 例としては、ヘッダ+ボディの解析時に
ヘッダ分割後残ったバッファ(ボディの
頭の部分)をストリームにまた戻すよう
な時に使う。
覚えておこう大切なこと
old-mode の切り替え
以下のいずれかの条件が合致すれば Stream1 の
互換モードとなります。
• data イベントリスナの登録
• pause()の実行
• resume() の実行
(注意) data イベントのデータ読み込みと
readable イベントでのデータ読み込みを両立さ
せないこと。
一番簡単なサンプル(同期)
var source = *‘a’, ‘b’, ‘c’, null+;
var Readable = require(‘stream’).Readable;
Readable.prototype._read = function() {
this.push(source.shift()); // 同期
};
var rstream = new Readable();
var data = ‘’;
rstream.on(‘readable’, function() ,
var b = rstream.read();
if (b) data += b;
});
rstream.on(‘end’, function() ,
console.log(‘data =‘ + b);
});
消費者
実装者
ソース
一番簡単なサンプル
動作概略
rstream.read(0)
内部バッ
ファ
ソース
‘hoge’
rstream.push(chunk)
rstream._read(hwm)
rstream.on(‘readable’,
);
消費者
実装者
一番簡単なサンプル
動作概略
内部バッ
ファ
ソース
‘hoge’
rstream.push(chunk)
readable
イベント
rstream.on(‘readable’,
function() {
var b = rstream.read()
});
まとめ
て
maybeReadMore
while() {
rstream.read(0)
}
rstream_read(hwm)
消費者
実装者
先読み
一番簡単なサンプル
動作概略
消費者
Readable
内部バッファ
空
ソース rstream.push(null) end
イベント
rstream.on(‘end,
function() {
console.log()
});
実装者
注意
var Readable = require(‘readable’).Readable;
Readable.prototype._read = function() {
this.push(‘hoge’);
}
var rstream = new Readable();
var data = ‘’;
rstream.on(‘readable’, function() ,
var b = rstream.read();
if (b) data += b;
});
rstream.on(‘end’, function() ,
console.log(‘data=‘ + data);
});
EOFがない!
無限同期読み込み
課題1
• 同期のサンプルコードを1秒毎にデータを
返す非同期のサンプルコードにしてみま
しょう。
課題2
• 課題1のコードをストリームの内部バッ
ファが満タンだった時(push の返り値が
falseの時)に処理を止めるように改良しま
しょう。
• 満タンになった場合は、消費者が全部内
部バッファを空にして、ソースからの読
み出しを継続させましょう。
課題3
以下の条件でソースのデータが全
部読み込めるようにしましょう
条件 値
テスト
1
n1 > hwm > n2 n1 = 3, hwm =2, n2 = 1
テスト
2
n1 > n2 > hwm n1 = 3, hwm =1, n2 = 2
テスト
3
hwm > n1 > n2 n1 = 2, hwm =3, n2 = 1
テスト
3
hwm > n2 > n1 n1 = 1, hwm =3, n2 = 2
ソース: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
読み込み: 非同期(1秒毎)
読み込み条件
• Readable.read(n1),
• new Readable {highWaterMark: hwm})
• Readable.push(chunk) ただし chunk.length === n2
WritableStream
こっちはわりと簡
単
WritableStream のプレイヤー
実装者
リソース
ストリームが書き込み中の
場合書き込みリクエストを
一時待機
ストリームから
データを書き込
まれる
write() _write()
callback
空になったら
drain
callback
生産者
WritableStream API methods
WritableStream 返り値
実
装
者
用
_write(chunk, encoding, cb) ストリームからリソースへデータを書き出す
関数。実装者が利用です。完了したら cb を実
行。
書き込みにエラーがあったら 第一引数をエ
ラーにして cb を実行する。
生
産
者
用
write(chunk, [encoding], [cb]); WritableStream を経由してリソースにデータ
を書き込む。書き込みが完了したら cb が起動
される。
true hwm以下
false hwm 以上
end([chunk], [encoding], [cb]); 書き込み終了を実行する。リソースがすべて
書き込まれたら finish イベントが発火する。
WritableStream API events
イベント名 説明 引数
drain WritableStream の内部バッファが空になったことを
示すイベント
N/A
error エラーが発生したイベント エラーオブ
ジェクト
finish end() 後全てのデータの書き込みが完了したことを示
すイベント
N/A
pipe パイプがつながったことを示すイベント source
unpipe パイプが外されたことを示すイベント source
一番簡単なサンプル(同期)
var dest = [];
var Writable = require(‘stream’).Writable;
Writable.prototype._write = function(chunk, encoding, cb) {
dest.push(chunk); // 同期
cb(null);
};
var wstream = new Writable();
var chunk = new Buffer(“abc”);
wstream.write(chunk, function(err) {
if (err) throw err;
console.log(“write finished”);
});
wstream.on (‘finish’, function() ,
console.log(‘dest=‘ + dest);
});
wstream.end();
生産者
実装者
書き込み
先
同期書き込みサンプルの動き
単一書き込み→完了
書き込み先
var dest = [];
wstream._write(chun
k, encoding, cb)
wstream.write(chunk,
wcb)
消費者
実装者
dest.push(chunk)
cb(err) wcb(err)
内部バッファ
同期書き込
み完了
同期書き込みサンプルの動き
複数書き込み
書き込み先
var dest = [];
wstream._write(chun
k, encoding, cb)
wstream.write(chunk1,
wcb1)
消費者
実装者
内部バッファ
書き込み中
wstream.write(chunk2,
wcb2)
chunk1
同期書き込みサンプルの動き
複数書き込み
書き込み先
var dest = [];
wstream._write(chun
k, encoding, cb)
wstream.write(chunk1,
wcb1)
消費者
実装者
内部バッファ
書き込み完了
wstream.write(chunk2,
wcb2)
内部バッファ中で
待機中の書き込み
リクエストを実行
chunk2
同期書き込みサンプルの動き
複数書き込み
書き込み先
var dest = [];
wstream._write(chun
k, encoding, cb)
wstream.write(chunk1,
wcb1)
消費者
実装者
内部バッファ
空
書き込み完了
wstream.write(chunk2,
wcb2)
内部バッファ中で
待機中の書き込み
リクエストを実行
wcb1(err)
wcb2(err)
drain
イベント
同期書き込みサンプルの動き
finish イベント
書き込み先
var dest =
*“abc”+;
wstream.end()
消費者
実装者
内部バッ
ファ
finish
イベント
内部バッ
ファが空
var dest = [], chunks = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
var Writable = require('stream').Writable;
Writable.prototype._write = function(chunk, encoding, cb) {
setTimeout(function() {
dest.push(chunk); cb(null);
}, 1000);};
var wstream = new Writable();
var sWrite = func tion( c ) {
wstream.write(c, function(err) {
if (err) throw err;
console.log("write(" + c + ") completed");
});};
for(var i = 0; i < chunks.length; i++) sWrite(chunks[i]);
wstream.on('finish', function() { console.log('dest=' + dest); });
wstream.end();
非同期書き込みサンプル
課題4
オレオレ パイプを作ろう
Readable
stream
hwm = 1
Writable
Stream
hwm = 1
src dst
ランダム
(MAX10秒)で
非同期読み
出し
ランダム
(MAX10秒)で
非同期書き
出し
_read
read/
write
_write
var src = *‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’+; var dst = [];
DuplexStream
Readable/Writable の多重継承
events.EventEmitter
stream.Stream
stream.Readable stream.Writable
stream.Duplex
net.Socket tls.CleartextStream
Readable用 Writable用
DuplexStream
ReadableとWritableが独立してるだけ
(allowHalfOpenオプションが追加されてます。 defaultが true なのでどちら
か(Readable/Writable)が end しても片方は終了せず、そのままです。)
課題5
エコーバックストリーム
DuplexStream
書き込み
読み込み
あるデータを書き込んだら、すぐ読
みだされるストリームオブジェクト
を作ろう。
TransformStream
TransformStream
stream.Duplex
stream.Transform
書き込まれたデータを変換処
理して読みだせるストリーム
TransformStream API methods
TransformStream 返り値
実
装
者
用
_transform(chunk, encoding, cb) 変換処理を実装する関数。 write(chunk) さ
れたデータが chunk として渡されるので
chunk を実装者で好きに処理して
push(chunk) する。
終わったら cb() して chunk の処理が終
わったことを伝える。
_flush(cb) ストリームデータのデータ変換の最後に
追加の処理をしたいときに実装する。終
了したら cb() を実行。その直後に finish イ
ベントが発火する。
簡単なサンプル(一文字シフ
ト)var Transform = require('stream').Transform;
Transform.prototype._transform = function(chunk, encoding, cb) {
for(var i = 0; i < chunk.length; i++) chunk[i]++;
this.push(chunk);
cb();
};
Transform.prototype._flush = function(cb) {
this.push(new Buffer('|saigo|'));
cb();
};
var tstream = new Transform();
var data = '';
tstream.on('readable', function() {
var b = tstream.read();
if (b) data += b;
});
tstream.on('end', function() {
console.log('transformed =' + data);
});
tstream.end(new Buffer('abc'));
変換
送り込み
完了通知
TransformStream の動き
tstream._transform(chunk,
encoding, cb)
tstream.write(chunk,
wcb);
tstream.end();
消費者
実装者
this.push(変換後データ)
cb(err)
Writable
内部バッファ
Readalbe
内部バッファ
データ変換処理
tstream.on(‘readable,
function() {
var b = tstream.read();
}
);
課題6
なにも変換をしない TransformStream を作っ
てみる。
課題6
書き込まれたデータの中で [a-zA-Z] を rot13
する Transform stream を作成しなさい。
PassThroughStream
PassThroughStream
stream.PassThrough
stream.Transform
データの変換をしない
TransformStream
課題6で作ったのと同じ
おわり
Q&A

Stream2の基本