読者です 読者をやめる 読者になる 読者になる

望月いちろうのREADME.md

書き溜めておいた技術記事や旅行記のバックアップです。

WebRTCの現状 - STUN/TURN・シグナリングサーバーについて

Sam Dutton

2013/12/4

WebRTCでP2Pができるようになる・・

でも・・

WebRTCにもやはりサーバーが必要なんだ。それは

  • クライアント同士が通信できるようにメタデータを交換しないといけないから

  • ネットワークのアドレス変換(NAT)とファイアーウォールに対応するために

この記事ではシグナリングをどのように行うのか、そしてSTUN/TURNサーバーを実際に利用した時のネットワークの奇妙な振る舞いについて説明したいと考えている。おまけにWebRTCを利用したアプリケーションが複数人との接続をどのように扱い、VoIPやPSTNのようなサービスとどのような関係があるのかを解説することしたいと思う。

もしWebRTCの基礎について知識の無い場合は、WebRTCの始め方を事前に読むことをお勧めしたい。

シグナリングとは?

シグナリングとは通信処理を設定する手続きのこと。つまりWebRTCを利用するアプリケーションが通信相手と情報を交換するための準備をすることだ。その内容は

  • 通信を開始・終了するためのセッションコントロールメッセージ
  • エラーメッセージを表示
  • メディアのメタデータ、たとえばコーデックやコーデック設定、帯域やメディア形式など
  • セキュアな通信を確立するために鍵情報
  • WANから見た時のホストのIPアドレスやポート番号情報

シグナリング処理にはメッセージを交換するための方法が必要で、そのメカニズムはWebRTCのAPIには実装されていない。このアプローチはJSEP(JavaScript Session Establishment Protocol)で概要が定められている。

WebRTCの思想の背景にあるのはメディアプレーンをコントロールして十分に特定することだ、しかしシグナリングプレーンをアプリケーションからできるだけ離しておきたい。この理論的根拠は異なるアプリケーションは異なるプロトコルを利用することが望ましいかもしれない。ということだ。たとえば、SIPやJingleの通話シグナリングプロトコルや他の特定のアプリケーション、あるいは新規の利用法に対応するためだ。このアプローチで交換される重要な情報はマルチメディアセッションの詳細で、その内容はメディアプレーンを確立するために必要な通信やメディア設定情報を特定するものである。

JSEPの設計はブラウザが状態を保つことを避けるようにしている。つまり、signaling state machineとして機能するように定義している。これは問題を起こすかもしれない。たとえばシグナリングデータはページが再読み込みされるたびに情報を失われてしまうだろう。その代わり、シグナリングの状態はサーバーに保存される。

f:id:mochizuki_p:20160908105618p:plain

JSEPはピア間の要求と答えの交換を必要としている。つまり、上で挙げたメディアのメタデータのことだ。要求と答えはSDP(Session Description Protocol format)で交換される。その内容の例を以下に挙げる。

v=0
o=- 7614219274584779017 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126
c=IN IP4 0.0.0.0
a=rtcp:1 IN IP4 0.0.0.0
a=ice-ufrag:W2TGCZw2NZHuwlnf
a=ice-pwd:xdQEccP40E+P0L5qTyzDgfmW
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=mid:audio
a=rtcp-mux
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQe
a=rtpmap:111 opus/48000/2
…

このちんぷんかんぷんなSDPにどんな意味があるのか 知りたい? それならばIETFの例を見てほしい。

肝に銘じて欲しいのは、WebRTCは、SDPの値を編集することで、ローカルとリモートの詳細が設定される前に、要求または答えが調節されるようにデザインされているということだ。

たとえば、apprtc.appspot.comではpreferAudioCodec() 関数 がデフォルトのコーデックとビットレイトを設定するために利用されている。

SDPはJavaScriptで扱うには少々骨が折れるので、将来のWebRTCではJSONを代わりに利用するべきだという議論がある。

しかしそれでもSDPにこだわる理由がいくつかある。

RTCPeerConnection + signaling: オファー、アンサーとキャンディデート

RTCPeerConnectionはWebRTCアプリケーションがピア間で音声や動画を交換するために利用するAPIのことだ。

