🧊

NodeJS製WebRTC DataChannel、NodeRTCのコードを読む Part.4

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`
  • `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をご紹介。

DTLS Discussion · Issue #2398 · nodejs/node · GitHub

要約すると、

  • NodeJSのコアでDTLSを実装したいよね
  • でも実装するの大変よね
  • TLSとDTLSって微妙に違うくてどうしたものか
    • UDPはStreamベースでもないし
    • `dgram`にも手をいれないといけないかも
    • 実装するならEventEmitterベースかな
  • Nodeのinternalなレイヤーで、`TLSWrap`みたいなの作るのが筋良い?
  • そもそもNodeよりも`libuv`のレイヤーのほうが嬉しくない?
    • `libuv-extra`っていうアイデアはどうだろう
  • イデア出すしレビューはできると思うけど、実装は重すぎるからできそうにない
    • という人がほとんど
  • 〜それから半年〜
  • NodeRTCの作者: DTLSを実装してみたよ
    • しかしコミュニティからの反応なし
  • 議論止まってるからCloseするね <- いまここ

という感じで、現時点だと自力で実装するしかないという結論に。

読んでみて

  • `Stream`まわりに慣れ親しんでないと書けないし読めない
    • 特に`Duplex`はどっち方向の処理に関するコードなのか読み分けないと厳しい
  • コードコメントの有るところ無いところあるので、仕様を知らずに読むのまじつらい
    • 型があればエディタで補完しつつ読めて多少マシ
  • 既にだいぶ重いけど、100%の仕様を満たしてない予感もする
    • TLSを実装するとはそういうこと
  • まあでもこれだけの行数で実装できるなら、思ってたよりかはマシ

次はSCTPなんですが、毛色は違うけどDTLS以上に重い実装な予感がしてて震えてる!