読者です 読者をやめる 読者になる 読者になる

Block Rockin’ Codes

back with another one of those block rockin' codes

Socket.IO API 解説

node.js Socket.IO WebSocket

追記

11/7/31
Socket.IO v0.7 解説を最初に途中までで出す。
11/8/1
だいたい全部新機能なので '(新機能)' って書くのやめた。
11/8/4
オプションの設定周りを追記
11/8/6
認証周りを追記
11/8/12
スタンドアローンのサンプルを追記
11/9/27
Socket.IO v0.8 対応について追記
11/9/27
タイトルを Socket.IO API 解説に変更
11/9/27
翻訳サイトリンク追加
公式マニュアル翻訳サイト

そういえば公式サイトの翻訳をフォークしたリポジトリで、それなりの更新頻度でやってます。
リポジトリwiki も地味に訳しててこっちは結構役に立ちます。本記事と合わせてどうぞ。

本家
http://socket.io/
翻訳ページ
http://jxck.github.com/socket.io
wiki
https://github.com/Jxck/socket.io/wiki
タイトル変更について

v0.7 で書き初めましたが、今後も次に大きなアップデートがあるまでは、ここに追記します。
そのためタイトルを
旧「 Socket.IO v0.7 新機能解説」から
新「 Socket.IO API 解説」に変更します。

Socket.IO v0.8 対応

11/8/28 に思った以上の早さで v0.8 が出ました。
主なアップデートは新しい WebSocket プロトコルへの対応です。

  • Added hybi07 support. [einaros]
  • Added hybi10 support. [einaros]

このアップデートによる、本文中サンプルの変更はありません。

本文

Socket.IO の新バージョン v0.7 がリリースされ、アナウンスされていた通り大幅に機能が拡充されました。
リリース後すぐに出したかったけど、思った以上に量が多くすぐには書けませんでした。
そこで、新機能とその使い方について書けるところから書いていきたいと思います。
Socket.IO を使いたい人は多いと思うし、もうすぐある Node Kock Out でもみんな使うと思うので、それまでには一通り網羅したい、予定。


基本的な通信はこれまでのバージョンとの後方互換をある程度保っていますが、
ここから新たに API を覚えて、古い資産は今のうちに書き直すことをお勧めします。
0.6 系で解説している他のサイトの内容は互換性に注意してください。
そして、このエントリでは基本的に新機能にあたる部分が大半です。
「v0.6 までは〜」とかは面倒なので書いてないところもあります。
これから学ぶ場合は、基本こう使うんだ。で読んでもらった方がいいでしょう。


その意味で、自分が以前に書いた node.js で エコーサーバと簡易コンテンツサーバ - Block Rockin’ Codes も古いのでここへのリンクを張っています。


この記事の元となるリソースは 公式マニュアルgithub の wikiです。


また、これらの翻訳は、コミッタ達の許可を得て自分が Fork したこちらのリポジトリで行っていく予定です。
しかし、まだ途中です。
(本人に確認したところ本体ページは近く Jade で書き直される予定らしい。)

準備とコネクション

今回のサンプルはドキュメントの物ではなく、全てオリジナルです。Node は v0.4.x を使います。
手軽に試せるよう、Express を用いて行いたいと思います。まず Express コマンドでスケルトン(ひな形) を作成してください。
テンプレートは EJS を使います。バージョンは以下を参考に。(Express, EJS はあまり関係ないので、バージョンはなんでもいいはず)

$ npm install express -g
$ express -s -t ejs socket-io-sample
$ cd socket-io-sample
$ npm install
$ npm install socket.io@0.7.7

まずサーバ側とライブラリで socket.io を使用できるようにします。
クライアントライブラリの socket.io.js は socket.io のサーバが自動的に配信します。(これについては下の方で詳しく。)
デフォルトのパスは以下になります。自分で書く CSJS は public/javascripts/sample.js にします。

<link rel='stylesheet' href='/stylesheets/style.css' />
<script src="/socket.io/socket.io.js"></script>
<script src="/javascripts/sample.js"></script>

準備完了です。
以下 //Server は app.js の最後に書き足し、 //Client は sample.js に書かれていると思ってください。

あと、中で出てくる log() は以下です。

  // Client
  var log = function(){ console.log(arguments); }
  // Server
  var log = console.log;

基本的な通信

基本的な通信は以下のとおりです。
どちら側でも、 socket.emit(eventname, data) でイベントを発火(=データの送信)をし、
socket.on(eventname, callback) でイベントを検知(=データの受信)を行います。


eventname は CS/SS で一緒であれば任意の文字列を指定することができます。
v0.7 以前では eventname は message 一種類だけでした。


