WebSocketを使ってWebブラウザ間P2P通信をしてみた

はじめに

ブラウザ間でP2P通信が実現できれば、ブラウザ上で動作するP2Pアプリが作れて面白そうだなーと思ったのでWebSocketを使って実現してみました。仕組みについては以下で説明していきますが、私が実現した方法は限定的で実用性が低く色々と足りない部分もあるので、軽い気持ちで読んで頂けるとありがたいですw

仕組みの概要

なぜWebSocketを使うのか

従来、Webサーバとクライアント(Webブラウザ)間で非同期に通信するにはXHR(XMLHttpRequest)を用いてきました。基本的にこのXHRは以下の図のように同一ドメインとしか通信できないという制約がありました。*1

しかし、WebSocketのthe Origin-based security modelでは異なるドメインとも通信することが可能になります。WebSocketプロトコルでは、サーバとクライアント間で接続を確立する際にハンドシェイクデータの送受信を行いますが、クライアント側のハンドシェイクデータ内にはOriginというヘッダーフィールドがあります。以下の図のように、クライアントのハンドシェイクデータを受信したサーバはこのOriginの値を見て接続を確立するかどうかを判断することができます。この時、もしもブラウザ上でWebSocketサーバプログラムを動かすことができれば、ブラウザ間P2P通信を実現できるのではないかと考えました。

どのようにしてブラウザ上でWebSocketサーバプログラムを動かすのか

WebSocketサーバプログラムを動かすためにはTCPのソケット機能が必要不可欠ですが、JavaScriptにはそのような機能が無いためブラウザ上でサーバプログラムを動かすことができません。そこで、私はXPCOMの機能を利用することにしました。

XPCOM(Cross Platform Component Object Model)とはMozillaが開発を行なっているコンポーネント技術のことです。XPCOMコンポーネントにはメモリ管理やソケット機能などがあり、これらの機能はFirefoxアドオンの中から呼び出すことができます。

私はこのXPCOMの機能を用いてWebSocketServerというクラス*2を作成し、ブラウザ上からそのクラスを呼び出せるようにするFirefoxアドオンを開発しました。つまり、アドオンがインストールされたFirefox上のJavaScriptではWebSocketServerクラスのインスタンスを作成でき、ブラウザ上で動作するWebSocketサーバプログラムを実行することが可能になります。

Firefoxアドオンのダウンロード・インストール

アドオンはWebSocketプロトコルのhybi-08に合わせており、動作確認は最新のFirefoxでしか試していないのでFirefox 10.0a1(Nigthly)にアドオンをインストールして下さい。他のバージョンのFirefoxにアドオンをインストールしたい方はinstall.rdfを書き換えて下さい(試していませんがv8, 9辺りでも動くと思います…多分)。

まずはgithubにあるリポジトリを落としてきます。リポジトリ内のAdd-onフォルダにはFirefoxアドオンのソースコードがあり、echoフィルダにはサンプルプログラムが入っています。

 % git clone git://github.com/yogit/WebSocketServer_hybi-08.git
 % ls WebSocketServer_hybi-08
Add-on/ echo/

次にアドオンのソースコードからxpiファイルを作成します。Firefoxのアドオンはただのzipファイルなので、以下のようにソースコードを1つのzipファイル(ファイル名はWebSocketServer_hybi-08.xpi)に圧縮します。この時、Add-onフォルダをzipファイルに圧縮すると正常に動作しないので気をつけて下さい。

 % cd WebSocketServer_hybi-08/Add-on/
 % zip -r WebSocketServer_hybi-08.xpi ./chrome ./install.rdf ./chrome.manifest
  adding: chrome/ (stored 0%)
  adding: chrome/content/ (stored 0%)
  adding: chrome/content/DataFrame.coffee (deflated 69%)
  adding: chrome/content/DataFrame.js (deflated 73%)
  adding: chrome/content/init.coffee (deflated 43%)
  adding: chrome/content/init.js (deflated 42%)
  adding: chrome/content/overlay.xul (deflated 55%)
  adding: chrome/content/WebSocketClient.coffee (deflated 72%)
  adding: chrome/content/WebSocketClient.js (deflated 73%)
  adding: chrome/content/WebSocketServer.coffee (deflated 70%)
  adding: chrome/content/WebSocketServer.js (deflated 70%)
  adding: install.rdf (deflated 46%)
  adding: chrome.manifest (deflated 44%)

xpiファイルに圧縮できたらFirefoxにドラッグ&ドロップしてダイアログに従ってインストールを行なってください。

サンプルプログラム(echoサーバ)の実行方法

サンプルとしてブラウザ上で動作するechoサーバを作りました。echoディレクトリをDocumentRoot以下に置いてブラウザからアクセスしてください。その際、クライアント側はhybi-08のWebSocketに対応したブラウザをお使いください(Chrome v15かFirefox v10.0a1がオススメです)。以下にサンプルプログラムを動かしているときのスクショを載せます。

