Block Rockin’ Codes

back with another one of those block rockin' codes

Socket.IO or WebSocket を AmazonELB でバランスする検証

追記

12/2/29
検証コードと環境は後にしてとりあえず結果だけ書く
12/3/5
Socket.IO の RedisStore を使えばスケール可能なことがわかったので追加
12/3/11
検証コード追加

caution

この検証は 東京Node学園 4時限目 - connpass でやった結果です。しかしその時の環境やソースが手元に無いので今再現ソースと環境を作っています。
2/28 現在分かってる結論だけ先に出しておきます。ソースは後で追って掲載します。その時点でもし結論が変わったりした場合は追記します。
また、この検証内容については一切責任は取りませんので、プロダクション等で使う場合はきちんと検証して下さい。
特に ELB の仕様が変わったら結果が変わると思います。結果が変わったことに気がついた方は教えて頂けると助かります。

code

検証コードを公開しました。 https://github.com/Jxck/elb-websocket-test

  • sio.js は普通の socket.io サーバです。本文に書いたように、これだと ELB でバランスすると、 reconnect が多発します。
  • sio-redis.js が session を redis に登録する例です。これなら、ELB で最初の接続と次の接続が別のサーバに行っても動きます。
  • balancer.js は ELB を使わずに自分でバランスする場合のサンプルです。同じく sio-redis.js ならうまくいきます。
  • ws.js は生の websocket の通信のつもりで書いたんですが、なぜか繋がらず。。断念。

ちなみに、 socket.io のサーバは /socket.io.html をデフォルトでレスポンスするので、
ELB のハートビートはそこに送ると便利です。

intro

複数の AWS サーバのインスタンス上に Socket.IO を立てて、ELB でバランスしたい時、
現状の ELB の仕様では、「できない」ということがわかりました。
これは Socket.IO を使わない生の WebSocket であれば設定によってはできます。
詳細を書いておきます。

また検証に @ と @ に多大な協力を頂きました。
本当にありがとうございます!!

ELB

簡単にまとめると、 ELB は TCP と HTTP の2つのモード(とここでは呼びます)があります。

また、 HTTP を見てバランスする場合は、 Sticky Session というオプションがあり、
Cookie を ELB が発行するか、自分の作ったアプリが発行するか選べます。

  • TCP モード
  • HTTP モード
    • ELB セッション
    • APP セッション

WebSocket サーバをバランスしたい

WebSocket の接続は、 HTTP の upgrade が通るかどうかが大きな境目です。
ELB の場合は、 TCP モードの場合は HTTP ヘッダは見ていないので、通るようです。
ただし、 HTTP モードの場合は、なぜか ELB が HTTP ヘッダを書き換えてしまっていました。

  • upgrade ヘッダが削除されてる
  • connection=websocket が connection=keepalive に書き換えられてる

ということで HTTP モードではバランス以前に WebSocket 接続が確立できません。
かならず TCP モードにして下さい。

Socket.IO をバランスしたい

生の WS では機能が足らないなどの場合に、Socket.IO を用いることも多いと思います。
しかし Socket.IO は WS と別に、最初に HTTP リクエストで Socket.IO サーバからいくつかのデータを受け取り、
その後再び upgrade リクエストを発行し、 WS のコネクションを確立します。


つまり、 Socket.IO は接続確立までに 2 回リクエストを投げるということです。
そしてこの最初の要求と、 WS の接続要求は、「同じインスタンス」にアクセスする必要があります。
二つの接続がバランスされて別々のサーバに行くと、ハンドシェイクがエラーになり、リトライが発生します。
たまたま二回同じサーバにいけば、接続が確立できるので、バランスするサーバが増えるほど成功確率が減り、確立までの時間が長くなります。


確実に同じクライアントをサーバに振るためには、 Sticky Session が利用できます。これは ELB が Cookie を見て同じサーバに降ってくれるものです。
Cookie は HTTP ヘッダだし HTTP モードでしか使えないのでこの時点で TCP モードでは無理ということになります。
HTTP モードで Sticky Session を有効にする際二つのモードがあります。

で、いずれでも Socket.IO のハンドシェイクは一発で成功するのですが、先ほどのように WebSocket はヘッダが書き換えられるので、フォールバックして XHR になります。
しかも ELB の仕様で HTTP モードは 60 秒に一回接続が切れるらしいです。Socket.IO はリコネクトできるんですがステートフルセッションにとってこれは致命的ですね。

[追記]

Socket.IO の SessionStore を Redis にすると、ハンドシェイクも共有されて接続可能なことがわかりました。
この RedisStore という機能は Socket.IOv0.8.4 のソースを読んでわかった次期新機能先取り - Block Rockin’ Codes でも紹介しましたが、
現在実装が見た感じほぼ終わってますが、まだ Stable 扱いではないアンドキュメントな機能です。

簡単に言うと、Redis サーバを立て Socket.IO の SessionStorage としてそこを指定します。
すると、上述した最初のアクセスと二回目のアクセス先のサーバが違ったとしても、ハンドシェイクが成功します。
(ソースはおって出しますすいません。。)


結果

生 WebSocket Socket.IO
TCP mode 接続が確立するし、バランスする 2回のアクセスが偶然同じインスタンスに行けば接続が確立する。 台数が増えればそれだけ、リコネクトの回数が増えて、接続確立までの時間が増える。
HTTP mode ヘッダが書き換えられて、接続が確立しない。 Sticky Session を有効にすれば一発で接続が確立する。 しかし、WebSocketは無理でフォールバックしてXHRになります。 また 60 秒に一回切断する。

考察

生の WS で足りるケースならいいかもしれないけど、 Socket.IO のバランスは現実的ではないようです。
ELB は設定項目が少ないし、プログラマブルな部分が無いので、 HTTP モードでヘッダを書き換える挙動が治らないと、 ELB を使ったバランスは無理そうですね。

どうしても Socket.IO の前にバランサが必要なら、

  • WebSocket を通す
  • Sticky Session

を実現できるバランサを自分で立てる必要があります。
今のところ Node-HTTP-Proxy など、Node でできたものがいくつか有ります。
そもそも WebSocket をきちんと通せるものがなかなか少ない現状だと思うんですが、なにか良い方法を知っていたら教えて頂きたいです。


また、この辺自分も理解が足らないところがあるので、何か気づいた点があったら教えて頂けると幸いです。

Socket.IO API 解説

追記

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

Node におけるスケールアーキテクチャ考察(Scale 編)

[追記]

途中までは Node での複数プロセス起動、プロセス間通信等について書かれていますが、後半は自分が前回の記事 を書くにあたって自分が考えてたことを少し強引に広げて書いた個人的な妄想が多く含まれ、Node におけると言っときながら、後半は Node 関係ない感じになってしまいました。
正直まだ分かっていないことが多いです。変なところをどんどん指摘していただけるとむしろ嬉しいです。