このRTCPeerConnectionのプロセスを初期化するためには2つのタスクがある。  

  • ローカルのメディアの状態を確認する必要がある。たとえば解像度やコーデックの容量のことだ。これは要求と答えのためのメタデータとして利用される。

  • 「キャンディデート」として知られるアプリケーションのホストのための潜在的ネットワークアドレスを取得すること

一度ローカルデータを確認したら、それはシグナリング機構を介して、リモートピアと交換されなければならない。

アリスがイブに電話をかけるとしてみよう。ここではオファー・アンサーのメカニズムの全貌が見えてくる。

  1. アリスは RTCPeerConnectionオブジェクトを生成する。

  2. アリスはRTCPeerConnectionのオファーをcreateOffer()関数で生成する。

  3. アリスはsetLocalDescription()関数でオファーを相手に送信する準備をする。

  4. アリスはオファーをイブにシグナリング機構を利用して送信する。

  5. イブはsetRemoteDescription()でアリスのオファーに返信する。これでRTCPeerConnectionはアリスの設定の詳細を確認することができる。

  6. イブは createAnswer()を呼び出して、通信が成功したことを伝えるコールバックを発信する。

  7. イブはsetLocalDescription()を呼び出すことでlocal descriptionとしてアンサーを設定する。

  8. イブはシグナリング機構を利用してアリスにアンサーを送信する。

  9. アリスはsetRemoteDescription()でイブのアンサーをリモートセッションの詳細としてセットする。

アリスもイブもネットワーク情報を交換する必要がある。キャンディデートを発見することを表現するのに ICE の枠組みを利用してネットワークのインターフェイスとポートを発見するプロセスを参照する。

  1. アリスはRTCPeerConnectionObjectをonicecandidate handlerと一緒に作成する。

  2. onicecandidate handlerはネットワークのキャンディデートが利用可能になったときに呼ばれる。

  3. onicecandidate handler 内部でアリスはシグナリングチャンネルを経由してキャンディデートデータをイブに送信する。

  4. イブがアリスからキャンディデートメッセージを受信したら、addIceCandidate()関数を呼び出してそのキャンディデートをリモートピアの詳細に追加する。

JSEPがサポートするのはICE Candidate Tricklingで、それは呼び出しをした側が呼び出しの対象に対して最初のオファーの後にキャンディデートを提供することを段階的に許可し、かつ呼び出しの対象に通信を開始し、すべてのキャンディデートが到着するのを待つことなく、コネクションを確立することを可能にする。

WebRTCのシグナリングをコーディングする

以下のコードは完全なシグナリングプロセスを完了するために手順を要約するものである。このコードはいくつかのシグナリング機構(SignalingChannel )の存在を前提としている。シグナリングの詳細はこの記事の後半で説明する。

var signalingChannel = new SignalingChannel();
var configuration = {
  'iceServers': [{
    'url': 'stun:stun.example.org'
  }]
};
var pc;

// call start() to initiate

function start() {
  pc = new RTCPeerConnection(configuration);

  // send any ice candidates to the other peer
  pc.onicecandidate = function (evt) {
    if (evt.candidate)
      signalingChannel.send(JSON.stringify({
        'candidate': evt.candidate
      }));
  };

  // let the 'negotiationneeded' event trigger offer generation
  pc.onnegotiationneeded = function () {
    pc.createOffer(localDescCreated, logError);
  }

  // once remote stream arrives, show it in the remote video element
  pc.onaddstream = function (evt) {
    remoteView.src = URL.createObjectURL(evt.stream);
  };

  // get a local stream, show it in a self-view and add it to be sent
  navigator.getUserMedia({
    'audio': true,
    'video': true
  }, function (stream) {
    selfView.src = URL.createObjectURL(stream);
    pc.addStream(stream);
  }, logError);
}

function localDescCreated(desc) {
  pc.setLocalDescription(desc, function () {
    signalingChannel.send(JSON.stringify({
      'sdp': pc.localDescription
    }));
  }, logError);
}

signalingChannel.onmessage = function (evt) {
  if (!pc)
    start();

  var message = JSON.parse(evt.data);
  if (message.sdp)
    pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
      // if we received an offer, we need to answer
      if (pc.remoteDescription.type == 'offer')
        pc.createAnswer(localDescCreated, logError);
    }, logError);
  else
    pc.addIceCandidate(new RTCIceCandidate(message.candidate));
};