broadcast は socket.broadcast.emit とすることで指定できます。
このように Getter になってるプロパティを間に挟む指定方法が最近は多いようです。
Socket.io では Flag と呼んでいます。

参考
Jxck's OutPut - JS の Getter による Flag

イベント駆動感が満載なので、以下で共通する流れを一緒に確認してください。

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) { // 2
  log('connected');
  socket.on('msg send', function (msg) { // 4
    socket.emit('msg push', msg); // 5
    socket.broadcast.emit('msg push', msg); // 6
  });
  socket.on('disconnect', function() { // 9
    log('disconnected');
  });
});
// Client
var socket = io.connect('http://localhost'); // 1

socket.on('connect', function() { // 2
  log('connected');
  socket.emit('msg send', 'data'); // 3
  socket.on('msg push', function (msg) { // 7
    log(msg); // 8
  });
});


これでサーバ、クライアント間で通信ができます。
サーバを起動し、クライアントが接続すると

  1. クライアントが 'http://localhost' にソケット接続を要求する。
  2. サーバがクライアントとの接続を確立すると、サーバで 'connection' 、クライアントで 'connect' イベントが発生する。
  3. クライアントが 'msg send' イベントを発火して文字列 'data' を送信する。
  4. サーバで 'msg send' イベントが発生し、コールバックが実行される。
  5. サーバが 'msg push' イベントを発火して受信したメッセージを送ってきた本人に送り返す。
  6. サーバが 'msg push' イベントを発火して受信したメッセージを送ってきた本人以外にブロードキャストする。
  7. クライアントで 'msg push' イベントが発生し、コールバックが実行される。
  8. 受信したデータをログ出力
  9. クライアントが切断したら、サーバ側で 'disconnect' イベントが発生する。


という流れです。

今までと違うのが、イベント名が 'message' だけでなく好きな文字列('msg send', 'msg push')を指定できるようになった事です。

送達確認

送信したデータが送信相手にきちんと到達したかを確認できるようになりました。単純に send() メソッドにコールバックを追加するだけです。コールバックの引数には、イベントを検知した側が渡した引数が送られます。これを用いてイベントを検知した側が無事データを受け取ったことを、送った側が確認できます。
以下の例では、

  • クライアント側で "data was successfully sent"
  • サーバ側で "data was successfully pushed"

がそれぞれ出力されます。

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  socket.on('msg send', function (msg, fn) {
    fn(msg + ' was successfully sent');
    socket.emit('msg push', msg, function(data) {
      log(data);
    });
  });
});
// Client
var socket = io.connect('http://localhost');

socket.on('connect', function() {
  socket.emit('msg send', 'data', function(data) {
    log(data);
  });
  socket.on('msg push', function (msg, fn) {
    fn(msg + ' was successfully pushed');
  });
});

ネームスペース

これまでは socket は基本的に一つのプロセスに一つで、それを用いて通信を行っていました。
たとえばチャットルームが一つであれば、 socket に対してメッセージを投げ、 socket が接続者全員に broadcast すれば成り立ちました。
しかし、複数のチャットルームを一つのプロセス上に設置したい場合は、ある部屋からのメッセージを単に broadcast すると他の部屋にもデータが送られてしまいます。
フラグ等を頼りにクライアント側で取捨選択する事もできますが、そもそもデータが送られたくない場合もあります。


こうした問題を解決するために、いくつかの方法が考えられてきましたが、この機能がネームスペースとして標準機能になりました。

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  log('connected');
  socket.on('msg send', function (msg) {
    socket.emit('msg push', msg);
    socket.broadcast.emit('msg push', msg);
  });
});

var chat = io
  .of('/chat')
  .on('connection', function(socket) {
    log('chat connected');
    socket.on('msg send', function (msg) {
      chat.emit('msg push', msg + ' from chat');
    });
  });

var news = io
  .of('/news')
  .on('connection', function(socket) {
    log('news connected');
    socket.on('msg send', function (msg) {
      news.emit('msg push', msg + ' from news');
    });
  });
// Client
var socket = io.connect('http://localhost')
  , chat = io.connect('http://localhost/chat')
  , news = io.connect('http://localhost/news');

socket.on('connect', function() {
  socket.emit('msg send', 'data');
  socket.on('msg push', function (msg) {
    log(msg);
  });
});

chat.on('connect', function() {
  chat.emit('msg send', 'chat');
  chat.on('msg push', function (msg) {
    log(msg);
  });
});

news.on('connect', function() {
  news.emit('msg send', 'news');
  news.on('msg push', function (msg) {
    log(msg);
  });
});