Node におけるスケールアーキテクチャ考察(SSP 編) - Block Rockin’ Codes の続きです。

もともと何となく結論があって書き始めたんですが、書きながら色々調べているうちによくわからなくなりました。
まだまだ調べたらないことがわかったので、とりあえず今わかっているところまで書きます。
結局何がいいたいのかよくわからない感じかもしれないけど、ゴールは SSP のバックエンドの Node をスケールさせることです。

おさらい

前回は、Node に合いそうなアプリケーションの実装として、 Ajax/WebSocket を用いた SSP なアプローチがようさそうだというところまで書きました。
つまり実質 Node がやることは、 RESTful JSON API の公開です。



Node は単一のプロセスでサーバを稼働させることができますが、同時にサーバの処理の限界がプロセスの限界である事を意味します。
そこで今回は、この RESTful JSON API を Node で実装し、かつそのサーバをスケールさせるためにどのような方法があるかを考え、
本当に SSP は水平分散させやすいのかを考えてみます。

Node の HTTP サーバ

簡単に振り返ると、 Node は単体で HTTP をパースすることが出来ます。
これは Node で書いたアプリケーションの実行に、 Apach や Nginex へのデプロイを必要としないということです。
シングルプロセスで稼働するために省メモリである事は前回述べました。


しかし、昨今のサーバハードウエアは、多くのマシンがマルチ/メニ―コア環境を持っています。
これら複数コアを "使い切る" という観点では、Node シングルプロセスモデルは不利になります。


こうした問題への解決策の一つに、プロセスの複数起動というアプローチがあります。


まず、 Node 本体の公式サイトには、マルチコアの有効活用に対する FAQ の回答として以下のように書かれています。(翻訳)

「将来のバージョンで、Nodeは現在のデザインととてもよくフィットする形で子プロセスをforkする ( Web Workers APIを使って) ことができるようになる予定です。」

子プロセスをforkしてプロセス間通信を実施するモデルです。


Node本体にこの実装はまだされていませんが、同じアイデアで WebWorker を使いプロセスを fork する実装に pgriess/node-webworker · GitHub がありあます。
また Tj 謹製の Cluster - extensible multi-core server management for nodejs も複数のプロセスを起動することができます。(いずれも詳細は後述)


プロセスを複数起動して、必要があればプロセス間通信でやり取りすることが、 Node のマルチコア環境の有効活用に対する一つの答えです。


プロセスの複数起動すると以下のようなメリットがあり Node のプロセスを複数起動するための方法もいくつか考えられています。

  • マルチコア環境の有効活用
  • 単一スレッドのイベント総量のオーバー対策
  • プロセスのフォールバック
  • ファームの実現

プロセス間通信

プロセス間通信のプロトコルにつては、いくつか選択肢があります。


例えば node-webworker では WebSocket を使っているし、
cluster では JSON-RPC を使用しているようです。


現状 Node で WebSocket と言えば Socket.IO ですが、 Socket.IO はクライアントにブラウザを想定したフォールバック機構(Flash, Long Pooling etc)を持っているため、
プロセス間通信には使用しません。プロセス間通信に WebSocket を使う場合は

の組み合わせが良さそうです。node-webworker でも内部ではこれを使用しています。
シリアライズされたデータを双方向通信する際に、クライアントとも共通して使える点で WebSocket は使いやすいかもしれません。


データ形式JSON で良いでしょう。他の言語では message-pack を使った方がデータサイズ等の点で良いかもしれませんが、
Node の場合は JS である以上 JSON がネイティブサポートされているし、汎用性もあるのでシリアライズした JSON で十分だと思います。


ちなみに先の Node 自体のプロセス間通信の実装の際には、TCP の使用を検討していると Ryan 本人は言っているようです。
(それ以上のことはわかりませんでした。)


現時点では独自にプロトコルを考えようとするより、既存のものを応用する方が良いような気がします。


ロードバランサとリバースプロキシ

プロセスを複数起動し負荷を分散させる場合は、フロントにロードバランサやリバースプロキシ等を置くことで、処理を各プロセスに割り振れます。


ここで重要になるのは、フロントに立つサーバが

  • WebSocket を通す必要がある
  • イベントドリブンが望ましい

です。


WebSocket は SSP には必須です。現時点では WebSocket を通さないプロキシ等も多いので注意が必要です。
そしてサーバの構成によりますが Node で実装したサーバがイベントドリブンでも、フロントサーバがそうでない場合、 Node の良さが生きない可能性がある点も注意が必要です。



まず二つの用件を満たす既存のサーバとしては WebSocket のパッチ(未検証)をあてた Nginx があげられそうです。
Nginx なら重みづけしたロードバランスとドメインベースでのリバースプロキシが設定できますし、Nginx 自体に静的コンテンツを配信させることもできます。
実績も積んできた Nginx をフロントに置くことは信頼性の面で歩がありそうです。
また個人的な予想では WebSocket の仕様さえ安定すれば、 Nginx は早い段階で WebSocket に対応しそうな気がしています。


Node の実装では、 Cluster は複数プロセスを起動し、
各プロセスへの処理を振り分ける事でロードバランスができます。


リバースプロキシとしては、Nodejitsu の nodejitsu/node-http-proxy · GitHub は、WebSocket や HTTPS も含めて対応しており、 http://www.nodejitsu.com/ で実運用されています。
nodejitsu は多くの実践的なプロダクトを公開しているため、注目に値します。



静的コンテンツサーバ

RESTful JSON API サーバとは別に、土台となる静的コンテンツのサーバも必要になります。静的コンテンツサーバももちろん Node で実装することができますが、既存の実績のあるサーバに委ねるのも良いアプローチだと思います。


特に SSP の場合は、静的コンテンツの更新頻度が非常に低く、土台となる HTML ファイルでいえば、その数もおそらく少ない事が想像されます。
すると、クライアント、サーバ共に積極的なキャッシュ機構を組み込みやすくなります。


静的サーバを Node と別に用意する場合は、Apache より Node と同じイベントドリブンである Nginx を置くのは相性がいいようです。
そして Nginx のキャッシュ、もしくは Squid 等を前におきメモリキャッシュを生かすこと。if-modified-since, expire, cache-manifest などを用いたブラウザキャッシュも考慮に入れます。


メモ: Squid は CARP(Cache Array Routing Protocol)でバランス
http://d.hatena.ne.jp/hideden/20091101/1257061316

セッションの共有

スケールさせた複数のサーバでセッションを共有する必要がある場合は、プロセスが確保したメモリとは別に、セッションストレージを用意するアプローチが有効です。
この場合応答速度を重視して memcache, TokyoTyrant, Redis 等といった NoSQL を用いることが多いようです。

memcached
  • 揮発性
  • expire 対応
  • 一番実績があり運用ノウハウも多く出ている。
TokyoTyrant
Redis