function logError(error) {
  log(error.name + ': ' + error.message);
}

オファーとアンサー、candidateを交換するプロセスの実際を理解するために、シングルページビデオチャットの simpl.info/pcのconsole.logをみてみよう。

さらに詳しく知りたいのだったら、WebRTCの完全なシグナリングと統計データのdumpファイルをダウンロードするといいだろう。

これはChromeではchrome://webrtc-internals ページを参照すればよいし、operaだったら opera://webrtc-internalsを見ればいい。

Peer を見つけ出す

これは思いつきの言い回し - どうやって通話する相手を見つければいいのだろうか?

電話だったら電話番号と電話帳がある。オンラインビデオチャットだったら、アカウントとその管理法、そしてセッションを開始する方法が必要だ。

WebRTCはクライアントに通知を送って通話に参加してもらう必要がある。 Peerを見つけ出すメカニズムはWebRTCそれ自体には実装されていない。そしてここではその選択肢には立ちいらないことにしよう。

そのプロセスはemailやカスタムリンクを共有することで通話ができるtalky.io, tawk.comまたはbrowsermeeting.comのようなビデオチャットアプリと同じくらい単純なものだ。

開発者のChris Ballは興味深い、サーバーを使用しないWebRTCの実験をした。その内容はピア間でe-mailや伝書鳩のような伝達手段を使ってメタデータを交換するものだ。

どのようにシグナリングサービスを構築するのか?

もう一度言おう、シグナリングのプロトコルとメカニズムはWebRTCの規格には含まれていない。

何を選ぼうとあなたは中継サーバーが必要で、それはシグナリングのメッセージとクライアント同士のアプリケーションデータを交換する。

残念なことにWebアプリは単純にインターネットに「僕の友達と繋いでくれ!」と叫ぶだけでは不十分だ。

ありがたいことに、シグナリングのメッセージは短いし、大半の手続きは通話の最初に行えばよい。

apprtc.appspot.comやsamdutton-nodertc.jit.su で試してみよう。

この場合、ビデオチャットのセッションを通して大体30から45のメッセージがシグナリングの過程で交換された。すべてのメッセージの容量は10KBほどだった。

帯域の観点からみると望ましいことだけど、WebRTCのシグナリングはあまり多くの処理を行わない、ただメッセージを中継して小さなセッション情報(どのクライアントが接続しているかなど)を保持するだけだ。

メッセージをサーバーからクライアントに送信する

シグナリングのメッセージサービスは双方向的である必要がある。つまりクライアントからサーバー、サーバーからクライアントへと。

この双方向の通信モデルはHTTPのクライアント・サーバー、リクエスト・レスポンスモデルと対照的だ。

しかし、ブラウザで動作するWebアプリにサーバーからデータをプッシュするために長年の間にlong pollingのような様々な工夫が考案されてきた。

もっと最近の話をすれば、 EventSource APIは幅広く実装されてきた。これはサーバーから送信されたイベントをHTTPを仲介してブラウザに通知する。

ここにその簡単なデモアプリを用意した。 simpl.info/es.

EventSourceは一方通行のメッセージングのために設計された。しかし、これはXHRと組み合わせてシグナリングメッセージを交換するサービスに利用することができるかもしれない。

つまり電話をかける側のメッセージをXHRで通知して、それをEventSourceで電話を受ける側にプッシュする。

完全な2重のクライアント・サーバー間通信(メッセージが同時に2方向に流れていく)のために設計されたWebSocketはより自然な解決法だ。シグナリングをWebSocketのみで、あるいはEventSourceを利用して設計する利点はこの種のAPIがたとえばPHPやRuby、Pythonのような大半のホスティングパッケージに普通に実装されていることだ。

概ね4分の3のブラウザがWebSocketをサポートしている。さらに重要なことは、WebRTCを実装しているブラウザは、デスクトップでもモバイルでも、すべてWebSocketを使うことができる。

TLSは、プロキシ透過問題(詳しい情報はHigh Performance Browser NetworkingのWebSocket and proxy traversal see the WebRTC chapter を見て欲しい)と暗号化されていないメッセージを遮断されないために利用すべきである。

