愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

Shindigのwave featureで実現する簡単なお絵かきチャット feat. node.js and Socket.IO

タイトル書いてから気づいたけど、別にチャット機能とかなかった。


http://d.hatena.ne.jp/zentoo/20100821/1282407303


以前に書いた↑のエントリでは、Shindigのextrasに入ってるwave featureを調べてみて、「あーこれShared Stateはコンテナ側で何とかしてくれって形ですね」という所で終わっていた。今回はいささか無理くりながらShared Stateの実装を行ってみた。

Shared Stateに用いるサーバはnode.jsで実装する。構成としては、Shindigが8080番で待ち受け、node with Socket.IOを9000番で起動。Socket.IOはnode.jsで簡便にWebSocketのサーバサイドを実現するライブラリとして知られているが、WebSocketが動かない環境ではxhr-multipartやxhr-pollingにfallbackするようになっている。そういったサーバ側ライブラリの例に漏れず、クライアント側に必要なライブラリをサーブするための口も用意してあり、Socket.IOでは

http://{host}/socket.io/socket.io.js

がそれに相当する。クライアントライブラリは上記に上げた以外にも様々なプロトコルに対応しており、node.jsとは独立に使うこともできるようだ。

http://github.com/LearnBoost/Socket.IO


Shindigのwave featureではstateの更新が起こった場合にgadgets.rpcを使ってまずcontainer側に処理を移し、そこから何らかの形でShared Stateを実現するための受け口にアクセスする、という形式になっている。なので、多少まどろっこしいが今回のwave gadgetにおける通信の流れを書くと以下のようになる。

wave gadgetでstateを更新 → gadgets.rpcで更新データがcontainer側へ → container side JSから、Shared Stateサーバ(仮称)へデータを投げる → Shared Stateサーバがデータを受け取り、その他のクライアントへブロードキャスト → container側JSが受け取る → gadgets.rpcでwave gadgetへ → wave.setStateCallback()で指定したコールバック関数が呼ばれる

あーまどろっこしい。とにかくこうなる。「Shared Stateサーバがクライアントから送信されたデータを受け取り、他のクライアントにブロードキャスト(もちろんpush)する」という部分がサーバサイドのキモになるわけだが、今回はSocket.IOがその辺をよろしくやってくれるので、該当部分のコードは以下のようになる。

socket.on('connection', function(client) {

  client.on("message", function(data) {
    client.broadcast(data);
  }); 

});


短っ!という感じだが、Socket.IOにはbroadcast()という名前で現在サーバに接続している他のクライアントにメッセージをブロードキャストするそのものズバリなメソッドがあるので、ほとんどやることはない。

もちろんこんな風にすると全てのwave APIを使っているガジェットの通信データが全部混じってしまうので、現実にはこんな実装をすることはないだろう。認証や永続化の仕組みも必要になる。でも今回は「とにかくShindigのwave featureを動かす」ことが目的なのでその辺は割愛。


これによってSocket.IO経由でクライアント間のメッセージングを行うことが可能になったので、後はcontainer側のhtmlでクライアント側ライブラリを読み込んでお膳立てをしてやるだけ。

<script type="text/javascript" src="http://localhost:9000/socket.io/socket.io.js"></script>

<script type="text/javascript">
  var socket = new io.Socket("localhost", { port: 9000 });

  function initRpcs() {
    gadgets.rpc.register("wave_gadget_state", function(data) {
      socket.send( { type: "gadget_state", content: data } );
    });
    gadgets.rpc.register("wave_private_gadget_state", function(data) {
      socket.send( { type: "private_gadget_state", content: data } );
    });
  }

  function initSocket(version) {
    var frameId = "remote_iframe_0";

    socket.on('message', function(data) {
      gadgets.rpc.call(frameId, "wave_" + data.type, null, data.content);
    });

    socket.connect();
  }

  gadgets.rpc.register('wave_enable', initSocket);
  initRpcs();
</script>


ちなみに上記コードのframeIdとかは、Shindig付属のsamplecontainerで動かす前提で決め打ちです。

で、wave APIのうちstate関係のAPIが動くようになったんだけどもこれらを使って何しよう、ということになったので、とりあえずcanvasを使ったお絵かき共有アプリ(めっちゃ単純なやつ)を作ってみた。以下が、ローカルでそのお絵かきアプリ on wave APIを動かしている動画。



元の画面がデカいのでちょっと解りづらいが、左でFirefox、右でChromeを立ち上げている。一方のブラウザで書いた線の座標データがwave.getState().submitValue()によってgadgets.rpc経由でnodeへ送られ、nodeがclient.broadcast()によって他方のブラウザへ送信することでcanvasの同期を実現している。ちょっと変だが、自分の描いた線は赤色で、他のクライアントから描画した線は青色になるという仕様。

この例では、「描画した(というかfillRectした)点の座標をバッファに突っ込んでいき、バッファが一定まで埋まったらそのデータをsubmitDeltaする → 受け取った側は配列をなめて描画」というナイーブなアルゴリズムで同期を行っている。canvasの同期に定石アルゴリズム?があるかどうかは分からないが、現実にはもっとマシな方法を使うだろう。できるだけサーバ側との通信を抑えつつ、スムーズな同期を行う方法…うーんマンダム。WebSocketも、どのブラウザでも使えるようになるのは先だろうし。


とにもかくにも、wave featureを動かすことには成功したのでめでたしめでたし。zentooo先生のShindigの上で動くwave gadgetが見れるのは愛と勇気と缶ビールだけ!



一応ではあるが、ソースもあげといた。

http://github.com/zentooo/shindig-wave-with-node


はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-