画面左側のアドオンインストール済みのFirefox v10.0a1ではechoサーバを動かし、右側のChrome v15ではechoクライアントを動かしてブラウザ間でP2P通信をしている様子が確認できると思います。

サンプルプログラム(echoサーバ)の解説

echo/WSServer.jsのコアになる部分だけ以下に抜粋してコメントを追加しました。

  // newできなければ関数として実行する *1
  server = new WebSocketServer(options) || WebSocketServer(options);
  // WebSocketサーバが起動したときに呼び出されるメソッド
  server.onstart = function () {
    // ページが遷移するときサーバが起動していたら終了するようにする
    addEventListener("beforeunload", function (e) {
      // クラス変数ではなくインスタンス変数(server.RUNNIN)を使う *2
      if (server.readyState == server.RUNNING) {
        server.stop();
      }
    }, true);
    log('server start');
  };
  // サーバが停止したときに呼び出されるメソッド
  server.onstop = function () {
    log('server stop');
  };
  // クライアントと接続が確立したときに呼び出されるメソッド
  server.onconnect = function (client) {
    log('[' + client.host + ':' + client.port + '] connected');
    // クライアントからメッセージを受信したときに呼び出されるメソッド
    client.onmessage = function (data) {
      log('[' + client.host + ':' + client.port + '] recieved(' + data.length + '): ' + data);
      // send()ではなくbroadcast()を使えばチャットサーバのような挙動になる
      client.send(data);      // 受信したデータをそのまま送り返す
      //client.broadcast(data); // 他の接続済みクライアントに送信する
    };
  };
  // クライアントとの接続が切れたときに呼び出されるメソッド
  server.ondisconnect = function (client) {
    log('[' + client.host + ':' + client.port + '] disconnected');
  };

  // サーバ起動
  server.start();
  // サーバ停止
  // server.stop();
「new出来なければ関数として実行する *1」について

Firefox v8, 9辺りではnew WebSocketServer(...)でインスタンス化できていましたが、v10.0a1ではnewしてもundefinedが返ってくるようになりました。原因はよく分からないのですが、関数呼び出しにして内部でnewすることでインスタンス化することができたので上記のような書き方になっています。

「クラス変数ではなくインスタンス変数(server.RUNNIN)を使う *2」について

new WebSocketServer(...)でインスタンス化できなくなったのと同時にWebSocketServerクラスのクラス変数が使えなくなりました(原因も同じかも?)。console.dir(WebSocketServer)で確認するとクラス変数(STOP: 0, RUNNING: 1)はちゃんと見れるのですが、実際にコードの中でクラス変数にアクセスするとundefinedになっています。なので、インスタンス変数にも同じ値を持たせることにしました。

アドオンの解説

簡単にですがWebSocketServerアドオンについて説明します。基本的にFirefoxのアドオンはJavaScriptで書くのですが、私はCoffeeScriptでコードを書いてJavaScriptコンパイルしています。ですので、以下の説明ではCoffeeScriptのコードやファイル名が出てきますが適宜JavaScriptに置き換えて読んでください。
# CoffeeScriptって良いですよね。これに慣れるとJavaScriptを書くのが面倒くさく感じます(^_^;)

WebSocketServerクラスについて

WebSocketSeverクラスはcontent/WebSocketServer.coffeeにて定義しています。このクラスではXPCOMnsIServerSocketインターフェースを用いてサーバ機能を実現しています。クライアントとTCP接続するとonSocketAccepted()メソッドが呼び出され、クライアントごとにWebSocketClientクラスのインスタンスが生成されれます。このWebSocketClientクラスはcontent/WebSocketClient.coffeeにて定義され、ハンドシェイクやデータフレームの送受信などの機能があります。これらのクラス内で用いているXPCOMの機能については以下の公式ページから参照してください。
XPCOM Interface Reference - Mozilla | MDN

JavaScriptに機能を追加する

アドオン内で定義したクラスはアドオン内のコードからしか使えませんが(アドオン内のJavaScriptコードとWebサーバからダウンロードしたJavaScriptコードは別物)、content/init.coffeeにて以下のように書くことでブラウザ上で実行されるJavaScriptにWebSocketServerクラスを追加することができます。

window.addEventListener 'DOMContentLoaded',
  ->
    browserWin = window.content.window.wrappedJSObject
    console = browserWin.console
    # WebSocketServerクラスを追加する
    browserWin.WebSocketServer = WebSocketServer
  , false

window.content.window.wrappedJSObjectを経由することでブラウザ上で実行されるJavaScriptのwindowオブジェクトにアクセスすることができます。これにより、JavaScriptに本来存在しないWebSocketServerクラスをwindow以下に追加することができます。