以下続き

WebRTCを利用したビデオチャットアプリケーションはGoogle App Engine Channel API,を利用してシグナリングを行っている。

これは Comet (long polling)を利用してバックエンドの App Engine (余談ながらこのApp EngineのwebSocketサポートには、長い間修正されていないバグがある。)とクライアントのシグナリングを可能にしている。

そしてこの記事にはコードの簡単な説明がある。

f:id:mochizuki_p:20160908105614j:plain

一つ付け加えておくと、WebRTCのクライアントがメッセージングサーバーに繰り返しAjaxでリクエストを送ることでシグナリングを行うことも可能だけど、これはかなり余計なネットワークリクエストを必要とするので、特にモバイル環境で問題を引き起こすかもしれない。たとえセッション確立後でも、ピアは他のピアのセッション終了や変更に備えてシグナリングメッセージを出し続けなければいけないからだ。この WebRTC Book のアプリの例は polling の回数の最適化を施した上でこの手段を利用している。

シグナリングをスケーリングする

シグナリングサービスはあまり帯域とCPUパワーを消費しないとはいえ、ほとんどのアプリでシグナリングサーバーは同時間帯に、さまざまな場所からの大量のメッセージを仲介する。WebRTCを利用するアプリケーションは相当な負荷にたえるためにシグナリングサーバーを必要とする。

ここでは詳細を説明するのは避けておきたいけれど、ハイパフォーマンスなシグナリングサーバーを実現するには複数の選択肢がある。

  • XMPP(eXtensible Messaging and Presence Protocol)、当初はJabberと言う名前だった。インスタントメッセンジャーのために開発されたプロトコルで、サーバー実装は ejabberd と Openfireを含み、Strophe.jsのような JavaScriptでのクライアントはBOSHを利用して双方向のストリーミングをエミュレートしている。しかしBOSHはWebSocketには向いていないかもしれない。これはあまりスケーリングがうまくできないという理由と同じものだ。(これは見方を変えれば、Jingleは音声と動画を扱えるようにしたXMPPの拡張で、WebRTCプロジェクトはネットワークとJingleのC++実装であるlibjingleライブラリを利用している。)

  • ZeroMQやその概念を、STOMPプロトコルをWebSocketを介して使用して、Webに応用したOpenMQ、NullMQのようなオープンソースライブラリ

  • WebSocket(long pollingに回帰するかもしれないけれど )を利用した商用クラウドメッセージングプラットフォーム、たとえばPusherやKaazing、PubNub(これはWebRTC用のAPIがある)

  • vLineのような商用WebRTCプラットフォーム

開発者のPhil LeggetterのReal-Time Web Technologies Guide はこの種のメッセージんグサービスとライブラリを網羅している

シグナリングをNode.jsのSocket.ioで構築する。

以下のコードはNode.jsのWebSocket実装であるSocket.ioで構築されたシグナリングサービスです。Socket.ioの設計はメッセージ交換サービスをシンプルに構築できるようにすることです。これはその「ルーム」というアイデアのため、特にWebRTCのシグナリングに適しています。この例はスケーラビリティを考慮していませんが小規模であれば十分に動作します。

Socket.ioはさまざまな環境で実装されています。たとえばAdobe Flash SocketやAJAX long polling、AJAX multipart streaming,そしてForever IframeやJSONP pollingです。これらはさまざまなバックエンドで活躍していましたが、Node.jsでの実装が一番有名です。

この例にはWebRTCのコードはありません。それはWebアプリでどのようにシグナリングサービスを構築するかを説明するためです。

console.logをみてクライアントがルームに参加してメッセージを交換するときに何が行われるか確認してみてください。WebRTC codelab ではこの例をどのようにWebRTCのチャットアプリに組み込むかを教えます。

step 5 of the codelab repo からコードをダウンロードして、試しにt samdutton-nodertc.jit.su:を実行してみてください。

このURLを2つのブラウザで同時に開いてビデオチャットを試してみるといいでしょう。

これがクライアントのindex.htmlです