それぞれ長所/短所今ありますが、今回の用途では Redis が良いのではないかと考えています。
これは十分な速度の上に(一定のタイミングでの?)永続機構があり、標準機能である Pub/Sub はプロセス間通信に使える(後述) という点を評価します。

Express ならミドルとして connect-redis が使用できるので session-store としての導入も楽です。

WebSocket 共有

スケールさせた場合、 WebSocket クライアントが別々のサーバにコネクションを張る可能性があります。
すると、例えば Socket.io の場合 broadcast() 相当のことができなくなります。
そこで、 WebSocket を共有できるようにするために、プロセス間でメッセージを共有させる必要があります。

これには二つのアプローチが考えられるようです。

  • Pub/Sub を用いたプロセス間通信
  • 共有メモリの実装


前者が Pub/Sub を用いて、各プロセスが自分に関連するメッセージを Subscribe できるようにする方法です。
実装としては Redis に備わっている Pub/Sub 機能を用いることができます。


後者はそのまま共有メモリ空間を設けて、そこで情報を共有する方法です。
実装方法としてはタプルスペース(TupleSpace)というアーキテクチャがあって、
それが良さそうなのですが、詳細はよくわかっていません。


おそらく難易度的にも Redis の Pub/Sub を用いる方法が良いでしょう。


またここで Express に Socket.IO を組み合わせる場合は、近い将来(Socket.IO v0.7以降)に Express.IO が公開されれば Express - Socket.IO 間でセッションを共有できます。

これはリアルタイムな更新を伴うアプリケーションを SSP で開発する際(つまりセッションを保って画面遷移もしたい)に非常に大きな力になるでしょう。


CPU 処理とファーム

前回の話とは逆になりますが、レンダリングといった CPU バウンドな処理も Node で行いたい場合は、
複数のプロセスを起動し、メインのプロセスからプロセス間通信等を通して、その処理を委譲してしまう方法があります。


この方法に関しては、2011 年の JSconf で Joyent の Tom Hughes-Croucher (Node 本/Oreilly の著者)が興味深い発表をしています。

「多層 Node アーキテクチャ
Multi-tiered Node Architectures - JSConf 2011


これは Node のプロセスを複数起動した「ファーム(コンテキストによってはプールと呼ばれるものと思う)」を構成し、重たい処理は専用のファームに振ってしまうという考えです。


情報の共有はプロセス間通信で行い、プロセス間通信自体は依頼する側から見れば非同期処理なので、依頼側のメインループは止まらない。おもしろい考えだと思います。
(スライドに文字で載っている以上にいろいろな事を言っていたので、自分が聞き取れなかった説明もあります。ビデオが公開されたら、頑張って聞きたい。)


ちなみにこの発表では実装案として cluster について言及がありました。


SSP とスケール

では、 Node のアプリをスケールさせるのに必要そうなモジュールやらは一通りありそうです。次は実際に SSP をスケールさせる際の構成を考えてみます。


まず、再確認ですが SSP での node の立ち位置は、 RESTful JSON API の公開です。
この API はアプリケーションを成り立たせるために、リソースに対する CRUD をサポートする必要があります。


リソースの永続化は RDB よりも NoSQL の方が良さそうです。そもそも RESTful API でのリソース操作には RDB の細かい機能はあまり必要としません。
というかあまりそういう機能に頼らず、実装する方がスケールさせやすです。


昨今の NoSQL の持つ Replication や Sharding 機能を用いた分散構成を考えています。
また、 ドキュメント指向のストレージの場合、 JSON をほぼそのまま格納できることが多い点もメリットになります。

プロセスごとのサービス

複数のプロセスを起動して、そこに永続領域に対する RESTFul JSON API を公開したサービスをたてる形でサーバを構築すると、いくつか方法が考えられます。
自分のイメージとしては以下の3つのような感じになるのかと。

構成1
  • データを ID のレンジで区切ってサーバに配置する。
  • プロセスにはレンジごとの CRUD を用意しておき、 memcache 等でキャッシュ可能にしておく。
  • レンジに応じた URI を設計し、プロセスにはレンジに応じてリバースプロキシで分散する。
  • レンジごとのデータに対応したストレージが用意できるのであれば、サーバはたぶん何でもいい。


  • 長所
    • API サーバが対象とする範囲が狭いので、キャッシュが一番効きやすい。
    • たぶんどんな DB でもできる。
  • 短所
    • ID ごとに区切るため自分でそのデータの分散を管理しないといけない。
    • データの性質によっては各サーバへの負荷が一定とは限らない。
構成2
  • データをシャーディングする。
  • プロセスには全ての CRUD を用意しておく。
  • 処理は Loadbalancer で分散する。
  • シャーディングの管理は DB に任せる。



  • 長所
    • ほぼ Mongo を使う構成で、 DB の管理は mongo に任せられる。
    • その後のスケールもさせやすい。
  • 短所
    • プロセスが対象とするデータ範囲は実質全体なのでキャッシュ効率は下がる
    • mongos にあたる部分の SPoF 対策が必要。
構成3
  • データをレプリケーションする。
  • プロセスには全ての CRUD を用意しておく。
  • 処理は Loadbalancer で分散する。
  • データの同期は DB に任せる。


  • 短所
    • プロセスが対象とするデータ範囲は実質全体なのでキャッシュ効率は下がる
    • サーバ間の同期が常に必要。Eventually Consistent を許容しないと行けない。
    • リアルタイム Web のバックなのに、その都度サーバ間の同期がネットワークを走るモデルはどうなのか。


couch や riak にはそれ自体が RESTful な API を持っているのでそのままでもたたける。
しかし、 WebSocket や 認証やセッションを考えると、直で叩くより裏に置く方が良いのかなと思う。
上の中では構成2が現実的なのかな、その用途では Mongo が一番相性がいいのかも。

まとまらないまとめ

アプリを SSP で実装した場合のバックエンドの Node による RESTFul JSON API のスケールについて考えました。
最終的には、色々と複雑な構成になった気がしますが、スケールアウトを考えると構成が複雑になるのは、 SSP に限らずそうだと思います。
実際の大規模サービス等の運用の場面では、もっと複雑なことが行われているだろうし、今回はそういった点は目をつぶっています。
プロセスの複数起動、プロセス間通信、 WebSocket の共有、 NoSQL による Sharding / Repliaction など、一通りのネタはそろいました。
そしてこれ以上は実際にやってみないとよくわからない。しかしこれから試してみるべき方針はなんとなくわかってきた気がします。


あと、前回はキャッシュに色々こだわって、SSP の裏に実装する JSON 形式のリソースは、キャッシュが容易だろうと考えていたけど、
本当にそうかよくわからなくなってきた。。
キャッシュをふんだんに取り入れるのは、それだけキャッシュのライフサイクル管理が大変になるのも事実です。
この辺は、もう少し考え直そう。

終わりに

