🍃このブログは移転しました。
3秒後、自動的に移動します・・・。

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

GitHub - nodertc/nodertc: [WIP] WebRTC Datachannels for Node.js

JavaScriptで書かれたWebRTCの実装で、現時点ではDataChannelのみ実装されてます。

WebRTCスタックの実装、興味はあって前々から読んでみたいとは思ってたものの、RFCの数も多いし高い壁よね・・。
というところで、DCだけやしコードもJSやし、これならなんとかなるんでは?という。
QUICがきても・・この経験は・・きっと無駄にはならな・・。

必要なRFCをうまくたどって読むのが大変そうなので、実装を先に読めばそのへんの雰囲気がつかめるのでは?という主旨のシリーズです。

ちなみに読んだバージョンは、`0.1.0`です。

まず試してみる

サンプルが用意されてるのでそれで。

  • `git clone`する
  • `npm i`する
  • `npm start`する
  • `localhost:7007`をひらく
  • DevToolsから、`channel.send('hello')`とかしてみる
    • `npm start`したコンソールになんか出るのを確認する
  • コンソールになんか入力してみる
    • ブラウザ側からなんか出るの確認する

ブラウザとサーバー間でP2Pしてるだけなのでコードもシンプル。

20181128追記:
コードを読み始めた時は送受信の機能はなかったけど、ついに実装されました!

サーバー側

`example-express.js`より。

`express`のサーバーのサンプルなので、使い方は見ればわかるはずやけど一応。

const nodertc = require('nodertc');

// サーバーを立てて待機
const rtc = nodertc();
await rtc.start();

// offerをもらったら新規セッションを作る
const session = rtc.createSession();
// このanswerをクライアントに送り返す
const answer = await session.createAnswer(offer);

rtc.on('session', session => {
  session.once('channel', channel => {
    // 送信
    channel.write(line);

    // 受信
    channel.on('data', data => {
      console.log(`${data.toString()}`);
    });
  });
});

今のところはVanilla ICE専用っぽい。

ブラウザ側

`fixtures/client.js`より。

  • `localhost:7007`をひらく
  • そこで`createDataChannel()`
  • `negotiationneeded`で`createOffer()`と`setLocalDescription()`
  • VanillaICEで収集待って、そのあとサーバーにOfferを送る
  • サーバーはそれを受けてAnswerを返す
  • `setRemoteDescription()`してP2P開始

最低限のコードしかないので何も言うことない。

nodertc/nodertc

さて本題。
まずは本丸であるこのリポジトリから。

  • index.js
  • lib/candidates.js
  • lib/fingerprint.js
  • lib/grammar.js
  • lib/ice-util.js
  • lib/sdp.js

Organizationとしての`nodertc`配下にもたくさんのRepoがあって内部的に依存してる。
必要があればその`node_modules`も読む。

lib/

小さな関数がいくつかあるだけ。

`grammar.js`は、`ice-util.js`のために存在する。ICEのユーザー名とかパスワードとかの文字列を生成するやつ。

index.js

大きく分けて2ついる。

  • NodeRTC
  • Session

こいつらは後述するとして、このパッケージとしては、`new NodeRTC(options)`する関数を返してるだけ。

class: NodeRTC

100行弱。

主要なのはこの2つで、あとはGetterがついてるだけ。

  • start(options)
  • createSession()

つくったSessionを格納したり、オプションで`certificate`と`certificatePrivateKey`を受け取って、あとでSDPに載せる時に使ってる。

start(options)

このサーバーのIPを決めてるだけ。

ここにも卿がいらっしゃった・・。

createSession()

セッションを作って返して、セッションが閉じたらインスタンスを破棄。

特筆すべき点はなし。

class: Session

300行ちょい。

`createAnswer()`のほかにもいくつかメソッドがあるけど、全ての起点はコレ。

あとは`constructor()`でUDPのソケットを用意したり、ICEのユーザー名・パスワードを用意したり。

createAnswer(offer)

これを接続してきたピアごとにやってる。

接続してきたピアごとに用意した`unicast`のUDPソケットが全ての基盤で、その上でDTLS、SCTPという構造。

unicastとは

GitHub - reklatsmasters/unicast: Unicast implementation of UDP Datagram sockets.

`dgram.createSocket()`のラッパー。

まさにその名の通りで、最初に指定したHostとPortに合致した`message`だけ通すようになってる。

const unicast = require('unicast');

const socket = unicast.createSocket({
  type: 'udp4',
  port: 2222,
  remotePort: 1111,
  remoteAddress: '127.0.0.1'
});

これで得られる`socket`は、`Duplex`のストリーム。
ラップする前は`Duplex`じゃない。

読んでみて

  • コードは割ときれいで読みやすい
    • ただ`Symbol`で`private`っぽいコード書くならTypeScriptとかにしてほしい
  • コードはわかりやすいが、なぜそうするのかを完全には理解できない
    • こういうところが結局RFC読め案件なんやろな・・

STUN, DTLS, SCTPの実装のほうが本体っぽい予感がしてるので、次回以降はそっちを読んでく。