<!DOCTYPE html>
<html>
  <head>
    <title>WebRTC client</title>
  </head>
  <body>
    <script src='/socket.io/socket.io.js'></script>
    <script src='js/main.js'></script>
  </body> 
</html>

そして、JavaScriptのmain.jsの内容です。

var isInitiator;

room = prompt('Enter room name:');

var socket = io.connect();

if (room !== '') {
  console.log('Joining room ' + room);
  socket.emit('create or join', room);
}

socket.on('full', function (room){
  console.log('Room ' + room + ' is full');
});

socket.on('empty', function (room){
  isInitiator = true;
  console.log('Room ' + room + ' is empty');
});

socket.on('join', function (room){
  console.log('Making request to join room ' + room);
  console.log('You are the initiator!');
});

socket.on('log', function (array){
  console.log.apply(console, array);
});

これがサーバーの実装です。

var static = require('node-static');
var http = require('http');
var file = new(static.Server)();
var app = http.createServer(function (req, res) {
  file.serve(req, res);
}).listen(2013);

var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket){

  // convenience function to log server messages to the client
  function log(){
    var array = ['>>> Message from server: '];
    for (var i = 0; i < arguments.length; i++) {
      array.push(arguments[i]);
    }
      socket.emit('log', array);
  }

  socket.on('message', function (message) {
    log('Got message:', message);
    // for a real app, would be room only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function (room) {
    var numClients = io.sockets.clients(room).length;

    log('Room ' + room + ' has ' + numClients + ' client(s)');
    log('Request to create or join room ' + room);

    if (numClients === 0){
      socket.join(room);
      socket.emit('created', room);
    } else if (numClients === 1) {
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room);
    } else { // max two clients
      socket.emit('full', room);
    }
    socket.emit('emit(): client ' + socket.id + ' joined room ' + room);
    socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);

  });

});

注:node-staticについてここで学ぶ必要はありません。サーバーを単純に書くためだと思ってください。 このアプリをlocalhostで動かすために,まずNode.js、socket.io、node-staticがインストールされていることを確認してください。Nodeは nodejs.org から簡単にインストールすことができます。socket.io、node-staticをインストールするにはコマンドラインから以下のコードを入力します。

npm install socket.io
npm install node-static

サーバーを起動するにはアプリを設置したディレクトリで以下のコマンドを入力します。

node server.js

Chrome,またはOperaのブラウザからlocalhost:2013にアクセスしてみましょう。新しいタブを開き、 localhost:2013を再び開きます。何が起こるかみてみてください。デベロッパーツールを開いて、コンソールをみてください。

どのようなシグナリング手法を使ったとしても、これに似た結果になるはずです。

シグナリングにRTCDataChannelを使用する。

シグナリングを行うにはWebRTCセッションを初期化する必要があります。

しかし、2つのピア間で一度接続が確立してしまえば、理屈の上ではRTCDataChannelはシグナリング経路としてtake overされます。これはシグナリングのレイテンシを損なう可能性があります。

なぜならメッセージを直接転送したとき、シグナリングサーバーの帯域と処理コストを低下させることになるからです。このデモはありませんが、このスペースを監視してください。

シグナリングのgotchas

  • RTCPeerConnectionはsetLocalDescription()が呼び出されるまでcandidatesを収集しない。これについては JSEP IETF draftを参照のこと

  • ICEの取り扱いの利便性のため、candidatesが到着したらaddIceCandidate() を呼び出す。

出来合いのシグナリングサーバー

すでにsocket.ioを利用したWebRTC用のシグナリングサーバーが用意されているので、手間をかけて構築する必要はない。そしてこれらはクライアント用のJavaScriptライブラリともうまく連携するように設計されている。

  • webRTC.io: 初期のWebRTC用の抽象ライブラリ

  • easyRTC: 古スタックのWebRTCライブラリ

  • Signalmaster: クライアントサイドライブラリのSimpleWebRTCと連携して使われるサーバーサイドライブラリ

もしコードを一切書きたくない場合は商用のWebRTCプラットフォームを使用するといいだろう。例としてvLineOpenTokAsteriskのような企業がパッケージを提供している。

レコードを取得したい場合は、エリクソン社が構築したApacheサーバー上で動作するPHPのシグナリングサーバー signaling server using PHP on Apache がある。これは少々時代遅れであるけれど、似たような物を作りたい時の参考になると思う。