これらの名前空間は動的に足す事ができるようです。(TODO:未検証)
(これまでやってきたハックが、実装に取り入れられる。便利!)

データの紐付け

socket.set(dataname, data, callback) と socket.get(dataname, callback) で
接続しているクライアントに対して任意のデータを紐づけることができるようになりました。
このデータは Redis 等に入れて複数のプロセスから共有したりできるようです。(TODO:未検証)

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  socket.on('set nickname', function (name) {
    socket.set('nickname', name, function () {
      socket.emit('ready');
    });
  });

  socket.on('get nickname', function () {
    socket.get('nickname', function (err, name) {
      socket.emit('name', name);
    });
  });
});
// Client
var socket = io.connect('http://localhost');

socket.on('connect', function() {
  socket.emit('set nickname', 'Jxck');
  socket.on('ready', function (msg) {
    socket.emit('get nickname');
  });
  socket.on('name', function (name) {
    log('name is', name);
  });
});

揮発性メッセージ

ネットワークの遅延等でデータの送信に時間がかかる場合や、受信ができない場合があります。
Socket.IO はメッセージの送信(ソケットへの書き込み)に失敗すると、自動的にリトライします。
しかし、リアルタイムなアプリケーションでは、送信が少しぐらい失敗してもいいから、
とにかく大量のデータをすばやく送りたい場合等が有ります。

この場合 emit() の前に volatile フラグを立てると、送信に失敗したらそのメッセージを捨てる(リトライしない)
揮発性のメーセージ送信をすることができます。
これは例えば、ツイッターのストリームや、ゲームの座標等、少し位消えても困らないデータを送るのに適しています。

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  var i = 0;
  var roop = setInterval(function() {
    socket.volatile.emit('volatile msg', ++i);
  }, 100);

  socket.on('stop', function(){
    clearInterval(roop);
  });
});
// Client
var socket = io.connect('http://localhost');

socket.on('connect', function() {
  socket.on('volatile msg', function (msg) {
    if(msg == 100) socket.emit('stop');
    log(msg);
  });
});

JSON

これまでは、オブジェクトは自動的シリアライズされて送信されていました。
しかし、文字列や数値もエンコードしてしまうため、オーバーヘッドがかかります。
そこで、 JSON フラグを立てると、送信するデータが JSON である事を明示する事ができます。
すると、内部で自動的シリアライズ(JSON.stringify()) されます。
(フラグを立てなかった場合は、 toString() されてから送信されます。)

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  var data = { "a": { "b": { "c": "d" }}};
  socket.json.emit('msg push', data);
  socket.json.broadcast.emit('msg push', data);
  socket.on('msg send', function (msg) {
    console.log(msg.a.b.c);
  });
// Client
var socket = io.connect('http://localhost');

socket.on('connect', function() {
  var data = { "a": { "b": { "c": "d" }}};
  socket.json.emit('msg send', data);
  socket.on('msg push', function (msg) {
    console.log(msg.a.b.c);
  });
});

ファーストクラスイベント

ここまでのイベントは、 io.sockets.on 内でコールバックの引数に渡された socket に対して on, emit をしてました。
しかし、 io に対して on でイベントを検知することもできます。
io が持っている sockets は接続しているコネクションを管理しているイメージで、ここに対して emit すると、接続している全体に対する送信になります。

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  io.on('msg send', function (msg) {
    log(msg);
  });
  io.sockets.emit('msg push', 'data'); // Broadcast for ALL
});
// io.sockets.emit('msg push', 'data'); ここでもできる。
// Client
var socket = io.connect('http://localhost');

socket.on('connect', function() {
  socket.emit('msg send', 'data');
  socket.on('msg push', function (msg) {
    log(msg);
  });
});
参考
How do I Broadcast anywhere from my code?

WebSocket 互換 API

Socket.IO は WebSocket とは違う独自の API を提供しています。
しかし、なるべく生の WebSocket の API に似た記述でも通信ができるようになっています。

具体的には socket.send(data) と socket.on('message', callback) を使用します。
send() は内部的には eventname を 'message' に固定しているだけです。

// Server
var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket) {
  socket.on('message', function (msg) {
    socket.send(msg);
    socket.broadcast.send(msg);
  });
});
// Client
var socket = io.connect('http://localhost');

socket.on('connect', function() {
  socket.send('data');
  socket.on('message', function (msg) {
    log(msg);
  });
});

セッション ID

セッション ID は以下のように取得できます。

var sid = socket.id;

特定のセッション ID でのメッセージ送受信は以下のようになります。

io.sockets.socket(/* session id */).send('data');
io.sockets.socket(/* session id */).emit('msg push', callback);

セッション ID の共有は以下のようになります。

