Part.1はこちら。
NodeJS製WebRTC DataChannel、NodeRTCのコードを読む Part.1 - console.lealog();
この記事では、前回から読んでるDTLS部分の後編を。
前編のおさらい
- DTLSのソケットがつながる過程を追ってた
- `Socket`がその本丸
- その`constructor()`でやってたことを追ってたのが前回
- `ClientSession`および登場人物がいっぱいいた
- 追えてない残りが`pipeline()`で`Stream`をまとめてること
具体的にはこのコード。
pipeline(writer, socket, onerror); pipeline(socket, isdtls, decoder, reorder, defrag, protocol, onerror);
そもそもDTLSの層は初期化の時点で渡してる`unicast`のUDPソケットの土管でしかない。
- この`unicast`ソケットを`pipe()`か`on('data')`してデータを受け取って
- この`unicast`ソケットに大して`write()`してる
この2箇所がどこかにあるはずで、それがこの`pipeline()`ってわけ。
最初の`pipeline()`では`Writable`として、後者のは`Readable`として使われてることになる。
`pipeline(writer, socket, onerror);`
変数名が違うけど、`writer`は`new Sendor(session)`なので、クラスとしての`Sender`を追えば良さそう。
class: Sendor
- extends `Readable`
- しかし`_read()`は空っぽ
- 代わりにどこかで`this.push()`してるはず
- 実態はおそらく`constructor()`で受け取った`session`からデータを供給する存在
- `this.push()`してるのは主に`_bufferDrain()`
- これは`output.record.on('data')`で呼ばれる
- `output.record`
- GitHub - reklatsmasters/binary-data: Declarative encoder/decoder of various binary data. の`createEncodeStream()`
- これの`write()`でデータが流れる
- `sendRecord()`という関数がそれ
- 各所で呼ばれてるが、メインは`_applicationData()`
- これは`session.on('send:appdata')`で呼ばれる
- これが`emit()`されるのは、`session.sendMessage()`
- そしてこれは`socket._write()`すると呼ばれる
つまり・・・、
// socket from socket.js socket._write() session.sendMessage() this.emit('send:appdata') // writer from sender.js session.on('send:appdata') this.sendRecord() this.output.record.write(record); this.output.record.on('data') this[_bufferDrain](packet) this.push(packet)
これで回り回って、`pipeline()`につないだ次の`socket`(つまり接続ピア)にデータが流れてくことになる。
最初に呼ばれるであろう`socket._write()`はこうなってた。
_write(chunk, encoding, callback) { if (this[_session].isHandshakeInProcess) { this[_queue].push(chunk); this.once('connect', () => callback()); } else { this[_session].sendMessage(chunk); callback(); } }
ハンドシェイクが終わるまでキューに貯めてる。
`Writable._write()`は、1つ上の層(おそらくSCTPの実装)で使われるてるはず。
`socket.write()`的な感じで。
binary-data
GitHub - reklatsmasters/binary-data: Declarative encoder/decoder of various binary data.
作者謹製のライブラリで、頻繁に使われてるので軽く見ておく。
- バイナリに対してスキーマを定義できる
- それを`encode()` / `decode()`することで、バイナリを意識しないでいい
- `Transform`ストリームも作れる
const protocol = { type: uint8, value: array(string(null), uint8), }; const message = { type: 12, value: ['foo', 1], }; socket.on('message', msg => { const packet = decode(msg, protocol); }); socket.write(encode(msg, protocol));
まあ確かにこういうの欲しいよねーという感じ。
`pipeline(socket, isdtls, decoder, reorder, defrag, protocol, onerror);`
もう1つの`pipeline()`がコレ。
- 先頭の`socket`は`session.on('data')`で`this.push()`
- `isdtls`から`defrag`までは`Transform`ストリーム
- `isdtls`
- STUNの時にもあった「パケットの先頭のbyteを見て、DTLSのそれかどうか」を判別するやつ
- `decorder` / `reorder` / `defrag`
- `filter/*.js`にあるやつら
- 内容は割愛するけど、おそらく名前どおり、暗号を解いて送信順序を整えて・・みたいなことをしてる
- `protocol`
- `fsm/protocol.js`
目的地である`Writable`な`protocol`は次で読む。
class: Protocol12ReaderClient
変数`protocol`の中身。
- extends `Writable`
- `constructor()`で`session`を受け取る
- `session.handshakeProtocolReaderState`で挙動が変わる
- `_write()`で最終的にデータを流すかどうか判断してる
- 疎通後のデータは`this.session.packet()`で流される
- `session.emit('data')`
これがまたおそらく次に読むSCTPのどこかで`on('data')`されてるはず・・。
そもそも
コードを読みながらいくつか疑問があった。
- NodeJSには`tls`のモジュールが標準で存在するけど、それは使わない?
- そもそも自前で実装するしかない?
で、それを調べてたときに見つかったIssueをご紹介。
要約すると、
- NodeJSのコアでDTLSを実装したいよね
- でも実装するの大変よね
- TLSとDTLSって微妙に違うくてどうしたものか
- UDPはStreamベースでもないし
- `dgram`にも手をいれないといけないかも
- 実装するならEventEmitterベースかな
- Nodeのinternalなレイヤーで、`TLSWrap`みたいなの作るのが筋良い?
- そもそもNodeよりも`libuv`のレイヤーのほうが嬉しくない?
- `libuv-extra`っていうアイデアはどうだろう
- アイデア出すしレビューはできると思うけど、実装は重すぎるからできそうにない
- という人がほとんど
- 〜それから半年〜
- NodeRTCの作者: DTLSを実装してみたよ
- しかしコミュニティからの反応なし
- 議論止まってるからCloseするね <- いまここ
という感じで、現時点だと自力で実装するしかないという結論に。
読んでみて
- `Stream`まわりに慣れ親しんでないと書けないし読めない
- 特に`Duplex`はどっち方向の処理に関するコードなのか読み分けないと厳しい
- コードコメントの有るところ無いところあるので、仕様を知らずに読むのまじつらい
- 型があればエディタで補完しつつ読めて多少マシ
- 既にだいぶ重いけど、100%の仕様を満たしてない予感もする
- TLSを実装するとはそういうこと
- まあでもこれだけの行数で実装できるなら、思ってたよりかはマシ
次はSCTPなんですが、毛色は違うけどDTLS以上に重い実装な予感がしてて震えてる!