セキュリティ

WebRTCで暗号化を行うかは任意である。

しかし、シグナリングの仕組み自体はWebRTCの基準に含まれていないので、シグナリングを安全に行うかはあなた次第である。もしシグナリングサーバーが攻撃者に乗っ取られてしまえば、セッションを停止したり、接続やレコードをリダイレクトしたり、あるいは通信の内容を改ざんされることになる。

シグナリングを安全に行うためにもっとも重要な要素は安全なプロトコルを利用することである。たとえば、HTTPSWSS(TLS)で、これらは暗号化されていないメッセージを盗聴されないように保障する。もうひとつ注意すべきなのはシグナリングメッセージを同じシグナリングサーバーを利用する別の利用者に盗聴されないようにすることである。

シグナリングのあとに - ICEを利用してNATとファイアーウォールに対応する。

メタデータのシグナリングについて、WebRTCは仲介サーバーを利用する。しかし実際のメディアとデータストリーミングでは、一度セッションが確立されたならば、RTCPeerConnectionはピアツーピアで、 クライアントを直接接続することを試みる。

これを単純化すると、すべてのWebRTCエンドポイントはユニークなアドレスをもっていて、ほかのピアと直接通信するためにそれを交換する。

しかし現実には、ほとんどのデバイスは1つないしは複数のNATを通過する。それは特定のポートやプロトコルを遮断するウイルス対策ソフトかもしれないし、プロキシやファイアーウォールを通してあるかもしれない。大抵の場合はファイアーウォールとNATは同じ機器で行われていることが多い。たとえば家庭用wifiルーターのように。

f:id:mochizuki_p:20160908114223p:plain

WebRTCのアプリはICEの枠組みを利用して現実世界のネットワークの複雑性を克服している。このためにアプリケーションはICEサーバーのURLを通過してRTCPeerConnectionに辿りつかないとならない。

ICEはpeer同士を接続するための最短経路を見つけることを試みる。それに並行してあらゆる可能性を試し、もっとも効率的な選択肢を見つけ出す。ICEはまずデバイスのOSとネットワークカードから取得したホストアドレスを使用して接続を試みる。もしこれが失敗したら(NATが利用されていることを意味する)STUNサーバーを利用して外部のアドレスを取得する。これも失敗してしまった場合はTURN中継サーバーを利用して情報を送信する。

つまり

  • STUNサーバーは外部のネットワークアドレスを取得するために使用される

  • TURNサーバーはピアツーピアの通信が失敗した場合に使用される中継サーバーのこと

すべてのTURNサーバーはSTUN機能を実装している。つまり、TURNサーバーは中継機能を実装したSTUNサーバーであるということだ。ICEはNATの複雑性に対応している。実際にNATの「ホールパンチング」はパブリックIPとポート番号だけでは不十分な場合がある。

STUN/TURNサーバーのURLはWebRTCアプリのICEサーバーの設定オブジェクトの第一引数で指定することができる。apprtc.appspot.com の場合は以下のようになる。

{
  'iceServers': [
    {
      'url': 'stun:stun.l.google.com:19302'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=udp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=tcp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    }
  ]
}

一度RTCPeerConnectionがその情報を取得したら,ICEの魔法が自動的に発動する。

つまり、PTCPeerConnectionは、STUNサーバーと連携しながら、ICEの仕組みを使ってピア間の最短経路を見つけ出す。もしこれで不十分ならば代わりにTURNサーバーが使用される。

STUN

NATはプライベートネットワークで利用されるIPアドレスを管理する機能の総称である。しかしここで使用されるアドレスは外部からは利用できない。

グローバルIPアドレスなしでは、WebRTCのピアと通信を行うことができない。この問題を回避するためにWebRTCではSTUNを利用する。

STUNサーバーはWAN上で1つのシンプルな仕事を行う。それはリクエスト元(NATの背後にあるアプリケーション)のIPアドレスとポートをチェックしてその情報をレスポンスとして返すことだ。

言い換えれば、STUNサーバーを利用するアプリケーションはWAN上から見た時の自分のIPアドレスとポートを知ることができる。

