WebSocket を利用したリアルタイムWebアプリケーション

HTTP Server の章で node.js を使ったWebアプリケーションの作成方法を学びました.既存のアプリケーションでもマルチスレッドあるいはマルチプロセスモデルを活用することで並列実行,並行実行可能なWebアプリケーションを組んでいました.node.js は単一プロセス,単一スレッドで動作しますが,IOをブロックせずに次々と処理していくため,複数のリクエストを効率的に逐次処理することが可能です(IO待ちの間に他のリクエストを処理します).

この特長が生かされるのは次のケースでしょう.

  • 多数のバックエンドIOを消費するケース
  • 多数のフロントエンドIOを消費するケース

つまり,Webアプリケーションです.

前者は,多数のDBやWeb APIをたたいてページを組み立てるケースです.そして後者はAjaxなどを使って動的な逐次描画を行うケースでしょう.

さて,後者の話題について,近未来のWebの姿と噂されている WebSocket と呼ばれるドラフト仕様が期待されています.

WebSocket とは ... 誤解を恐れずにいえば,ブラウザが使う TCP over HTTPみたいなものです.WebSocket の詳細は他の記事にゆだねるとして,この章ではとりあえずWebSocketをnode.jsで使ってみようと思います.

ステップ1: twbot のインストール

WebSocket を使うサンプルとして Twitter の UserStream を使ってみます.まず拙作のtwitterボットツール twbot をインストールします [1]

$ npm install twbot

その後,OAuth のConsumerKey, ConsumerSecret を twitter のサイトで作ってください.その上で twbot:config {ConsumerKey} {ConsumerSecret} コマンドを実行してストリームを流すアカウントの AccessKey, AccessSecret を入手します.

$ twbot\:config sl95SqKcJEVSSEUqPHLmNQ kl01lJgUzJzbvdxSZr12PfF3vvnCPSNa1wdzsBBzI
The 'sys' module is now called 'util'. It should have a similar interface.
please visit http://twitter.com/oauth/authorize?oauth_token=XXXXXX to get verification code.
input verification code: {上記URLにアクセスしてVerificationコードを取得して入力}

AccessKey, AccessSecret の入手に成功すれば次のように結果が表示されます.

*********************************************************************
Access key/secret have been successfully retrieved from Twitter
You can use Bot application by following constructions.
*********************************************************************

var TwBot = require("twbot").TwBot
var bot = new TwBot({"consumerKey":"XXXXXXXXXX","consumerSecret":"XXXXXXXXXXXXXXX","accessKey":"XXXXXXXXXXXXXXX","accessSecret":"XXXXXXXXXXXXXXX"})

ステップ2: ストリームの動作確認

twbot を使うと簡単にユーザーストリームを取得できます.まず普通に動作することを確認しましょう.debug プラグインを使うとすべてのストリームを標準出力にダンプします.

var TwBot = require("twbot").TwBot
var bot = new TwBot({"consumerKey":"XXXXXXXXXX","consumerSecret":"XXXXXXXXXXXXXXX","accessKey":"XXXXXXXXXXXXXXX","accessSecret":"XXXXXXXXXXXXXXX"})

bot.loadPlugin('twbot/plugins/debug');
bot.startUserStream();

これを app.js として保存して起動します.接続がうまくいくとストリームの先頭の friends 一覧が表示されるはずです.