なんとも煮え切らない結果になってしまった。
自分がよくわかってないところが多すぎて、たぶんわかっている人から見るとしょうもない内容になっていると思います。
ずっと考えててもしょうがないので、いったんここで出します。
続きはもう少し経験を積みながら考えて行こうと思います。
なんかちょっとでもフィードバックがあったらもらえるとうれしいです。


この記事は、なにかわかったら修正していくかもしれない。
そして、まとまってきたら、別で書くかも。

Node におけるスケールアーキテクチャ考察(SSP 編)

*息抜きがてら書いていたら長くなってしまった。。
*当たり前ですが、あくまで個人的な考えです。
*ころころ変わるかもしれません。


Node の基本的な知識についての話は色々なところで出始めて、
じゃあこーいう場合はどうするの? みたいな話が出始めたりもするようになってきた気もします。


正直、自分にもまだ分からないことだらけです。
そもそも自分はそこまでスケールに関するアーキテクチャや、OS の低レイヤに精通しているとは言えないので、
これを期に Node は何が得意で何が不得意なのか、スケールさせるために考えないといけないこと、などを自分なりにまとめて、
ついでに、これまで学んできた周辺のアーキテクチャに関する知識も混ぜて、色々思考実験をしてみたいと思っています。


だから WebSocket にブラウザが対応してないとか、そんな複雑なサーバ群本当に運用できるのかとか、
そういう話は無しに、とにかく考えてみようと。


とりあえず今の時点では

  • Node に合うアプリケーションと実装
  • Node に合うスケールアウトとその方法

について書きたいと考えています。


後半がまだ書けないので、まず前半だけ出します。
特に大したことは書いてありません、今まで自分がやってきたことを、Node と合わせて考え直しただけです。
スケール云々の前提として、どういう方向でアプリが作れるかという話。


(色々書いて、理解が深まったらこのエントリを修正したり、part2的なエントリやら修正エントリを書くかも。)


Node の前提知識

Node っていうと必ず最初に出てくる、以下キーワード関連の基礎知識は省略。

関連
node.js とは何か - I am Bad at Math


あと

CSJS
Client Side JavaScript
SSJS
Server Side JavaScript
CSDB
Client Side DataBase(localStorage, IndexedDB etc)
SSDB
Server Side DataBase
SSP
Semi Single Page (Approach|Architecture)

Node の得意なこと、不得意なこと

Node は他の言語で行っている大抵のことができますが、特化しているのはサーバ周りの実装です。
特に Web アプリ等が手軽に実装できるのですが、本当に得意なのは何なのかを、もう少し考えてみます。


Node が最も得意とする点は、I/O の処理だと考えています。ここで言っている I/O は主に "ネットワークI/O" と "ディスクI/O" といった 比較的重い I/O
対して最も不得意な処理は CPU バウンドな処理だと考えます。理由は Node がシングルスレッド上のイベントループで処理を実行するためです。


Node に I/O 処理が要求された場合、この処理は非同期に行われるから I/O している最中は後続の処理を継続できる。
しかし、I/O を伴わないCPU バウンドな処理は非同期には実行されないため、イベントループが止まってしまい、以降の処理がブロックされる。
(もちろん回避策は無くはないけど、基本の話)


これまで主流だった、同期 I/O で処理する場合、I/O の処理が重たい場合は後続の処理が止まります。
Web アプリ等の場合は、クライアントの I/O をすべて一つのスレッドに並べて処理することは難しいため、
多くの場合はスレッドを複数起動する事でこれを解決してきました。


スレッドを複数起動するとそれぞれがメモリを消費するし、スレッドを起動し続けても限界点として C10K なんて問題もあります。
Node は、沢山の I/O 処理が来ても非同期でノンブロックな処理ができるし、たった一つのプロセスで処理しているためメモリの消費が抑えられる、 C10K に対する耐性もある。
(ベンチは見つからないけど、このシングルプロセスが処理できるイベント(= I/O)の総量にも限界はあるはず。)


これが主に重たいネットワーク I/O が増えがちなリアルタム Web や、ファイルアップロード/ダウンロードとの相性が良いとされる理由の一つです。

関連
Jxck's OutPut - Nodeのモチベーション

CPU 処理を I/O に振る

これらをふまえると、Node では CPU 処理ではなく、I/O 処理が多発する場面での活躍が期待できます。
ネットワークの I/O とディスクの I/O に強い事は、ディスクから取り出したデータを HTTP でクライアントに渡すという Web アプリで効果を発揮すると言えます。


手軽に強力なサーバを実装できる、というのは Node の一つのモチベーションにもなっていて、事実 HTTP 周りの API はかなり充実しています。
正しく生かしていくためには、 I/O 処理 > CPU 処理 な特性を把握することが重要です、時には、これまで CPU で処理してきたものを、
I/O の処理に振ってしまうという考え方もありだと思います。


Node とテンプレートエンジン

たとえば、テンプレートエンジンを用いてデータを HTML や XML に加工しようとすると、そこには CPU バウンドなレンダリング処理が入る事になります。
これは従来の Web アプリでは基本的な考えだったけど、 特に haml, jade, stylus 等の抽象度の高いテンプレートエンジンはレンダリングにそれだけ演算を必要とするため、
性能面で見れば Node との相性がいいとは言い切れないと思います。


ここで、先ほどの CPU 処理を I/O に振る考え方を適応すると、シングルページなアーキテクチャが見えてきます。
つまり、Node はディスクから取り出したデータを JSON 等で配信するサービスを提供し、クライアントは非同期(Ajax, WebSocket)にそれを取得して画面を構築します。
画面のレンダリングをクライアントに振ること自体は、昨今のクライアント(PC, スマホ, テレビ etc)のスペックであれば大きな問題にはならなくなってきました。


レンダリングしたページの配信に比べると、Ajax や WebScket を多用するアプリケーションには、コネクション数の増加の問題が懸念されるかもしれません。
しかし、それはサーバから見れば、CPUバウンドなレンダリング処理を、多数のネットワークI/Oに置き換えた形になっています。Node には、その方が良いのではないでしょうか?


ところが全てを本当に単一のページで行うのは限界があると考えているため、ある有効な点でページは遷移させます。
また、実際は全てを Ajax やら WebSocket で行うことが非効率な場合も有るでしょう。多少は埋め込むことも有ると思います。(埋め込むと後で言うキャッシュの問題がでるけれど)
それでも従来に比べて圧倒的に遷移が少ないため、ここでは仮にセミシングルページ = SSP (アプローチ|アーキテクチャ) としておきます。

次はこの SSP の特徴をもう少し見てみます。

関連
Jxck's OutPut - Nodeにテンプレートエンジンはもういいかな

SSP=Semi Single Page(Approach|Architecture)


比較のために、サーバで画面(HTML)を動的に構築してレスポンスとして返すスタイルのアーキテクチャの特徴を振り返り、以下の二点に注目します。

  • ページの構築で同期する。
  • ページ自体をキャッシュしにくい。
画面というリソース形式の限界