出来ないこと・出来ること

上記のアドオンをインストールすることでブラウザ間でP2P通信が可能になりますがNAT越えの機能は実装していません。よって、構築出来るP2Pネットワーク網はプライベートLANの中に限られ、大規模なネットワーク網を構築することは出来ません(でも、グローバルIPを持つノードを中継役にすればある程度の規模のネットワーク網は構築できるかも?)。また、WebSocketServer機能はアドオンをインストールしないと利用できませんが、WebSocketは双方向に通信できるプロトコルなのでアドオンをインストールしていない(WebSocket対応)ブラウザでも以下のようなネットワークに参加できると思います。

図の中の「Fx☆」はアドオンインストール済みのFirefoxを表し、「WSS」は一般的なWebSocketサーバを表し、「ブラウザ」はWebSocketに対応した普通のWebブラウザを表しています。この図のように普通のブラウザでもFx☆かWSSの下にぶら下がるようにすることでネットワークに参加できると思います。

また、WebSocketServerはhybi-08に合わせて作りましたが以下の機能については未実装です。

  • バイナリフレームの送受信
  • 分割されたフレームの送受信
  • subprotocolの対応
  • 機能拡張(Sec-WebSocket-Extensions)の対応
  • TLS通信(wss://URI)の対応

Firefox以外のブラウザについて

今回の方法だとアドオンをインストールしたFirefox上でしかWebSocketサーバプログラムを動かせませんが、他のブラウザでも同様のことをする方法について考えてみました。

Google Chrome

ChromeにはNaCl(Native Client)*3というネイティブコードを実行する機能が実装されています。NaClでは全てのネイティブコードを実行できるのではなく、NaCl SDKコンパイルされたコードしか実行できません。今現在公開されているSDKには通信機能が無いみたいですが、今後サポートされればFirefoxアドオンのようにソケット機能が使えるようになるかもしれません。
# 少し前まではabout:flagsにP2P Pepper APIという項目がありましたが、何故か今は無くなってます。

Opera

Opera UniteというOperaをサーバにする機能があったので、これを使えば似たようなことが出来そうな気がしますが詳しく調べていないので何とも言えません。
# Opera Uniteは発表されたときは良く耳にしたんですが、ここ最近は話を全然聞かないです…。

Flashを使ってクロスブラウザ

ブラウザ上で動作するAdobe Flashにはサーバ機能は無いようですが、Adobe AIRにはServerSocketクラスというものがあり、TCPサーバとして動作するようです。また、LocalConnectionクラスというものを使えばAIRアプリケーションとブラウザ上のFlash間で通信ができ、ExternalInterfaceクラスを使えばFlashJavaScript間で連携できるので、以下の図のようにWebSocketサーバ部分をWebブラウザから独立したAIRアプリケーション化することで今回と同様のことができるのではないかと考えています。実際にサンプルプログラムを書いて確かめてみようと思ってるのですが時間が無くて放置してます…。

おわりに

以前、電子情報通信学会の研究会にて「WebSocketを用いたWebブラウザ間P2P通信の実現とその応用に関する研究」というタイトルで発表を行いました。発表後、せっかくだからブログネタにしようと思っていたのですが、後回しにしてずっと放置していました。ですが、数日前に以下のブログにて私の研究会での発表を取り上げて頂いたので更新することにしました。
[P2P]Websocketでブラウザ間P2P通信は実現できるか?: Tomo’s HotLine

研究会で発表したときは、WebSocketでブラウザ間P2P通信ができたらアツい!と思っていたのですが、最近はWebRTCが気になっています。まだドキュメントをきちんと読んではいませんが、将来有望な気がするので今後の進展が楽しみです。ついでに、以下にWebRTCについても書いておきます。

WebRTCについて

WebRTC(Web Real-Time Communications)Webブラウザ間で音声や動画によるリアルタイム通信を可能にするもので、WebRTCのPeer Connection APIを使えばアドオンをインストールしなくてもブラウザ間P2P通信ができるのではないかと考えていますが、WebRTCは新しい技術なので対応ブラウザはまだ無いようです。ですが、以下のページによるとWebRTCの幾つかのAPI(getUserMedia, Stream API, PeerConnection API)を実装したWebkitライブラリを置き換えることでWebRTCを使ったアプリケーションの開発が行えるらしいです。ここも詳しく調べ、実際に環境を構築してサンプルプログラムを書いてみたいのですが時間が無いので放置してます…。
https://labs.ericsson.com/apis/web-real-time-communication/documentation

*1:XMLHttpRequest Level 2では異なるドメインと通信する機能が追加されているそうです

*2:JavaScriptにはクラスがありませんが便宜上クラスという単語を使ってます

*3:発音はナックルで良いのかしら?