$ node app.js
{ friends:
  [ .... ]

うまくいったら次は,debug プラグインをコメントアウトして次のようにしてください.

// bot.loadPlugin('twbot/plugins/debug');
bot.on('status', function(tweet){
    console.log(tweet.user.screen_name + ': ' + tweet.text);
});
bot.startUserStream();

こうすると,status の更新イベントだけを拾って標準出力に垂れ流します.では,この垂れ流し先をブラウザにしましょう.

ステップ3: Webページの用意

app.js に HTTP Server のコードを追加します.startUserStream 自体も非同期メソッドなのでそのまま下に追記していく形でかまいません.

var http = require('http'),
    fs = require('fs');

var server = http.createServer(function(req, res){
   res.writeHeader(200, {'Content-Type': 'text/html'});
   var read = fs.createReadStream(__dirname + '/websocket.html');
   read.on('error', function(err){
      res.end(err.stack);
   });
   read.on('data', function(data){
      res.write(data);
   });
   read.on('end', function(){
      res.end();
   });
});
server.listen(3000);

__dirname + /websocket.html でHTMLファイルをホストするようにしているので次のように websocket.html を app.js と同じディレクトリに起きます.

<html>
  <body>
    <h1>Twitter UserStream with WebSocket</h1>
    <p>
      Hello WebSocket Streaming!
    </p>
  </body>
</html>

まずはこの状態 app.js を起動し,静的なファイルが正しくホストできていることを確認してください.

ステップ4: socket.io で垂れ流す

node.js で WebSocket を手軽に使う方法は socket.io というモジュールを使うことです.例によって npm でインストールします.

$ npm install socket.io

インストールが成功したら app.js にWebSocket用のコードを追加します.bot.on(‘status’, function(tweet){...}) のイベントは移動させた上でブラウザに対してWebSocketを通じてメッセージを垂れ流すようにします.

var io = require('socket.io');
var socket = io.listen(server);
socket.on('connection', function(client){
   var fun = function(tweet){
      console.log(tweet.user.screen_name + ': ' + tweet.text);
      client.send(JSON.stringify(tweet));
   };
   bot.on('status', fun);
   client.on('disconnect', function(){
      // クライアントが消えたらイベントを外す
      bot.removeListener('status', fun);
   });
});

サーバー側にWebSocketの実装を追加するのは簡単です. io.listen(httpServer) するだけです.あとは connection イベントを拾ってクライアントオブジェクトを取得したら,クライアントに対するメッセージ配信のルールを追加していきます.

今回の場合は bot がメッセージをtwitterから受け取ったら即座にclientに配信する,ということで bot の status イベント時に client.send() を使ってメッセージを送信しています.

クライアント側でWebSocketに接続し [2] ,メッセージを受信するには websocket.html を次のように書き換えます.

<html>
  <body>
    <h1>Twitter UserStream with WebSocket</h1>
    <p>
      Hello WebSocket Streaming!
    </p>
    <div id="statuses"></div>
  </body>
  <script src="/socket.io/socket.io.js"></script>
  <script>
    var socket = new io.Socket();
    socket.connect();
    socket.on('connect', function(){
       // 接続完了時
    });
    socket.on('message', function(msg){
       // メッセージ受信時
       var status = eval('(' + msg + ')');
       var elm = document.createElement('p');
       elm.innerHTML = status.user.screen_name + ': ' + status.text;
       document.getElementById('statuses').appendChild(elm);
    });
  </script>
</html>

ステップ4: アプリケーションをリアルタイムに共有する

WebSocket のすごいところはこれだけではありません.

  • クライアント側からもメッセージが送れる.
  • socket.broadcast 関数を用いると,接続中の全クライアントに対してメッセージを配信することができます.
  1. は当たり前といえば当たり前の機能ですし,(WebSocketに比べて非効率ですが)普通にHTTPを使って実現できます.2. の機能を見てみましょう.

接続がある度に接続数をカウントして,全配信を行うことにします.そしてクライアント側ではリアルタイムに接続数を更新します.

まずサーバー側のコードです.WebSocketの部分のみを変更しています.twitter のストリームもbroadcastで配信するように変更しています.

var io = require('socket.io');
var socket = io.listen(server);
var count = 0;
bot.on('status', function(tweet){
   console.log(tweet.user.screen_name + ': ' + tweet.text);
   socket.broadcast(JSON.stringify(tweet));
});
socket.on('connection', function(client){
   // 全クライアント配信
   count = count + 1;
   socket.broadcast(count);
   client.on('disconnect', function(){
      count = count - 1;
      socket.broadcast(count);
  });
});

クライアント側のコードでは,number型のメッセージを受け取る度に接続数の表示をアップデートするようなコードを加えます.

<html>
  <body>
    <h1>Twitter UserStream with WebSocket</h1>
    <p>
      Hello WebSocket Streaming! (<span id="connections"></span>)
    </p>
    <div id="statuses"></div>
  </body>
  <script src="/socket.io/socket.io.js"></script>
  <script>
    var socket = new io.Socket();
    socket.connect();
    socket.on('message', function(msg){
      // メッセージ受信時
       var status = eval('(' + msg + ')');
       if( typeof status == 'object' ){
          var elm = document.createElement('p');
          elm.innerHTML = status.user.screen_name + ': ' + status.text;
          document.getElementById('statuses').appendChild(elm);
       }else if( typeof status == 'number' ){
          document.getElementById('connections').innerHTML = msg;
       }else{
          alert(msg);
       }
    });
  </script>
</html>

実装が完了したら,ブラウザで複数タブを開くなどして localhost:3000 に同時接続を試みてください.接続数を変更する度に,既存のすべての画面が更新されることが確認できると思います.

broadcast 機能を使うと,サイトに集まるユーザー同士が密に連携するようなそんなアプリケーションを作ることができるのです.

[1]http://github.com/yssk22/node-twbot にソースコードはおいてあるので何かあれば issue にあげるか twitter などで連絡ください.
[2]/socket.io/socket.io.js は socket.io モジュールによって自動的にホストされます.このJSを使うことで WebSocket の実装されていないブラウザではFlashを使って同じ挙動にするという実装になり,ブラウザ感の差異を気にする必要がなくなります.