🧊

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

Part.1はこちら。

NodeJS製WebRTC DataChannel、NodeRTCのコードを読む Part.1 - console.lealog();

この記事では、ついにSCTPのレイヤーへ。

前回までのあらすじ

  • `nodertc/nodertc`を読んでた
  • クライアントとSessionを確立する際に、内部的にいくつかサーバーを立ててた
    • STUN: 読んだ
    • DTLS: 読んだ
    • SCTP: 今回はコレ

というわけで、今回はSCTPの部分。
これも読んでみたら長くなったので、前後編になってます。

使われ方

startSCTP() {
  console.log('[nodertc][sctp] start');

  this.sctp = sctp.createServer({
    transport: this.dtls,
  });

  this.sctp.once('listening', () => {
    console.log('[nodertc][sctp] server started');
  });

  this.sctp.on('connection', socket => {
    console.log('[nodertc][sctp] got a new connection!');

    socket.on('stream', sctpStreamIn => {
      console.log('[nodertc][sctp] got stream %s', sctpStreamIn.stream_id);

      const sctpStreamOut = socket.createStream(sctpStreamIn.stream_id);

      const channel = createChannel({
        input: sctpStreamIn,
        output: sctpStreamOut,
        negotiated: true,
      });

      channel.once('open', () => {
        this.emit('channel', channel);
      });
    });

    socket.on('error', err => {
      console.error('[nodertc][sctp]', err);
    });
  });

  this.sctp.on('error', err => {
    console.error('[nodertc][sctp]', err);
  });

  this.sctp.listen(5000); // Port defined in SDP
}

という感じ。

  • SCTPの`createServer()`に、DTLSのStreamを渡す
  • `on('connection')`で得られる`socket`の`on('stream')`から、DataChannelが作れる
  • `listen()`で起動

サーバーとソケット、それぞれを見ていく必要がありそう。

nodertc/sctp

読んだバージョンは`0.1.0`です。

GitHub - nodertc/sctp: [WIP] SCTP network protocol in plain js

今回は`node_modules`って名前のディレクトリではないらしい。

lib
├── association.js
├── chunk.js
├── defs.js
├── endpoint.js
├── index.js
├── packet.js
├── reassembly.js
├── serial.js
├── sockets.js
└── transport.js

さて、さっき見てたコードは`index.js`からエクスポートされてる。

というわけでまずは`createServer()`からですが、コレは`new Server()`してるだけなので、`Server`を。

class: Server

  • extends `EventEmitter`
  • `transport`のオプションは、`udpTransport`と名前を変えて保持される

`constructor()`ではほぼ何もしてない。
続いて`listen()`を見てみる。

  • 実態は`_listen()`
  • `Endpoint.INITIALIZE()`という大仰なやつを呼んでる
    • `udpTransport`も渡される
  • `on('association')`でサーバーが立って、`Socket`が返る
    • `emit('connection', socket)`

`Endpoint`ってやつが何者なのかと、`Socket`を追う。

class: Endpoint

`INITIALIZE()`と`on('association')`を見る。

  • `INITIALIZE()`
    • `new Endpoint()`してる`static`メソッド
    • `transport.register(endpoint)`

`Endpoint`本体はというと、

  • extends `EventEmitter`
  • `constructor()`では特に副作用はない
    • `on('icmp')`と`on('packet')`くらい
  • `this.udpTransport`も、このクラスでは使われてない

なので`transport.register(endpoint)`の先で何かやってるはず。

transport.register()

この関数だけを返してるファイル。

  • `WeakMap`に`endpoint.udpTransport`を保持してる
  • `endpoint.transport`に`UDPTransport`をセットして、それの`register()`を呼ぶ

コードを見るに、同じピアから複数回呼ばれることを想定してる。

class: UDPTransport / Transport

  • extends `Transport`
  • `udpTransport`を受け取ったあと、`on('data')`で`receivePacket()`

この`on('data')`がおそらくきっかけになって動き出す。

`register()`も`receivePacket()`も、`Transport`クラスに定義されてるやつ。

  • `Transport`の`constructor()`
    • `pool`に`port`ごとに`endpoint`を保持
  • `register()`
    • さっきの`pool`にポートを割り当てるだけ
    • `endpoint.localPort`が埋められて返される
  • `receivePacket()`
    • `endpoint.emit('packet')`

さっきの`on('packet')`がココでつながる!

続 class: Endpoint

`on('packet')`で呼ばれるのは、`onPacket()`。

ただこいつが270行くらいある重い感じの処理・・。

ようは、

  • 届いたパケットをそのまま流していいかチェックするのが仕事
  • ついでにデコードして後で使おうとしてる
  • ココで唐突に出てくる`association`という概念
    • これがないなら`onInit()`
    • あるなら`onCookieEcho()`
    • もしくは`sendPacket()`
  • 送られてきたパケット、`Chunk`によって処理が変わるっぽい

Associationとはなんぞや?というと、SCTPのエンドポイントをペアにして扱う単位みたいなもので、各エンドポイント = ノードは単数かもしれないし複数かもしれない・・みたいなことがRFCに書いてあった。

`Association`のクラスは、2000行くらいあって読めたもんじゃなかったので割愛。

ちなみにさっきの`onPacket()`のくだりで、`onCookieEcho()`にたどり着いた場合、最初のほうで待ってた`on('association')`が発火するようになってる。

`association.acceptRemote()`で内部的な状態を初期化してる。

これでやっとSCTPのSocketが手に入る・・。

ざっくり

ここまできてふと思った。

RFCを読み込まないまでも、ざっくりした情報を知っておくともっと読みやすかったなーと。

SCTPによるネットワーキングの向上

たとえば、

このあたりを知ってると、コードに出てきた`endpoint`とか`association`とかも理解できたので。

いやー、RFC読むって大変・・。

続きは後編へ。