Web ではサーバに対するリクエストをインプット、レスポンスをアウトプットととらえるのが標準的で、
細かいオーバーヘッドを無視した場合、アウトプットが複数の I/O を伴う場合の合計時間は、同期・非同期の場合以下のようなイメージになります。

A,B,C
ディスク I/O
X
レスポンスできる状態のリソースが完成した点


同期I/O sum(A,B,C,...)+render

A----
         |
         B---------
                           |
                           C----
                                    |
                                    render---X -> HTTP response

非同期I/O max(A,B,C,...)+render

A---........
                  |
B---------+ render---X -> HTTP response
                  |
C------.....

*あくまでイメージです。


HTML画面というアウトプットを伴う場合は、レスポンスの時点でレンダリングが終了している必要があり、
レンダリングの時点で必要なI/Oが終了している必要があります。(非同期なレンダリング等は除く)


これでも、非同期I/Oによってレンダリング終了までの時間が「短くなる可能性」があることは分かりますが、
先ほど言った「バウンドなレンダリング処理はしたくない」という考えから、これも Node を生かし切れているとは言えません。


ということで、アウトプットは HTML ではなく JSON にしてしまいます。
実は Node の多くの DB や File 関係モジュールの API は、アウトプットが大体 JSON なので、それぞれに URI を振って、RESTful JSON API を実装するためのコストは比較的低いです。
ベースとなる HTML ファイルは別途配信しますが、これはレンダリングを伴わず( haml や sass とかでもあらかじめレンダリングしておく)配信できます。

(renderd)HTML-* -> HTTP response

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A----X -> HTTP response

B---------X -> HTTP response

C----X -> HTTP response


クライアントはこれらを順次 Ajax や WebSocket で受け取って DOM 操作で画面を構築することになります。


リソースのキャッシュ

レスポンスを画面にすると、構築にコストがかかるくせにキャッシュしにくいという問題が常に付きまとい、 Web アプリケーションフレームワークは長いことこの問題と闘ってきました。
多くの場合はセッションが保持する情報の一部(ユーザ名とか)が画面に反映されているために、キャッシュできるほど汎用性が無いのが問題です。


たとえば Rails3 では現在三つのキャッシュ機構を提供しています。

  • ページキャッシュ
  • アクションキャッシュ
  • フラグメントキャッシュ


ページキャッシュが画面全体のキャッシュにあたり、先の理由からこれが活躍する場面は非常に限定的です。
一番多用するのは、一部だけをキャッシュしておくフラグメントキャッシュだと思いますが、これも実用的ではあっても、割と苦し紛れの方法な気がします。


RESTful JSON API で用意されたサービスが返すデータは、 DB から取り出した後、多少手を加えただけのもので、
これをキャッシュするのは非常に簡単だし、 HTML と比べると容量も小さい。各サービスを提供する Node プロセスに対して memcache を付ければそれだけで効果を発揮します。
この時点ではまだ、 DB が KVS であるかどうかも関係なく、URI がきちんと設計できているかによるでしょう。


また JSON でやり取りしていれば、送信先のクライアントで、IndexedDB 等を用いたキャッシュも視野に入れられます。
CSDB を用いたキャッシュは、そこに対して CRUD までしてしまうような CSJS を組むことで、システム全体でみた場合バッファの役割にもなります。
オフラインでの動作と、リクエストの減少、クライアントデバイスのリソース有効活用等、今後は CSDB をどう使うかがかなり重要になってくると考えています。


逆にシングルページの土台となる HTML, JS, CSS といった静的なファイルの配信については、 Apache や Nginx といった信頼性のあるサーバに譲る方がいいかもしれません。
また動的なデータを担う JSON を除いた .html, .css, .js といった静的ファイルは、文字通り静的リソースでありキャッシュ可能です。
squid 等のサーバのメモリに載せてしまえばいいし、If-modefied-since や cache-manifest を通じて、クライアントにキャッシュすることもできます。


キャッシュ可能性を高めることは、Web の高速化の常套手段です。


これはリソースからアプリケーションの状態を極力排除することで、より効率的に得られます。
SSP ではアプリケーションの状態はクライアントで管理し、クライアントがその状態を表現するために必要なリソースをサーバにリクエストします。
サーバがレスポンスするリソースの形式は「リソースの状態」を JSON という必要最小限の方法で表現しているため、キャッシュが効きやすいのです。


関連

リソースのビューとリアルタイム Web のクライアント

忘れてたのではっきり書いておきますが、こうした実装が必要なアプリの代表としては、リアルタイムな Web なのが自分の中で暗黙の了解になってました。
普通にトランザクションガチガチな CRUD をする業務アプリをこんな風に作る必要は無いでしょう。


そしてリアルタイムなアプリケーションは、クライアントがリッチになりがちです。


サーバは JSON API を用意すればいい、そしてキャッシュしましょうといういつもの話になるだけですが、
ここまでで、結果的には Node の得意な部分に合わせて、都合の悪い以下の部分をクライアントに押し付けた形になります。

  • 画面構築
  • アプリケーション状態管理


これによりクライアントでは、サーバから押し付けられた色々な処理を作りこまないといけなくなります。
これらのノウハウは CSJS な分野で色々たまっているかとは思いますが、簡単ではないですね。


ただ、経験的には、「既存のシステムの一部を Ajax 化」なんてやってだんだん複雑になっていくよりも、
最初から「全てクライアントでやる」と割り切ってスタートした方が、色々と覚悟ができていいような気もします。


現在こうした SSP を本気で構築するために必要な技術としては、以下のようなものがあるかと思います。

  • WebSocket(NodeならSocket.IO)
  • PushState(pjax)
  • WebStorage(localStorage,IndexedDB)
  • WebWorker
  • backbone.js(CSMVC)
  • underscore.js(utility)
  • jQuery(DOM)

WebSocket ベースでデータをやり取りしたデータを IndexedDB に保存し、
backbone.js 上で処理し、DOM は今まで通り jQuery で操作しながら、
必要に応じて PushState を挟むみたいなのが最先端のトレンドになるんでしょうか?w


状態管理にセッションが皆無とは行かないとおもいますが、 Socket.IO は将来的に Express とセッションを共有できる予定なので、その辺を使って行くのが良いかと思います。

関連
2011-05-30 - Block Rockin’ Codes


またここにあがった技術は、ブラウザの実装どころか、仕様すらままならないものもあります。しかしリアルタイムな Web を作っていく上で、レガシーブラウザの非対応はほぼ必須用件になるでしょう。それなら、クロスブラウザより先を考えた方が有益と思います。 Socket.IO は優秀なクロスブラウザライブラリをもつけど、それだけで解決する問題ばかりではないですし。


いずれにせよ、かなり規模の大きいJS開発になると思います。
Node はサーバ・クライアント両方がJSで書けるので、以下のようにノウハウや既存のリソースを再利用できたりします。

  • ES5(ほぼ)対応
  • 同じエディタが使える
  • 同じユニットテストフレームワークが使える
  • クライアント用のライブラリを移植できる。
  • (バリデーション等)ロジックを双方で共有できる。
  • 学習すべき言語を一つに絞れる。


