Socket.IOでリアルタイム双方向通信(その1)

Ajaxの登場で、非同期通信が当たり前なって、Webアプリではクライアントからサーバーへの一般的な通信手段になっている。一方Node.jsの学習していると、サーバーとの通信手段としてSocket.IOに関する記述が多く見られる。
いったいSocket.IOがどんなものか、基本的なことから学習したメモを残す。

まず、Socket.IOの特徴について簡単にまとめると、

  1. TCP上で動作する双方向通信規格WebSocketに準拠したNode.js用ライブラリであり、その他のCGI系言語もサポートされている。
  2. サーバーからクライアントへのプッシュ配信が可能(もちろんクライアントからサーバへも)
  3. 非同期で、応答などはすべて、イベント駆動である

Socket.IOのインストール

Node.jsの他のモジュールと同じくnpmコマンドでインストールする。

※2015-09-16時点、最新Node.js(node : v4.0.0, npm : 2.14.2)で、ver1.3.6のNode.jsをインストールすると、プラットフォームに合わせて、ビルドが実行されるが、依存モジュールである、utf-8-validatebufferutilのビルドで失敗する。

※2015/11時点の最新版では、正常にビルドされることを確認

アプリケーション構成

※下記のプログラムは、Node 4.0.0、express 4.13.3、socket.io 1.3.6で確認した内容である。(ただしビルドエラー対応を独自に実施している)アプリケーション構成は以下のとおり、実際にはほかにもあるが、関係あるファイルだけを記載。

sample
├ app.js   Node.jsアプリのメインスクリプト(socket.ioのサーバ実装)
└ public
   ├ root.html  クライアント画面(socket.ioのクライアント実装)
   └ js
      └ jq
         └ jquery-1.9.1.js  HTML処理にjQueryを使用

現実世界の例とSocket.IOの比較

現実世界において、なんらかの館、店などでは、一般的に以下のような感じである。

  1. “管理人”は、1つの建屋に対し一人である。
  2. その”管理人”は幾つかの”部屋”を管理することができる。
  3. “各部屋”には、何人もの人が”入室”して”会話”することができる。
  4. 同じ部屋であれば、特定の人とヒソヒソ話することもできれば、大声で”部屋の全員”に向かって話すこともできる。
  5. “入室した人”は、隣の部屋の話を聞くことはできないが、管理人は館内放送のように全員に向かってメッセージを投げることができる。
  6. 何らかの理由で、管理人は特定ユーザの入室を拒否することができる。
  7. 同一ユーザがいくつもの部屋を行き来して、各部屋の人と交流することもできる。
  8. 部屋にはたくさんの人がいて、いろんな”話題”でそれぞれ会話が成り立っている。
    Socket.IOは上記をjavascript化したようなつくりになっている。
上記の例Socket.IO
管理人Socket.IOサーバ
建屋Socket.IOモジュールまたはSockets
部屋Socket
クライアントまたは接続Id
入室Connect
会話Emit
特定の人Socket(Id)
部屋の全員Socket.Broadcast
館内放送Sockets.Emit
話題On, Emitのイベント名

Socket.IOの大まかな使い方

基本的には、クライアントもサーバーも”双方向”だけあって実装方法が似ており、わりとシンプルである。

  1. onメソッドでメッセージを待つ
  2. emitメソッドでメッセージを送信する
  3. 各メソッドは、非同期で実行されて、結果はコールバックで受信する

基本的なやりとりは以上である。非同期かつコールバックによる送受信なので、コールバック内にコールバックが定義されていているようなjavascriptの良くあるスタイルになる。

ここでは、チャットプログラムを例として、サーバー側のシンプルな実装をまとめる。下例は、Socketサーバーがmessageイベントを監視し、メッセージを受信したら、同じ部屋にいるユーザ全員にそのメッセージを配信するシンプルなプログラムになる。

サーバー側の実装

// Socketサーバー開始
var socketio = require('socket.io');
var io = socketio.listen(server);
 
// クライアントからの接続を検知すると、"connection"イベントのコールバックが発火する
// このfunctionには、接続が確立したsocketが引数として渡される。
io.sockets.on("connection", function(socket){
    // この接続に対し、"message"イベントを監視するように設定
    socket.on("message",function(data){
        //受信したデータをほかのsocketユーザにブロードキャスト送信する
        socket.broadcast.emit("message", data);
        // 送信ユーザにも同じメッセージを返す(ブロードキャストには送信ユーザが含まれない)
        socket.emit("message", data);
    });
});

クライアント側の実装

/socket.io/socket.io.jsへのjavascriptファイルの参照を追加すると、javascriptのグローバルスコープにioオブジェクトが追加されてアクセスできるようになる。(socket.io.jsというファイルは存在しない)

io.connect()で接続して取得したSocketオブジェクトに対し、onメソッドで受信設定を行い、emitでデータ送信する流れになる。サーバーとほとんど同じである。接続ユーザに対して与えられるユニークなIDはSocket.idから取得できるが、接続完了するまでは、undefinedのままである。 ※下記はHTMLコードは省略している

<script src="http://localhost:3000/socket.io/socket.io.js"></script>
<script>
    $(function(){
        // socketサーバーに接続
        var socket = io.connect("http://localhost:3000");
         
        // 接続結果イベント
        socket.on("connect", function(message){
            // socketサーバーから発番されたユニークIDを表示する
            $("#chat_message").append($('<li>').text("connected. id = "+socket.id));
        });
         
        // "message"イベント
        // socketサーバーから"message"向けに配信したデータを受信する
        socket.on("message",function(message){
            $("#chat_message").append($('<li>').text(message));
        });
         
        // 送信ボタンをクリックしたら、入力ボックス
        // の文字列を"message"イベントへ送信する
        $("#send_btn").on("click", function(){
            var msg = $("#input_message").val();
            socket.emit("message", msg);
        });
    });
</script>

実際に実行すると以下のようになる。

  1. ブラウザを立ち上げると、Socketサーバーに接続して、idを表示する
    chat_01x

  2. 左側のブラウザからメッセージを送信すると、左右のブラウザにメッセージが表示される。左側はサーバーから応答メッセージであり、右側はブロードキャストされたメッセージである。
    chat_02x

  3. 右側のブラウザからメッセージを送信したイメージ。上記と仕組みが逆になる。
    chat_03x