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

前回では、Socket.IOの基本的な使い方に関してメモした。今回は、1歩踏み込んだ使い方に関するメモを残す。

Socket.IOのネームスペース

前回最後に掲載したサンプルコードでは、部屋に関して特に意識したコードにはなってなかったが、Socket.IOではわりと簡単に部屋を分けることができる。下記例では、room1room2を作成してそれぞれの部屋でチャットできるようにする。

サーバー側の実装

var socketio = require('socket.io');
var io = socketio.listen(server);
//io.sockets.on("connection", function(socket){
io.of("/room1").on("conne   ction", function(socket){
    socket.on("message", function(data){
        socket.emit("message", "[room1]" + data.value);
        socket.broadcast.emit("message", "[room1]" + data.value);
    });
});
io.of("/room2").on("connection", function(socket){
    socket.on("message", function(data){
        socket.emit("message", "[room2]" + data.value);
        socket.broadcast.emit("message", "[room2]" + data.value);
    });
});

io.socketsをio.of(name_space)に変更するだけで実現できる。

クライアント側の実装

var room1 = io.connect("http://localhost:3000/room1");
    var room2 = io.connect("http://localhost:3000/room2");
     
    // room1
    room1.on("connect", function(message){
        $("#chat_message1").append($('<li>').text("connected. id = "+room1.id));
    });
    room1.on("message", function(message){
        $("#chat_message1").append($('</li><li>').text(message));
    });
    $("#send_btn1").on("click", function(){
            var msg = $("#input_message1").val();
            room1.emit("message", msg);
    });
     
    // room2
    room2.on("connect", function(message){
        $("#chat_message2").append($('</li><li>').text("connected. id = "+room2.id));
    });
    room2.on("message", function(message){
        $("#chat_message2").append($('</li><li>').text(message));
    });
    $("#send_btn2").on("click", function(){
            var msg = $("#input_message2").val();
            room2.emit("message", msg);
    });

クライアント側は、io.connect()で指定するURLを[サーバー名]+[部屋名]にして、
それぞれのソケット(room1, room2)に対し、onemitを実装するだけである。

下図は前回のチャットサンプルを改造して、左右同じチャットプログラムを作成してネームスペースをroom1、room2に分けたものである。

  • 接続直後の状態。セッションが同じなので、左右のネームスペース共にIDが同じである。

chat2_001

  • 左側のroom1に対し、メッセージを送信すると、相手側のroom1だけにメッセージ送信される。

chat2_002

  • こんどは、相手側のroom2からメッセージを送信すると、右側だけにメッセージが送信される。

chat2_003

1つのソケットに対し、イベント名を複数定義すれば、似たようなことはできるが、socket.broadcast()の結果が違ってくる。http通信(ajax)の置き換えとして使うのであれば、クライアントとサーバーは1対1なので、イベント名を複数用意するほうがお手軽じゃないかと。1対Nなら、ソケットを複数用意した使いかたを検討すれば良さそう。

Socket.IOのユーザ認証

グローバル認証とネームスペース認証の2つがある。
※socket.ioのver1.0以降では、実装方法が変更になった模様.以下はsocket.io v1.3.6を使用している.

サーバー側では、io.use(callback(socket,next))メソッドを定義する。接続してきたクライアントごとにコールバックが発火して、接続情報がcallbackのsocket引数に渡される
この内容を確認して、next()を呼びかえるだけでよい。ネームスペース毎に認証を分ける場合、io.of(“namespace”).use(callback)とすれば良く、あとは同じである。

// global authentication
io.use(function(socket,next){
     
    // 認証判定
    var auth_error = ...
    if( auth_error == true )
    {
        // authentication error
        next( new Error("not authorized"));
        return;
    }
    // ok
    next();
});

問題なのは、どうやって認証を判断する材料を渡すかである。
Socket.IOにはデフォルトの認証方法などの実装がないので、各自で考える必要がある。
いろいろサイト回っていると、io.connect()の第2引数にJson形式でqueryプロパティを指定すると、サーバー側に値が渡せる模様。ここでユーザIDとパスワード(暗号化したほうが良さそうだが)が渡せそうである。

// queryプロパティの値としてkey=value形式の文字列を指定すると、
//サーバー側に値を送信できる
var room1 = io.connect("http://localhost:3000/room1",
    {query:{"user":"hoge","password":"hogehoge"}});

上記のようなJsonを指定した場合、サーバー側では以下のようにして取り出せる

// global authentication
io.use(function(socket,next){
    // クライアントからの認証データを取り出す
    var user = socket.handshake.query.user; // user = hoge
    var password = socket.handshake.query.password; // password = hogehoge
     
    // 認証ロジック
    var auth_error = ...
    if( auth_error == true )
    {
        // error
        next( new Error("not authorized"));
        return;
    }
    // ok
    next();
});

パスワードなどは、クライアントで暗号化して、サーバーで復号化するしかなさそうである。

ちなみに第2引数で入力するパラメータは、GET送信のようにURLに展開される。上記の場合、下記でも同じ結果になる。

var room1 = io.connect("http://localhost:3000/room1?user=hoge&passowrd=hogehoge");

不具合なのか、仕様なのか良くわからないが、接続を2つ作成してそれぞれに違うパラメータを渡しても、先勝ちで同じパラメータの内容になってしまう。

var room1 = io.connect("http://localhost:3000/room1?user=hoge&passowrd=hogehoge");
var room2 = io.connect("http://localhost:3000/room2?user=hoge2&passowrd=hogehoge2");

とすると、ちゃんとroom1とroom2のソケット通信はそれぞれ可能にはなる。が、room2の接続時のパラメータは、user=hoge,password=hogehogeになってしまう。つまり上記の方法だと、room1とroom2で個別のユーザIDとパラメータを渡せないことになる。先勝ちなので、

var room1 = io.connect("http://localhost:3000/room1");
var room2 = io.connect("http://localhost:3000/room2?user=hoge2&passowrd=hogehoge2");

とすると、room2にはパラメータが渡らないことになるので注意。これを回避する方法は今のところ不明である。
room1とroom2のセッションが一致するので、分解してパラメータ化するときに一緒になってしまうのかと。

データの送信方式

emit()コール時にフラグを追加することによって、配信方法を指定することができる。
volatileフラグを指定すると、送信失敗時にリトライしないので、失敗しても無視して大量データを配信する場合に適している。

フラグ配信方法実装場所
socket.emit() socket.broadcast.emit()文字列送信クライアント・サーバー
socket.json.emit() socket.json.broadcast.emit()Jsonオブジェクト送信クライアント・サーバー
socket.volatile.emit() socket.volatile.broadcast.emit()リトライ無し送信サーバー