ちなみに生産性を考えると CoffeScrpt も見えてきます。
(これはまだ賛否両論有るようです。)


このメリットは、規模が大きくなればなるほど、効いてくるかも知れませんね。
(そう思わない人ももちろんいるとは思います。)

関連
Jxck's OutPut - キャッシュ主体のアプリケーション(クライアント)

パフォーマンスの考え方

もちろんですが、このスタイルのアプリケーションのパフォーマンスは AB(Apache Bench) といったツールで簡単には測ることができません。


ページの構成はクライアントで非同期に行われるため、何らかの仕組みで、最後の Ajax 通信の結果が DOM に埋め込まれるまでの時間を計る必要があるかもしれません。
しかし、 WebSocket を用いて、サーバから更新がプッシュされ順次更新されていく場合は、これも意味がなくなります。


そもそも、そこが目的で、ベースの画面をさっさと表示して、残りのデータが順次くる訳なので、
UI 設計に注意して「体感速度」を向上させ、それをうまく測らないといけません。難しいでしょう。


AB でテストするのは、各 API のレスポンスタイムで、ここは細かいチューニングが API ごとに可能かもしれません。

関連
Jxck's OutPut - ab テストより体感テスト

まとめ

全体的な欠点としては、キャッシュの更新や管理、そして SSP なアプリを実装する技術的な難易度です。
ここはもっと考えていかないといけないところですね。

また、SSP では、トランザクションを必要とするような処理が必要なアプリには向いていないでしょう。
その場合は、(画面遷移を伴う)マルチページ?な従来の方法を使うんだと思いますし、 Node でそれも不可能ではありません。
ただ、そうなった場合に I/O よりもレンダリング等の問題がネックになるのであれば、それは恐らく Node でやることではないのでしょう。
そうした処理は Rails 等に任せて行うのもよいかもしれません。
それらが共存するアプリの解決手段が Rails + EventMachine なのかもしれませんが、Node + Rails の組み合わせも検討対象かもしれません。


とにかく Node には以下のような特徴があります。

  • 非同期でノンブロッキングI/O
  • シングルスレッド上のイベントループによる省メモリな処理
  • ECMA5(ほぼ全)対応の JavaScript による記述


これらの意味をきちんと把握して、それを生かして初めてメリットがあります。
誤解を恐れずに言えば、自分は「Nodeは万能だ」などとはこれぽっちも思っていません。
(全ての言語、WAF、ライブラリがそうであるように)

あとがき

今回は、Node を生かすためには、 SSP なアプローチがどうもよさそうで、
そうするとレンダリングなくせるし、キャッシュもできるし、WebSocket でリアルタイムまんせーなアプリができそうだよね。
という、言われてみれば今更な話でした。


次回は、Node でそんなアプリを作る場合、サーバサイドの構成はどんなものが考えられるのか、を考えてみたいと思います。そっちが本当のスケールの話です。主にロードバランス、リバースプロキシ、マルチプロセス、Sharding した KVS を用いたスケールアウト構成についてです。

フィードバックや新たな発見が有れば、色々考えが変わるかもしれません。
今回はやや手応えが薄いですが、指摘、コメントは相変わらず大歓迎です。

Socket.IO v0.7 のロードマップと新機能

Node と言えば、Socket.IO といっても過言ではないほど、Socket.IO は Node にとって大きい存在です。


先の NodeConf で、この Socket.IO の作者 [twitter@rauchg] より、
Socket.IO v0.7 にむけたロードマップについての発表がありました。
新機能や API の改善等、重要な変更があるようなので一部を紹介したいと思います。


資料は以下。

Socket.IO(0.7)


(注 2011/05/31 現在 v0.7 はブランチで開発中であり、以下の紹介は上記資料をもとに書いているため、実行確認はしていません。
正式にリリースされた場合は、実装をもとに別途エントリを作成すると思うので、このエントリ自体は更新されない予定です。)

API の改善

サーバインスタンス

これまでは、ソケット通信を確立するためには、 http サーバインスタンスを渡す必要が有りましたが、今後は必要なくなります。

// Before
var io = require(‘socket.io’);
io.listen(http.createServer());

// After
var io = require(‘socket.io’);
io.listen(); // creates one for us

var io = require(‘socket.io’);
io.listen(8000); // creates one for us and listens on port 8000
オプション設定

これまでは全体で共通する設定を listen() 時に一つ適応できましたが、
今後は、 configure() メソッドを通じて listen() とは別に設定できます。
また configure() は第一引数に環境('production','development','test' etc) を設定でき、設定を切り替える事ができます。
(おそらく express との連携を視野に入れた対応)

// Before
var io = require(‘socket.io’);
io.listen(app, {
  tranportOptions: {}
});

// After
var io = require(‘socket.io’).listen();
io.configure(function () {
  io.set(‘polling duration’, 30);
});

io.configure(‘development’, function () {
  io.set(‘log level’, 3);
});

新機能

揮発性メッセージ

volatile.send() で、送信したら消える、揮発性のメーセージ送信をすることができます。