このプロセスはWebRTCのピアの公開ネットワーク上でアクセスできる自身のアドレスを取得することを可能にして、それを別のピアにシグナリングを使って通知して、直接的な通信の準備(実際はNATの規格別に異なる挙動を示すが、原理は同じだ)をする。

STUNサーバーはあまり多くの仕事をするわけでも多くの情報を保存するわけでもない。だから比較的低スペックのサーバーでも多くの数のリクエストをさばくことができる。

ほとんどのWebRTCの通信はSTUNのみで接続することができる。その割合は webrtcstats.comによれば概ね86%だ。

とはいえ複雑なNATとファイアーウォールの背後のPeer同士ではその割合は低くなるだろう。

f:id:mochizuki_p:20160908114227p:plain

TURN

RTCPeerConnectionはUDPを使用した直接的な通信を確立することを試みる。もしこれが失敗したらRTCPeerConnectionはTCPでの通信を試みるだろう。そしてこれも失敗したら、TURNサーバーの出番になる。これはエンドポイント間の通信を中継する。

注:TURNはピア間での音声・映像・データのストリーミングを仲介するために使われる。シグナリングではない!

TURNサーバーはグローバルIPを割り当てられている必要がある。これはプロキシやファイアウォールの背後にあるピア間で通信できるためだ。TURNサーバーは概念的には単純な仕事しかしない。

つまり、データーを中継することだ。しかし、STUNサーバーと異なり、多くの帯域を占有する。このためTURNサーバーは高性能である必要がある。

f:id:mochizuki_p:20160908114231p:plain

この図はSTUNが失敗した場合のTURNサーバーの役割をあらわしている。両方のピアがTURNサーバーを介して情報をやり取りする。

STUN・TURNサーバーをデプロイする。

apprtc.appspot.comで利用しているように、試しにGoogleが公開しているパブリックSTUNサーバー(stun.l.google.com:19302 )を使用してみよう。

もし商用でSTUN/TURNサーバーを利用する場合は、 rfc5766-turn-serverを利用することをおすすめする。

これはサーバー設定の親切なガイドもついているので、AWSなどでの利用も簡単だ。

別のTURNサーバーとしてresoundがある。これもAWSで利用可能だ、Google Compute Engineでの利用手順を大まかに説明しよう。

  1. ファイアーウォールを設定を変更するポート443番と3478番を解放する

  2. 4つのインスタンスを設定するそれぞれにグローバルIPを割り当てる

  3. ローカルのファイアウォールを設定を行う(あらゆるANYからのANYを許可する)

  4. ツールをインストールするbash sudo apt-get install make sudo apt-get install gcc

  5. creytiv.com/re.htmlからlibreをインストールする

  6. creytiv.com/restund.htmlからrestundを取得して展開する。

  7. wget hancke.name/restund-auth.patch を実行して-p1 < restund-auth.patchでパッチを適用する。

  8. libreとrestundでmakeを起動する。

  9. restund.confを設定する(IPアドレスを置き換えて共通の暗証番号を設定する)そして/etcにコピーする

  10. restund/etc/resturnを/etc/init.dにコピーする

  11. restundの設定をして、LD_LIBRARY_PATHをセットする。そしてrestund.conf を /etc/restund.confへコピーする

right 10のipアドレスを使用するようにresound.confを設定する

  1. restundを起動する

  2. リモートのクライアントでstundが動作するか確認する。

1対1を超えて、複数間でのWebRTC

Justin Ubertが提案したREST API for access to TURN Services IETF基準を見てみるといいかもしれない。

単なる1対1のメディア配信を超えて、たとえば3人以上の同僚でのビデオ会議、あるいは何百人の視聴者にライブ映像を届けるようなケースを想像してみよう。

WebRTCはこのような複数人に対してのRTCPeerConnectionを利用して網の目のようにエンドポイント間のネットワークを構築することもできる。

この手法はtalky.ioのようなアプリで採用されている。

そしてあまり大きくないネットワークであれば完璧に動作する。しかしそれ以上になると、特にモバイル環境では、処理性能と帯域を圧迫して難しくなる。

f:id:mochizuki_p:20160908114220p:plain