サーバインスタンス

これまでは、ソケット通信を確立するためには、 http サーバインスタンスか、その拡張である Express のサーバを渡す必要が有りましたが、今後は必要なくなります。
つまり Socket.IO サーバをスタンドアローンでたてられるということ。

Before

今まではサーバインスタンスを渡していた。

// Server
var io = require('socket.io');
var app = express.createServer();
io.listen(app);
After

サーバインスタンスを渡す必要が無い。

// Server
var io = require('socket.io').listen(3000); // デフォルトポートは 80

io.sockets.on('connection', function (socket) {
  log('connected');
  socket.emit('msg push', 'data');
  socket.on('msg send', function (msg) {
    log(msg);
  });
});

このサーバに接続するサンプルとして例えば以下。

<!DOCTYPE html>
<html>
  <head>
    <title>Standalone.sample</title>
    <script src="http://localhost:3000/socket.io/socket.io.js"></script>
    <script type="text/javascript">
       var socket = io.connect('http://localhost:3000/');
       socket.on('msg push', function (data) {
         console.log(data);
         socket.emit('msg send', data);
       });
    </script>
  </head>
  <body>
    <h1>Standalone Sample</h1>
    <p>Welcome to Standalone Sample</p>
  </body>
</html>

このファイルをブラウザで開くと Socket.IO サーバとの接続が確認できます。
Chrome なら file:// で開いても接続できました。http:// で開きたい場合は例えば、

$ python -m SimpleHTTPServer 3001

などとしてブラウザから http://localhost:3001/filename.html などで接続。

WebSocket はクロスオリジン通信が可能で、 Socket.IO も同様にクロスオリジン通信ができます。
よって、このようにソケット通信だけをするサービスサーバを立てておくという需要はあるかと思います。

オプション設定

これまでは全体で共通するオプション設定を listen() 時に一つ適応できましたが、
今後は Express と同じスタイルの、 configure() メソッドを通じて listen() とは別に設定できます。
また configure() は第一引数に環境( 'production' , 'development' , 'test' など任意の文字列) を指定でき、設定を切り替える事ができます。設定メソッドは set(), enable(), disable() です。

Before
// Server
var io = require('socket.io');
io.listen(app, {
  tranportOptions: {/* snip */}
});
After
// Server
var io = require('socket.io').listen(app);

// production の設定は、リポジトリの wiki で推奨されている設定です。
io.configure('production', function(){
  io.enable('browser client minification');  // minified されたクライアントファイルを送信する
  io.enable('browser client etag');          // バージョンによって etag によるキャッシングを有効にする
  io.set('log level', 1);                    // ログレベルを設定(デフォルトより下げている)
  io.set('transports', [                     // 全てのtransportを有効にする
      'websocket'
    , 'flashsocket'
    , 'htmlfile'
    , 'xhr-polling'
    , 'jsonp-polling'
  ]);
});

io.configure('development', function () {
  io.set('log level', 2);                   // level は 1〜4
  io.set('transports', ['websocket']);      // 通信方法の選択、 flashsocket も指定しないと有効にならない。
});

環境の切り替えは、 NODE_ENV で指定します。これも Express と同じ。

$ NODE_ENV=production app.js
$ NODE_ENV=development app.js

オプションの一覧は Configuring Socket.IO · Jxck/socket.io Wiki · GitHub 参照。

認証

socket に対して認証ロジックをはさむことができるようになりました。
認証方式は、グローバルとネームスペースの二つがあります。
グローバルはソケット全体の、ネームスペースは上述したネームスペースごとの認証です。

詳細は wiki にありますが、ここと同じ流れで説明します。

認証を知るには、ハンドシェイクとハンドシェイクデータについて理解する必要があります。

ハンドシェイク

クライアントとサーバがリアルタイム接続を確立するためには、
最初に XHR (same origin)か JSONP (cross origin)
で、ハンドシェイクをします。

このとき、リクエストから下記のようなデータを収集しサーバ側に保存します。
これがハンドシェイクデータ(HandShakeData)です。

{
 , headers: req.headers       // <Object> リクエストヘッダ
 , time: (new Date) +''       // <String> コネクションの日付
 , address: socket.address()  // <Object> アドレスとポート
 , xdomain: !!headers.origin  // <Boolean> クロスドメイン通信かどうかのフラグ
 , secure: socket.secure      // <Boolean> HTTPS かどうかのフラグ
}

これは、 WS などの通信はヘッダを付けずにデータをやり取りするため、
接続に紐づけたハンドシェイクデータを最初に保持しておくことで、
通信開始後もこのデータから Cookie 等の情報を取得できるようにするのが目的です。