io.sockets.on(‘connection’, function (socket) {
  var t = setInterval(function () {
    getStock(‘APPL’, function (price) {
      socket.volatile.send(price); // send して終わり
    });
  }, 500);
  socket.on(‘disconnect’, function () {
    clearInterval(t);
  });
};
JSON

json.send() で送信データがJSONである事を明示できます。(シリアライズ不要)

io.sockets.on(‘connection’, function (socket) {
  socket.json.send(‘string’);
  socket.json.send(5);
  socket.json.send([
        {}
      , {}
  ]);
});
メッセージディスパッチャ

複数プロセスや分散環境での容易なスケールを目的とした機能も強化されるようです。
想像ですが、デフォルトではメモリとシングルプロセスになりますが、おそらく Redis 等との連携、マルチコア環境の適応を視野に入れているのかと。

まず環境をまたいだ変数共有のようなことができるようです(?)。

io.sockets.on(‘connection’, function (socket) {
  socket.get(‘nickname’, function (err, nick) {
   // セッションに対して設定した nickname の操作。
  });
});


非同期に接続中のクライアントを取得する場合は以下、環境をまたぎ、非同期に取得するようです。

io.sockets.on(‘connection’, function (socket) {
  io.sockets.clients(function (clients) {
    // 接続中のクライアントに対する操作
  });
});
送達確認

単純に send() メソッドにコールバックを追加することで、送達確認処理ができるようになります。

// server side
io.sockets.on(‘connection’, function (socket) {
  socket.send(‘bye’, function () {
    // 送達確認
  });
});

// client side
var socket = io.connect();
socket.send(‘hi’, function () {
  // 送達確認
});
// server side
io.sockets.on(‘connection’, function (socket) {
  io.on(‘nickname’, function (nick, fn) {
    checkAvailability(nick, fn);
  });
});

// client side
var socket = io.connect();
socket.emit(‘nickname’, prompt(‘Nickname’), 
function (res) {
  // if the function arity > 0, a callback is passed
  // to the event listener and the protocol signals
  // this is a Data ACK
});
名前空間

for() を使って、イベントごとの名前空間を指定できるようになります。
(無ければデフォルト)

// namespace “” (default)
io.sockets.on(‘connection’, function (socket) {
  io.on(‘nickname’, function (nick, fn) {
  });
});

// namespace “/news”
io.sockets
 .for(‘/news’)
 .on(‘connection’, function (socket) {
   io.on(‘item’, function (nick, fn) {
  });
});


for() ではなく、connect() を使ってソケット自体の名前空間で分ける事ができます。

var socket = io.connect(‘http://localhost/chat’)
var socket2 = io.connect(‘http://localhost/news’)

これにより、イベントやメッセージをチャネルごとに操作する事ができます。
(これまでやってきたチャネリングのハックが、実装に取り入れられる。便利!)

var news = io.sockets
 .for(‘/news’)
 .on(‘connection’, function (socket) {
   io.on(‘item’, function (nick, fn) {
   });
   news.emit(‘item’);
 });

var stream = io.sockets
 .for(‘/stream’)
 .on(‘connection’, function (socket) {
   io.on(‘item’, function (nick, fn) {
   });
   stream.emit(‘item’);
 });
ファーストクラスイベント
io.sockets.on(‘connection’, function (socket) { 
  io.on('chat message', function () {                             
  });
  io.on(‘nickname change’, function () {
  });
  io.sockets.emit('user connected', socket.id);
});

What’s coming

Express.IO

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

Other

他にも

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

などと、開発に意欲的な事が伺えます。

まとめ

Socket.IO 自体の完成度が上がる事で、リアルタイムアプリケーションを開発する際に出来ることもより増えると思います。
Guillermo Rauch さんはとてもストイックな開発者なので、今後のコミットにも期待したいと思います。
(日本に来ないかな ボソ)

node.js で エコーサーバと簡易コンテンツサーバ

追記

ここの内容は Socket.IO のバージョンが v0.7 に上がったことで、古くなりました。
v0.7 については Socket.IO API 解説 - Block Rockin’ Codes を参照してください。

本文

リアルタイムWebハッカソン : ATND に参加しました。

みなさん websocket を用いて開発する感じで、websocket の実装としては node.js を筆頭に
jetty や ChannelAPI の話も聞けてかなり充実したハッカソンだったと思います。

ここで自分は node.js の websocket ライブラリである socket.io をいじってたんですが、
いくつかアプリ書いて、共通するのは以下のような感じだなということで簡単なメモ。

socket.io でエコーサーバ

websocket でリアルタイムなアプリとなると、socket.io を使ってとりあえずエコーサーバ(サーバに送信すると、サーバがクライアント全員にそれを配信する)を実装する感じかと思います。
シンプルに書くとこれだけ。

var socketio = require('socket.io');

// websocket
var io = socketio.listen(server);

io.on('connection', function(client) {
    client.on('message', function(message) {
        //クライアントがメッセージを送って来たら実行される。
        //messageが送って来たデータ
        client.send(message); //送って来た本人だけに送る。
        client.broadcast(message); //送って来た人以外全員に送る。
    });
});

こんな感じに実装しておきます。

で、ここに投げて受け取るだけのクライアントは、 jQuery を使うとこんな感じ。

$(function() {

    // socket の生成
    var port = 8080;
    var socket = new io.Socket(null, {port: port});
    socket.connect();

    // 送信
    $('#button').click(function() {
        var message = $('#input').val();
        socket.send(message);
        return false;
    });

    // 受信
    socket.on('message', function(obj) {
        $('#output').text(obj);
    });

});

あとは、受け取ったクライアント側でそれをどう表現するかですね。



なので後は html やら js ファイルを配信出来れば良いわけです。

簡易コンテンツサーバ

node ではフレームワークを使わない場合は静的コンテンツの配信も自分でやる必要が有ります。
いちいち書くのは面倒くさいので、相対パスから必要なファイルが落ちるように、以下の様なものを書いておくと幸せになれます。

var http = require('http'),
    url = require('url'),
    path = require('path'),
    fs = require('fs'),

var port_local = 8080;

var load_static_file = function(uri, response) {
    // 相対パスで静的リソースを配信する。
    var tmp = uri.split('.');
    var type = tmp[tmp.length - 1];
    var filename = path.join(process.cwd(), uri);

    path.exists(filename, function(exists) {
        if (!exists) {
            response.writeHead(404, {'Content-Type': 'text/plain'});
            response.write('404 Not Found\n');
            response.end();
            return;
        }

        fs.readFile(filename, 'binary', function(err, file) {
            if (err) {
                response.writeHead(500, {'Content-Type': 'text/plain'});
                response.write(err + '\n');
                response.end();
                return;
            }

            switch (type) {
            case 'html':
                response.writeHead(200, {'Content-Type': 'text/html'});
                break;
            case 'js':
                response.writeHead(200, {'Content-Type': 'text/javascript'});
                break;
            case 'css':
                response.writeHead(200, {'Content-Type': 'text/css'});
                break;
            }
            response.write(file, 'binary');
            response.end();
        });
    });
};

これで、ファイルが一通り配れます。

結局リアルタイムウェブって

client.broadcast(message); 

この1行がリアルタイムらしさってことなんですかね。


なので、上のようなエコーサーバとファイルサーバを用意しておいて、
クライアント側でサーバのプッシュをうまいこと取り回す JS を書くって感じになることが多いかと思います。


という意味で上の二つはちょっとしたものを書くなら結構汎用的だと思います。


一応、動く様にした物を github に上げておきます。(socket.io.nodeは npm 等で用意してください。)

http://github.com/Jxck/node-blog-sample

cloneしたら移動して、

% node server.js

で起動して、http://localhost:8080/client.html でアクセス出来ます。

and more

上のような実装でちょっと遊んだら、次に欲しくなりそうなもの。

  • websocket のマルチチャネル
    • この実装では例えばチャットルームを複数作ることができないので、こういうのが欲しくなる。
  • 永続化(DB)
    • node でも DB を使う場合は KVS と合わせるのがトレンドのようです。mongo や redis あたりが多いようです。
  • テンプレートエンジン
    • haml とか使えるんですが、JS 大好きなあなたは jed/fab · GitHub に挑戦すると良いでしょうw


後はアイデア次第ですね。

node.jsのSocket IO-nodeでWebSocket

前回は、pywebsocketでのWebSocketを紹介しましたが、HTML5 hack-a-thonでは結局node.jsの実装である Socket IOとSocket IO-nodeを使いました。
こちらも備忘録。

node.js

node.jsはいわゆるサーバサイドJavaScriptの一つで、最近にわかに注目を集めてる(?)技術です。

主な特徴は

  • もちろん全てJSで書ける。
  • コアがGoogleのV8エンジンで速い
  • イベント駆動I/O


node.jsの概要自体は、こちらのPREZIを使ったプレゼンが簡潔で分かりやすいかと思います。

Introduction to node.js by Toshihiro Shimizu on Prezi


丁度今日、node.jsの48時間耐久Hack-a-thonが開催されていました。
その名も、Node.js Knockout。@さんも参加されてます。


Node.js Knockout | Nov 9-11th, 2013


node.jsについてはそのうちもう少し纏めた記事を書こうかなと思っています。
思った以上に手軽に色々出来るので、面白いです。


node.websocket.js

node.jsのWebSocket実装としては、ちょっと前までは以下のプロジェクトが良く紹介されていました。
今月のWeb+DB Press の「HTML5×CSS3」でもちらっと紹介されています。


guille/node.websocket.js · GitHub


とりあえずソースを取得し、runserver.js を起動したところ以下のようなエラーが出ました。

this.conn = new process.tcp.Connection();
                            ^
TypeError: Cannot read property 'Connection' of undefined


どうもこの実装は、node.jsのver 0.1.9以前でないと動かないようで、自分がビルドしているver 0.2.0ではサーバを起動することが出来ないようです。
(tcp.Connection()ではなくのAPIが変わってるみたいですね、今はnet.Stream() を使えばいいみたいです。)

プロジェクトのコミット自体が、2010/04/30で止まっていることを見ても、node.jsのバージョンアップに追従していないようです。

で、READE.mdには以下の記述。

Update: For a more complete and cross-browser/cross-platform solution, 
I recommend you check out Socket.IO and Socket.IO-node, which I'm actively maintaining.

今は、Socket.IOとSocket.IO-nodeをメインでメンテナンスしている。
そっちの方がより完璧なクロスブラウザ/クロスプラットフォームを実現しているので、そっちをお勧めするよ!

とのことです。

ということで、Socket.IOを使うことにします。

Socket.IO と Socket.IO-node

こちらも、node.websocket.jsと同じ作者の公開している実装です。
サーバ側の実装とクライアント側のライブラリの両方が提供されています。

Socket.IO-node

基本的にはWebSocketの実装というより、リアルタイム通信用のサーバ実装という位置づけのようです。
つまり、WebSocketが無いクライアントには、代替の手段を提供する実装になっているということで、以下をサポートしています。

    * WebSocket (with Flash policy support)
    * XHR Polling
    * XHR Multipart Streaming
    * Forever Iframe

node.jsはver0.1.102以上が必要とのことです。
また、クライアント側はSocket.IOを専用クライアントライブラリとして使います。
(普通のWebSocketオブジェクトを作成して試しましたが、通信は出来ませんでした。)

Socket.IO

Socket.IO-node専用のクライアントライブラリです。
これを使って実装すると、WebSocketの無いブラウザでもリアルタイム通信を実現することが出来ます。
READMEによるとサポートしているのは、

Supports

    * WebSocket
    * Adobe Flash Socket
    * ActiveX HTMLFile (IE)
    * XHR with multipart encoding
    * XHR with long-polling
    * JSONP polling (for cross-domain)

Tested on

    * Safari 4
    * Google Chrome 5
    * Internet Explorer 6
    * Internet Explorer 7
    * Internet Explorer 8
    * iPhone Safari
    * iPad Safari
    * Firefox 3
    * Firefox 4 (Minefield)

素晴らしいですね。
Draftの違いだけでなく、WebSocketの実装有無もカバーしてくれるので、WebSocketの無いブラウザでも同じコードで動かすことが出来るという点です。
多分現時点でここまで実用的なライブラリは中々無いと思います。


使い方

注意が必要なのは、二つが依存関係というか、二つペアで使うことを前提にしている点です。
なので、githubのソースも片方づつではなく、両方いっぺんに落として来て配備することが出来る様になっています。
Socket.IO-nodeの方に書かれてますが、以下の様に取得します。

git clone git://github.com/LearnBoost/Socket.IO-node.git socket.io-node --recursive
cd socket.io-node/example/
sudo node server.js


これで既にサンプルのchatアプリが動かせます。
それを動かしながら色々試してみたところ、前回の記事の様に、
サーバ:「来た物をそのまま返す」
クライアント:「サーバにただデータを投げて、ただ受け取る」
といった簡単な実装であれば、大まかにこんな感じで実装します。

//サーバ側の実装
var http = require('http'), 
    io = require('./path/to/socket.io'),

server = http.createServer(function(req, res){
    // サーバの設定
    res.writeHeader(200, {'Content-Type': 'text/html'});
    res.writeBody('<h1>Hello world</h1>');
    res.finish();
});

server.listen(8080); //リスニングするポート

var io = io.listen(server); //socketの取得

io.on('connection', function(client){
	//ユーザが接続して来たら実行される
	//接続時の初期化処理を書く

	client.on('message',function(message){
		//クライアントがメッセージを送って来たら実行される。
		//messageが送って来たデータ
		client.send(message); //送って来た本人だけに送る。
		client.broadcast(message); //送って来た人以外全員に送る。
	});

	client.on('disconnect', function(){
		//クライアントが切断したら実行される。
		client.broadcast(client.sessionId + ' disconnected' }); //他全員に切断した人のsessionIdを送る。
	});
});
//クライアント側の実装
//socket.io.jsファイルを読んでおく

//コネクションの確立
io.setPath('/path/to/socket.io/');
socket = new io.Socket('localhost',{port:8080});
socket.connect();

//データの送信
socket.send('data');

//データの受信
socket.on('message', function(data){
	//受信したら実行される
	//dataが受け取ったデータ
    console.log(data);
});


通信部分の実装自体は非常にシンプルですね。基本的にはこれだけです。
あとはこれどう使うかというアイデアだけです。


サンプルの中にあるChat等をいじりながら、色々試してみると良いと思います。

あと、今回のnode.js KOでも、リアルタイム系のアプリが多く有るようです。
おそらくSocket.IOを使った実装も多いはずなので、これらは多いに参考になると思います。
以下は、Hack-a-thonの作品と参加者の一覧。

Node.js Knockout | Nov 9-11th, 2013

メモ

node.jsは自分もまだ始めたばかりですが、今回使ってみて思ったところをメモしておきます。

  • node.jsは開発者がパフォーマンスをかなり気にしているだけあって、噂通りサクサク動いてる気がする。
  • イベント駆動IOの利点を生かして、こうしたソケット通信を用いたリアルタイムアプリケーションとnode.jsは相性が良いと言えそう。
  • Linux/Unixであれば導入する敷居はさほど高くない。
  • サーバ/クライアント共に同じ言語で書けるのは結構有り。
  • APIの実装も進み、フィレームワークもいくつか出ている。


node.js色々楽しみですね。