その代わり、WebRTCは一つのエンドポイントに対して別の複数のエンドポイントにデータを配信するようにすることができる。

これはWebRTCのエンドポイントをサーバとして動かすことができ、再送信させるメカニズムを構築することになる。

このサンプルはwebrtc.orgによってsample client applicationとして提供されている。

Chrome31とOpera18から、1つのRTCPeerConnectionからのMediaStream を別のクライアントに送信できるようになった。

これによってより柔軟性のある構造が実現できる。なぜならWebアプリがどのピアと接続するか選択できるようになったからだ。

複数のコントロールユニット

多くのエンドポイントに対してはMCU Multipoint Control Unit を使うことが得策だ。これは複数のクライアントに対してメディアを配信するブリッジとして機能するサーバーである。

MCUはビデオ会議における複数のコーデック、フレームレートを扱うことができ、ストリーミングの再配信や音声や動画のリミックスをすることもできる。マルチパーティの通信では、考慮すべき問題がある。

特にどのように複数のビデオ入力を表示して、音声を合成するかということだ。vLineのようなクラウドサービスではこの最適化を扱うことができる。 これはMCUのハードを購入することで可能であるし、自分で自作することもできる

f:id:mochizuki_p:20160908105623j:plain

オープンソースのMCUも利用可能だ。たとえばLicode(以前はLynckiaという名前)はWebRTC用のMCUで、OpenTokにはMantisがある。

ブラウザを超えて:VoIP、電話、メッセージング

WebRTCの規格によってWebRTCのアプリはいろいろなデバイスの間、つまり、ブラウザやほかの通信機器、たとえば電話やビデオ会議システムで利用することができる。

SIPはVoIPやビデオ会議システムで使用されるシグナリングの規格だ。WebRTCのクライアントとSIPクライアントで通信するためには、WebRTCはシグナリングを仲介するプロキシサーバーが必要となる。

シグナリングはゲートウェイを経由する必要があるけど、一度通信が確立すればSRTPのトラフィック(音声でも動画でも)はピアツーピアで直接やりとりできる。

PSTN(Public Switched Telephone Network)はアナログ回路で動作する古い電話システムだ。

WebRTCと昔ながらの電話と通話をするためにはPSTNゲートウェイを通過する必要がある。同様に、WebRTCのWebアプリがIMクライアントのようなJingleエンドポイントと通信する場合はは仲介役となるXMPPサーバーが必要だ。JingleはGoogleによってXMPPの拡張として開発され、音声や動画をやり取りすることができる。

現在のWebRTC実装は、GoogleがGoogle Talkとして開発したC++のlibjingleライブラリを元にしている。

いくつかのアプリやライブラリ、プラットフォームはWebRTCと通信できるようなAPIを用意している。

  • sipML5 オープンソースのJavaScriptで実装されたSIPクライアント

  • jsSIP JavaScriptのSIPライブラリ

  • Phono オープンソースのJavaScriptの電話APIで、プラグインとして構築された。

  • Zingaya 埋め込み可能なphone widget

  • Twilio 音声とメッセージング

  • Uberconference 会議用

sipML5 の開発者はwebrtc2sipゲートウェイも開発している。TethrとTropoはa framework for disaster communicationsで簡潔にデモンストレーションを行った。OpenBTS cellでフィーチャーフォンとWebRTCでの通話をキャリアなしで行った。

さらに知りたい人には

WebRTC codelab: step-by-step instructions how to build a video and text chat application, using a Socket.io signaling service running on Node.

2013 Google I/O WebRTC presentation with WebRTC tech lead, Justin Uberti.

Chris Wilson's SFHTML5 presentation: Introduction to WebRTC Apps.

The WebRTC Book gives a lot of detail about data and signaling pathways, and includes a number of detailed network topology diagrams.

WebRTC and Signaling: What Two Years Has Taught Us: TokBox blog post about why leaving signaling out of the spec was a good idea.

Ben Strong's presentation A Practical Guide to Building WebRTC Appsprovides a lot of information about WebRTC topologies and infrastructure.

The WebRTC chapter in Ilya Grigorik's High Performance Browser Networkinggoes deep into WebRTC architecture, use cases and performance.

この文章はHTML5 ROCKS から 翻訳・転載したものです。