グローバル認証

グローバル認証は、オプションと同様 configure() 内で set() を用いて設定します。
関数の引数には、収集されたハンドシェイクデータと、認証結果を伝えるためのコールバックが渡されます。
例えば、ハンドシェイクデータ内の Cookie をもとに認証を行う例は以下のようになります。
認証結果に応じたコールバック呼び出しは、

  • callback(err) // 認証エラー
  • callback(null, false) // 認証失敗
  • callback(null, true) // 認証成功

のいずれかです。認証エラーか認証失敗の場合は、クライアントのサーバへの接続は拒否されます。

function find_by_cookie(cookie, callback) {
  var user = {
    name: "Jxck",
    cookie: cookie
  }
  if(cookie) return callback(null, user);
};

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    var cookie = handshakeData.headers.cookie;
    find_by_cookie(cookie, function(err, user) {
      if(err) return callback(err); // 認証失敗
      if(!user) return callback(null, false);  // 認証不可

      handshakeData.user = user;
      callback(null, true); // 認証成功
    });
  });
});

ハンドシェイクデータは、認証の終了後に保存されます。
認証が成功した場合は、通常通り connection イベントが発生し、認証時に保存されたハンドシェイクデータは socket.handshake から参照することができます。

io.sockets.on('connection', function (socket) {
  socket.on('message', function (msg) {
    socket.send(socket.handshake.user.name); // Jxck
    socket.broadcast.send(msg);
  });
});


一方クライアント側では、認証が失敗した場合 error イベントが、成功した場合は通常通り connect イベントが発生します。

// Client
var socket = io.connect('http://localhost');

socket.on('error', function(reason) {
  console.log(reason);
});

socket.on('connect', function() {
  socket.send('data');
  socket.on('message', function (msg) {
    log(msg);
  });
});
ネームスペース認証

認証はネームスペースごとに実行できます。 v0.7 ではネームスペースを分けられるだけでなく、そのネームスペースごとに認証を挟むことで、例えば複数の認証付きチャットルームのようなものを簡単に実装できるようになりました。

まずネームスペースの記述を思い出してみます。

var io = require('socket.io').listen(app);
io.of('/chat')
  .on('connection', function(socket) {
    log('chat connected');
  });

ここで、全てのネームスペース( .of() の戻り値)は authorization() を持ちます。このメソッドにコールバックを渡すことで認証を行い、その後通常の on() をチェインします。
コールバックの引数はグローバルと同じです。

// Server
var io = require('socket.io').listen(app);

function find_by_cookie(cookie, callback) {
  var user = {
    name: "Jxck",
    cookie: cookie
  }
  if(cookie) return callback(null, user);
};

var chat = io
  .of('/chat')
  .authorization(function (handshakeData, callback) {
    var cookie = handshakeData.headers.cookie;
    find_by_cookie(cookie, function(err, user) {
      if(err) return callback(err); // 認証失敗
      if(!user) return callback(null, false);  // 認証不可

      handshakeData.user = user;
      callback(null, true); // 認証成功
    });
  }).on('connection', function(socket) {
    log('chat connected');
    socket.emit('username push', socket.handshake.user.name);
  });

ネームスペース認証の場合、クライアント側の処理はグローバル認証と少し異なります。
認証成功時は、 connect イベントですが、認証失敗時は、 error イベントの代わりに connect_failed が発生します。

// Client
var socket = io.connect('http://localhost');

socket.on('error', function(reason) {
  console.log(reason);
});

var chat = socket.of('/chat')
  .on('connect_failed', function (reason) {
    console.error('unable to connect to namespace', reason);
  })
  .on('connect', function () {
    console.info('sucessfully established a connection with the namespace');
  });

chat.on('username push', function (msg) {
  console.log(msg); // Jxck
});

Other

他にも下記のようなものも視野に入れているようです。

  • CI とクラウド上での自動テスト
  • より高速な Flash フォールバック
  • Websocket の仕様変更への追従

Express.IO

さらに Express との連携を強化し、Express のセッションと Socket.IO のセッションを共有する Express.IO なるモジュールが公開される予定らしいです。
Express の作者 Tj と Socket.IO の作者 Guillermo Rauch は同じ LearnBoost で働いているため、彼らは手を組みやすい状況なのかも知れません。
目指すところは 「リアルタイム Web 界の Rails」とのこと、期待できますね!!

まとめ

Socket.IO 自体の完成度が上がる事で、リアルタイムアプリケーションを開発する際に出来ることもより増えると思います。
@ と @ さんはあたりが中心に開発していて、とてもストイックにコミットしているので、今後の発展にも期待したいと思います。