Block Rockin’ Codes

back with another one of those block rockin' codes

Socket.IO と Express でセッションの共有

Socket.IO のサーバは v0.7 からスタンドアローンでも立てられるようになりましたが、
Express のサーバ上に Socket.IO のサーバを同居させる構成は多いと思います。
しかし Socket.IO は Express が HTTP で確立したセッションとは別のコネクションを確立するため、
例えば、 Socket.IO で接続したユーザが Express で認証したユーザかどうか等が判別できません。
そこで、 Socket.IO で接続を確立時に Express のセッション用の Cookie を取得して、接続を識別できるようにし、
さらに Socket.IO でのやり取りが長くなっても、その間にセッションデータが切れることが無いように、更新する必要が有ります。


Socket.IOv0.7 を用いてこれを実現する方法が、こちらで紹介されていたので、試してみました。

Socket.IO の使い方(特に認証周り)は Socket.IO API 解説 - Block Rockin’ Codes を参照してください。

セッションへのアクセス

Express のサーバ上に Socket.IO を載せられますが、
そのままでは Socket.IO と Express はお互いの接続を認識できません。
Socket.IO v0.7 では、接続時のハンドシェイクを通じて、 Express の発行した Cookie の情報を取得できます。


ポイントは

  • Socket.IO のセッションを確立するとき、認証を挟むことが出来る。
  • 認証時にハンドシェイクが行われる。
  • HTTP ヘッダからハンドシェイクデータが収集される。
  • ハンドシェイクデータから Cookie が取得できる。
  • この Cookie は Express が発行した SessionID を含む。

Express が express.session() でセッションを実現している場合、
これの実態は connect.session() で、セッション用の Cookie 名は、 "connect.sid" です。

// Server
var io = require('socket.io').listen(app);
// Connect から Cookie パーサを借りる
var parseCookie = require('connect').utils.parseCookie;

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    if(handshakeData.headers.cookie) {
      // handshakeData から cookie を取得
      var cookie = handshakeData.headers.cookie;
      // パースして express.sid を取得する。
      // これは Express のセッション用の Cookie
      var sessionID = parseCookie(cookie)['connect.sid'];
      // これを handshakeData に保存しておく
      handshakeData.sessionID =  sessionID;
    } else {
      return callback('Cookie が見つかりませんでした', false);
    }
    // 認証 OK
    callback(null, true);
  });
});

handshakeData に保存したデータは、 connect されている間はアクセスできます。

io.sockets.on('connection', function (socket) {
  console.log('sessionID ', socket.handshake.sessionID);
});

これで、 Socket.IO から Express のセッションを取得することが出来ました。
しかし、本当にやりたいのはただ取得するだけではありません。

次は Express がセッションデータを保存するストレージに、 Socket.IO からアクセスし、
セッションの更新等も含めた「セッションの共有」を実現しましょう。

セッションの共有

方針は

  • Express は express.session() でセッションを実現してる。
  • デフォルトでは、メモリに保存してる。
  • 実装は express.session.MemoryStore
  • この MemoryStore を Socket.IO 側から触れるようにする。


ストレージを作って Express に設定。

var MemoryStore = express.session.MemoryStore
  , sessionStore = new MemoryStore();

// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({ 
    secret: 'your secret here' 
  , store: sessionStore
  }));
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

Socket.IO が認証時にこれを見るようにする。

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    if(handshakeData.headers.cookie) {
      // handshakeData から cookie を取得
      var cookie = handshakeData.headers.cookie;
      // パースして express.sid を取得する。
      // これは Express のセッション用の Cookie
      var sessionID = parseCookie(cookie)['connect.sid'];
      // セッションをストレージから取得
      sessionStore.get(sessionID, function (err, session) {
        if (err) {
          // セッションが取得できなかったら
          callback(err.message, false);
        } else {
          // セッションデータを保存する
          handshakeData.session =  session;
          // 認証 OK
          callback(null, true);
        }
      });
    } else {
      return callback('Cookie が見つかりませんでした', false);
    }
  });
});

io.sockets.on('connection', function (socket) {
  // セッションのデータにアクセス
  console.log('session data', socket.handshake.session);
});

これで socket.handshake.session から Express のセッションデータにアクセスできます。

Socket.IO からセッションの更新

Express のセッションのデータに触れたので、それを Socket.IO 側から更新します。
Socket.IO で Express の Session オブジェクトを new できれば良いのですが、
このオブジェクトのコンストラクタは、 Session に紐づいたリクエストオブジェクトが必要です。
これは、 Express 側では簡単に取得できますが、 Socket.IO 側では今セッションデータしか持っていません。
しかし、ソースを読むと実際コンストラクタが内部で使用しているのは、

  • セッションID
  • セッションオブジェクト
  • セッションストレージオブジェクト

の三つです。

そこで、リクエストオブジェクトの代わりに、この三つを付与したデータで Session を new します。

// Server
var io = require('socket.io').listen(app);
// Connect から Cookie パーサを借りる
var connect = require('connect');
var parseCookie = connect.utils.parseCookie
  , Session = connect.middleware.session.Session;

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    if(handshakeData.headers.cookie) {
      // handshakeData から cookie を取得
      var cookie = handshakeData.headers.cookie;
      // パースして express.sid を取得する。
      // これは Express のセッション用の Cookie
      var sessionID = parseCookie(cookie)['connect.sid'];
      // 必要なデータを格納
      handshakeData.cookie = cookie;
      handshakeData.sessionID = sessionID;
      handshakeData.sessionStore = sessionStore;
      // セッションをストレージから取得
      sessionStore.get(sessionID, function (err, session) {
        if (err) {
          // セッションが取得できなかったら
          callback(err.message, false);
        } else {
          // req の代わりに handshakeData を入れる。
          // セッションオブジェクトを new する。
          handshakeData.session = new Session(handshakeData, session);
          // 認証 OK
          callback(null, true);
        }
      });
    } else {
      return callback('Cookie が見つかりませんでした', false);
    }
  });
});

io.sockets.on('connection', function (socket) {
  // セッションのデータにアクセス
  console.log('session data', socket.handshake.session);
});

セッション用の Cookie が Socket.IO の通信中にタイムアウトしないように、内部で定期的に更新します。

io.sockets.on('connection', function (socket) {
  var handshake = socket.handshake;
  // セッションのデータにアクセス
  console.log('sessionID is', handshake.sessionID);

  // 1 分ごとにセッションを更新するループ
  var intervalID = setInterval(function () {
    // 一度セッションを再読み込み
    handshake.session.reload(function() {
      // lastAccess と maxAge を更新
      handshake.session.touch().save();
    });
  }, 1000 * 60);

  socket.on('disconnect', function () {
    console.log('sessionID is', handshake.sessionID, 'disconnected');
    // セッションの更新を停止
    clearInterval(intervalID);
  });
});

これで、 Socket.IO の接続が長くても、最初に Express で発行したセッションの期限が切れることはありません。
ただし、ここで更新しているのはサーバ側のみで、クライアント側のクッキーは更新していないことに注意してください。

セッションストアを Redis に変更

現行の Express では、セッションストレージにデフォルトの MemoryStore を用いたまま本番(production モード)運用をした場合、警告メッセージがでます。
これは、 MemoryStore が本番運用を想定した作りになっていないためです。

$ NODE_ENV=production node app.js
Warning: connection.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and obviously only work within a single process.
Express server listening on port 3000 in production mode

セッションストアの実装は複数有りますが、この用途では Redis が使われることが多いようです。
Redis をインストールしていれば、 connect-redis を用いて簡単に置き換えることができます。

var MemoryStore = express.session.MemoryStore
  , sessionStore = new MemoryStore();

これでセッションストアが Redis になります。

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

JUS総会併設勉強会「『Node.js』とは何か。そして、その先へ。」でお話しさせていただきました。

JUS総会併設勉強会「『Node.js』とは何か。そして、その先へ。」 でお話しさせていただきました。
タイトルからして、なんというか
「そして、その先へって、どこなんだよ meso さん。。」
と、何を話すかかなり悩みましたがw

前半が bad_at_math さんによる恒例の低レイヤの深い話。
後半が自分の実際に node でのアプリ開発はどんな感じになるのかを express と socket.io を使った恒例の簡易チャットで紹介しました。その後に、一応「その先」っぽい話も入れました。


どうしても導入的な話になると、いつも通りお決まりの話になってしまうので、 Node をある程度知っている人から見たら物足りない内容かもしれませんが、今回の参加者の方は Node に触ったことがある人がほとんど居なかったようなので、良かったのかと思います。


今回は割と長丁場のライブコーディングだったので、失敗した場合に備えた完成版は用意していたのですが、案外すんなり動いてくれたので、大きく時間を押すことも無く無事に終われました。
(一応それなりの評価もいただけたようで、良かったです。)


資料は JUS の方に確認がとれたので下記に公開します。


最後にお世話になりました jus の法林さん、ありがとうございました。

Node で使える ECMA Script 5 の新機能

追記

11/9/24
Gistのリンクを本家Wikiに貼ってみました。
11/9/24
log 関数を修正しました。
11/7/10
JSON.stringify の第二引数 replacer について、補足しました。
11/7/14
os0x さんの指摘を反映しました。
  • String.trimRight、trimLeft は ECMA Script 5 非標準です。
  • JSON.stringify の第3引数には"\t"などの文字列も渡せます。
  • JSON.parse の第2引数 reviver について補足しました。
  • Array.prototype.forEach の第2引数 について補足しました。
  • "use strict" 時の Object.freeze 等の挙動について補足しました。
  • 「ECMA5 というのはちょっとおかしな略し方」について補足しました。
  • タイトルを修正しました。(旧「Node で使える ECMA5 の新機能」)

追記「ECMA5 というのはちょっとおかしな略し方」について

コメントにいただいたものを引用。

ところで、元記事からしてそうですが、ECMA5というのはちょっとおかしな略し方だと思います。
ECMAは欧州電子計算機工業会(European Computer Manufacturers Association)の頭文字で、その5という意味になってしまうかと。

まさしくその通りですね。しかし自分は普段 ECMA5 と言ってしまっていました。。
"ECMA Script 5" もし略すなら "ES5" が妥当ですね。
この記事はタイトルも含めて ECMA Script 5 に訂正しました。

本文

ECMA Script 5 で新しく実装されるいくつかの機能は、ブラウザでの実装がまばらなため、 CSJS では導入が難しいかもしれません。
しかし、 Node では、環境が一つなため、実装されているものは思う存分使うことができます。


今回は、Node の JS エンジンである V8 で実装されている ECMA Script 5 を紹介します。(自分でも一度使い方をまとめて書いておきたかった。)


内容はほぼ ECMA 5 Mozilla Features Implemented in V8 · joyent/node Wiki · GitHub の訳+補足になっていますが、順番はこの Wiki とは違います。
まず最初に一番重要(と個人的に思っている)な Object 関係の API を紹介し、
その後 Array, Date, String, JSON 等の API を解説します。


本文内のサンプルは、REPL で手軽に試せるものは REPL を、長くなるものはソースに書いて実行としています。全てのサンプルを(REPLにしてるのも含めて)一つのファイルにまとめたものを gist に上げました。自分のメモ用ですが、ご自由にどうぞ。

The sample usage of ECMA 5 Features Implemented in V8


バージョンは v0.5.0(unstable) と v0.4.9(stable) で確認してます。


ちなみに途中確認に使ってる log() は以下のようになっています。(console.log が長いってだけす)

var log = console.log;

Object

オブジェクトの定義方法が拡張されました。
まずオブジェクトリテラルには、 Setter, Getter を定義することができます。

var obj = {
  _str: 'default',
  get str() { // getter
    return this._str + '!';
  },
  set str(val) { // setter
    this._str = val.trim();
  }
};
log(obj.str); // "default!"
obj.str = ' asdf ';
log(obj.str); // "asdf!"

Property Descriptor Defaults

プロパティの定義には、プロパティディスクリプタを用いて属性の詳細を細かくコントロールできるようになります。
プロパティディスクリプタのメンバとそのデフォルトは以下の通り。

プロパティ 意味 デフォルト
value プロパティの値 undefined
get Getter undefined
set Setter undefined
writable 書き込み可能か false
enumerable 列挙可能か(for in で出るか) false
configurable プロパティディスクリプタを変更可能か false
Object.defineProperty(obj, prop, desc)

第一引数のオブジェクトに、第二引数のプロパティを、第三引数のプロパティディスクリプタを用いて定義します。

var obj = {};
Object.defineProperty(obj, 'num', {
  value: 10,
  writable: false,
  enumerable: false,
  configurable: false
});
log(obj.num); // 10
for (var i in obj) {
  log(i); // 出力されない
}
obj.num = 20;
log(obj.num); // 10 のまま
Object.defineProperties(obj, props)

第一引数のオブジェクトに新規プロパティを追加するか既存のプロパティを更新します。一度に複数のプロパティを操作することができます。

var obj = {};
Object.defineProperties(obj, {
  num: {
    value: 4,
    writable: false,
    enumerable: false,
    configurable: false
  },
  root: {
    get: function() {
      return Math.pow(this.num, 0.5);
    }
  }
});
log(obj.num); // 4
log(obj.root); // 2
Object.create(proto, props)

まず、第一引数をプロトタイプに持つオブジェクトを生成することができます。

// x, y を継承するオブジェクトを生成
var obj = Object.create({x: 10, y: 20});
log(obj.x); // 10
log(obj.y); // 20

この時、null を指定すると、何も継承しないオブジェクトを生成することもできます。
JS ではオブジェクトリテラル '{}' や 'new Object()' で生成したオブジェクトは、 Object.prototype を継承しているため、Object.create() に null を指定して生成したオブジェクトが最もピュア?(何も継承してない)なオブジェクトということになります。
こうしたオブジェクトは、Object.create() を使わないと、生成することができません。

var obj = Object.create(null);
log(obj); // 何も継承していない

var obj = Object.create(Object.prototype);
log(obj); // {} や new Object() と同じ

第二引数を指定することで、同時にプロパティを定義することもできます。

var obj = Object.create({x: 10, y: 20}, {
  z: {
    value: 30,
    writable: true
  },
  sum: {
    get: function() {
      return this.x + this.y + this.z;
    }
  }
});
log(obj.x); // 10
log(obj.y); // 20
log(obj.z); // 30
log(obj.sum); // 60
Object.keys(obj)

引数のオブジェクトの ownProperties かつ enumerable なプロパティのリストを取得します。

function o() {
  this.a = 1;
}
log(Object.keys(new o())); // [ 'a' ]
function p() {
  this.b = 2;
}
p.prototype = new o();
log(Object.keys(new p())); // [ 'b' ]

Object.create() を使ってるとこんな感じ。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.keys(obj)); // [ 'x' ]
Object.getOwnPropertyNames(obj)

引数に渡されたオブジェクトの ownProperty の名前の配列を返します。これには enumerable でないプロパティも含まれます。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.getOwnPropertyNames(obj)); // [ 'y', 'x' ]
Object.getPrototypeOf(obj)

第一引数に渡されたオブジェクトの prototype を返します。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.getPrototypeOf(obj)); // { a: 10, b: 20 }
Object.getOwnPropertyDescriptor(obj, property)

第一引数のオブジェクトが持つ、第二引数のプロパティのプロパティディスクリプタを取得します。

var obj = Object.create({a: 10, b: 20}, {
  x: {
    value: 30,
    enumerable: true
  },
  y: {
    value: 40,
    enumerable: false
  }
});
log(Object.getOwnPropertyDescriptor(obj, 'x'));
// { value: 30,
//   writable: false,
//   enumerable: true,
//   configurable: false }
Object.preventExtensions(obj)

引数のオブジェクトに新しいプロパティがこれ以上追加されることを防ぎます。

var obj = {a: 10, b: 20};
Object.preventExtensions(obj);
obj.x = 30; // TypeError: Can't add property x, object is not extensible
Object.isExtensible(obj)

引数のオブジェクトに対して、 Object.preventExtensions() が呼ばれているかを確認します。
呼ばれている場合は、拡張不可能なので、 false を返します。

var obj = {a: 10, b: 20};
log(Object.isExtensible(obj)); // true
Object.preventExtensions(obj);
log(Object.isExtensible(obj)); // false
Object.seal(obj)

引数のオブジェクトに対するプロパティの追加、削除、プロパティディスクリプタの変更を防ぎます。ただしプロパティの値は変更できます。

var obj = {a: 10, b: 20};
Object.seal(obj);
obj.a = 30;
log(obj.a); // 30
obj.x = 30; // TypeError: Can't add property x, object is not extensible
Object.isSealed(obj)

引数のオブジェクトに対して、 Object.seal() が呼ばれているかを判定します。

var obj = {a: 10, b: 20};
Object.seal(obj);
log(Object.isSealed(obj)); // true
Object.freeze(obj)

Object.seal() と同じですが、プロパティの値の変更も防ぎます。プロパティが完全に変更不可能になります。(変更しても反映されないだけで、エラーにはならないようです。)
通所は変更しても反映されないだけですが、 Strict Mode の場合はエラーになります。

var obj = {a: 10, b: 20};
Object.freeze(obj);
obj.a = 30; // (strict mode の場合) TypeError: Cannot assign to read only property 'a' of #<Object>
log(obj.a); // (strict mode じゃない場合) 10
Object.isFrozen(obj)

引数のオブジェクトに対して Object.freeze() が呼ばれているかを判定します。

var obj = {a: 10, b: 20};
Object.freeze(obj);
log(Object.isFrozen(obj)); // true

Object.prototype

isPrototypeOf(obj)

(ECMA 3, 5) このメソッドを呼び出されたオブジェクトが、引数に渡されたオブジェクトのプロトタイプだった場合、 true を返します。

var proto = {a: 10, b: 20};
var obj = Object.create(proto);
log(proto.isPrototypeOf(obj)); // true
非標準メソッド

Object.defineProperty() を用いて、既存のオブジェクトにもアクセサを設定することが出来ますが、ECMA Script 5 以前では、 __defineGetter__ 等を用いてこれらを行っていました。
Node でもこれらのメソッドが使用できます。しかし、メソッド名の通り非標準なので、使用には注意してください。紹介だけします。

  • __defineGetter__(name, callback)
  • __defineSetter__(name, callback)
  • __lookupGetter__(name)
  • __lookupSetter__(name)

Function.prototype

bind(thisArg[, arg1[, argN]])

まず関数内の 'this' に引数に取った thisArg を束縛します。

var f = function() {
  return this.a + this.b;
};
log(f()); // NaN
var g = f.bind({ a: 10, b: 20 });
log(g()); // 30


オプションで、第二引数以降に渡した値を、その関数の引数とすることができます。

var f = function(c) {
  return this.a + this.b + c;
};
log(f()); // NaN
var g = f.bind({ a: 10, b: 20 }, 30);
log(g()); // 60

これを用いると、カリー化が簡単に実現できます。

var f = function(c, d) {
  return this.a + this.b + c + d;
};
log(f()); // NaN
var g = f.bind({ a: 10, b: 20 }, 30);
log(g(40)); // 100

簡単に説明すると、引数を二つ取るもともとの関数 f(c, d) から、一つの引数を事前に割り当てた、引数を一つとる新しい関数 g(d) を作ったということ。
詳しくは「カリー化」とか「部分適用」なんかでググってください。

JSON

JSON.stringify(obj [, replacer [, space]])

引数に取る JSON を文字列にシリアライズします。

> JSON.stringify({ a: 1 });
'{"a":1}'

実は第二、第三引数もあります。
説明はこちらから引用させていただきます。 http://d.hatena.ne.jp/Syunpei/20090907/1252301929

replacerは省略可能で、function(key, value)と言うシグネチャの関数オブジェクトを渡します。JS→文字列の変換ルーチンを独自に提供できます。
spaceは、結果の文字列を人間が読みやすくするための、インデントの数を指定します。

replacer を自分で定義することは、まず無いでしょう。。
replacer も使いどころはあるようです。ごめんなさい。



@Constellation さんの gist からサンプルを引用させていただきます。(この記事のサンプルにも追記しました。)

log(JSON.stringify([1,2,3], function replacer(key, value) {
  if (!Array.isArray(value)) {
    return value;
  }
  var len = value.length;
  var result = { length: len };
  for (var i = 0; i < len; ++i) {
    result[i] = value[i];
  }
  return result;
}));
// {"0":1,"1":2,"2":3,"length":3}

他にも こんな使い方 もあるようです。


space は出力に数値で指定した深さのインデントを施します。数値だけではなく '\t' などの文字列も渡せます。例えば、以下のように使えます。

> log(JSON.stringify({ a: 1, b: 2}, null, 2));
{
  "a": 1,
  "b": 2
}
> log(JSON.stringify({ a: 1, b: 2}, null, 4));
{
    "a": 1,
    "b": 2
}
> log(JSON.stringify({ a: 1, b: 2}, null, '\t'));
{
        "a": 1,
        "b": 2
}
JSON.parse(string [, reviver])

JSON バリッドな文字列を JSON オブジェクトにパースします。

> var s = JSON.stringify({ a: 1, b: 0 });
> JSON.parse(s);
{ a: 1, b: 0 }

第二引数の reviver は JSON.stringify の replacer と同様、パースのロジックを自分で定義することができます。以下は、値が 0 か 1 だったらそれぞれを真偽値に変更する例です(こちら を参考にさせて頂きました)。

var s = JSON.stringify({ a: 1, b: 0 });
log(
JSON.parse(s, function(key, value) {
  if (value === 0)
    value = false;
  else if (value === 1) {
    value = true;
  }
  return value; // { a: true, b: false }
})
);

String.prototype

trim(), trimRight(), trimLeft()

文字列の左・右・両端からホワイトスペースを除去します。(ECMA Script 5 では trimRight, trimLeft は非標準です。)

> ' abc '.trim();
'abc'

> ' abc '.trimRight();
' abc'

> ' abc '.trimLeft();
' abc'

Array

Array.isArray(array)

対象のオブジェクトが配列かどうかを検査し、配列なら true を返します。

> Array.isArray([]);
true

Array.prototype

indexOf(value)

配列内に指定した値が有った場合、その最初の添字を返します。無い場合は -1 を返します。

> ([1, 2, 2, 3]).indexOf(2);
1
lastIndexOf(value)

配列内に指定した値が有った場合、その最後の添字を返します。無い場合は -1 を返します。

> ([1, 2, 2, 3]).lastIndexOf(2);
2
filter(callback)

全ての要素にコールバック関数を実行し、 true になる値からなる新しい配列を返します。

> ([1, 2, 3, 4]).filter(function(x) {
... return x > 2;
... });
[ 3, 4 ]
forEach(callback[, thisObject])

全ての要素に対してコールバック関数を実行します。

> ([1, 2, 3]).forEach(function(x) {
... log(x * 2);
... });
2
4
6


第二引数で、コールバック内での this の値を指定することができます。

> ([1, 2, 3]).forEach(function(x) {
... this.log(x * 2);
... }, console);
2
4
6
every(callback)

全ての要素にコールバック関数を実行し、全ての要素がそのコールバック関数の条件を満たしたら true を返します。

> ([1, 2, 3]).every(function(x) {
... return x > 0;
... });
true
> ([1, 2, 3]).every(function(x) {
... return x > 1;
... });
false
> 
some(callback)

全ての要素にコールバック関数を実行し、少なくとも一つがそのコールバック関数の条件を満たしたら true を返します。

> ([1, 2, 3]).some(function(x) {
... return x > 2;
... });
true
> ([1, 2, 3]).some(function(x) {
... return x > 3;
... });
false
map(callback)

全ての要素に対してコールバック関数を実行した結果からなる、新しい配列を返します。

> ([1, 2, 3]).map(function(x) {
... return x * 2;
... });
[ 2, 4, 6 ]
reduce(callback[, initialValue])

配列の先頭から順に二つの要素を取り出し、コールバック関数を適用して一つの値に集約して行きます。引数にはコールバック関数以外にも初期値を渡すことができます。

> (['a', 'b', 'c']).reduce(function(x, y) {
... return x + y;
... });
'abc'
> (['a', 'b', 'c']).reduce(function(x, y) {
... return x + y;
... }, '*');
'*abc'
reduceRight(callback[, initialValue])

配列の最後から順に二つの要素を取り出し、コールバック関数を適用して一つの値に集約して行きます。引数にはコールバック関数以外にも初期値を渡すことができます。

> (['a', 'b', 'c']).reduceRight(function(x, y) {
... return x + y;
... });
'cba'
> (['a', 'b', 'c']).reduceRight(function(x, y) {
... return x + y;
... }, '*');
'*cba'

Date

Date.now()

現在時刻を数値(Unix Time)で返します。

> Date.now();
1310205452263

Date.prototype

toISOString()
> (new Date()).toISOString();
'2011-06-30T23:01:48.320Z'

実装されてない機能

あとがき

ECMA Script 5 の機能が使えると便利な面が多くあります。ブラウザの互換性を気にせずに使えるのは JS 好きには結構嬉しいですね。
これらを使えば、コードの書き方はもちろん、ライブラリの設計方法等も変わっていくでしょう。そういうときの参考になればと思います。

また、現時点で ECMA Script 5 について最も詳細に記述されているのは、JavaScript The Definitive Guide 第6版 (サイ本6版)でしょう。
この本には、ブラウザで使う場合のことも書かれていて(例えば Object.create() を自分で実装するとか)、とても参考になります。



なにか間違いを見つけたら教えてください。

require('events').EventEmitter.call(this) の意味

[修正]
コメントで指摘されたように、回答4の訳が間違っていたので訂正しました。


Node の ML に以下のような質問が投稿されました。

What is the meaning of require('events').EventEmitter.call(this)

内容としては。


「以下のようなコードがあったんだけど、これってどういう意味?」

var util = require("util");
var events = require("events");

function MyStream() {
  // ここの意味がよくわからん、これは `new MyStream` と同じに見えるんだけど違うの?
  events.EventEmitter.call(this);
}

util.inherits(MyStream, events.EventEmitter);

var steam = new MyStream();


というもの。

これは結構良い質問で、 Node でプログラムをする時の大事なイディオムが詰まってるように感じました。
このスレッドにはいくつかの回答が上がったので、それらを紹介しながら、このイディオムを少し解説してみようと思います。


以下、まず ML でのやり取りの大雑把な訳(ちょいちょい省いてます)を載せて、その後にちょっと解説をします。

ML でのやりとり

投稿順ではなく、質問に対する回答の順になってます。

回答1

これは、 「MyStream のコンストラクタで、EventEmitter のコンストラクタを呼ぶ」 という意味。
util.inherits を一緒に使って、 MyStream は EventEmitter になる。
util.inherits については http://nodejs.org/docs/v0.4.8/api/util.html#util.inherits


回答1-2

それは JS のオブジェクト指向に共通するプラクティスだ。
もし自分のクラス内で親クラスのコンストラクタを呼ぶことにより、
自分のクラスを this として親クラスのプロパティを継承したりできる。

function A () { 
  console.log('a constructor'); 
  this.a = 'a' 
};

function B () { 
  A.call(this); 
  console.log('b constructor'); 
  this.b = 'b' 
};


var b = new B();
// => a constructor
// => b constructor
// b.a => 'a';
// b.b => 'b';
質問2

つまり `EventEmitter.call` は EventEmitter のコンストラクタだってこと?
これ(メソッド)はなにしているの?
MyStream 内で `EventEmitter.call` を呼ぶ目的は何?
その辺詳しく頼む。


回答2

あるオブジェクト上で EventEmitter.call は EventEmitter のインスタンスメソッド/プロパティのセットアップを行う(継承ではないよ)。


これは Java の Super() や C# の base(...) にあたるもの。

でも JS にはないので、自分でやらないといけない。


util.inherits については、これで MyStream が 「ある prototype を継承したオブジェクト」 を継承するようになり instanceof が動くようになる。
(ちなみに JS は多重継承はできない。)

new を実行することによって、もしインスタンスを生成しようとするコンストラクタ内に this があって、そこで EventEmitter.call を呼ぶと、
MyStream オブジェクト上で EventEmitter のコンストラクタが呼ばれたように、 (MyStream の this に対して) 処理が行われる。

回答3(izaacs)

補足すると、

JavasSript における "Class" は "prototype" プロパティを持つ単なる "関数" でしかない。

関数オブジェクトが持つ "call" メソッドは、引数に渡されたオブジェクトの "this-context" で関数を実行する。

fn.call(obj, 1, 2, 3)
// ↑は↓と同じ
obj.fn(1, 2, 3)

つまり、 `ParentClass.call(this, x, y, z)` (もしくはより一般的な ParentClass.apply(this, arguments)))
は、 "親クラスのコンストラクタを、あたかも `new ParentClass(x, y, z)` を実行したかのように呼び出す。
でも、新しくオブジェクトを作るのではなく、あくまで自分のオブジェクトに対して。" という意味。

unction Point2D (x, y) {
  this.x = x;
  this.y = y;
}

Point2D.prototype = {
  distance: function (p) {
    return Math.pow(Math.pow(p.x - this.x, 2) + Math.pow(p.y - this.y, 2), 0.5);
  }
};

function Point3D (x, y, z) {
  // 親クラス(Point2D)のコンストラクタを呼ぶ
  Point2D.call(this, x, y);
  this.z = z;
}

// 親クラスのプロトタイプを継承する
Point3D.prototype = Object.create(Point2D.prototype, {
  constructor: {
    value: Point3D
  }
});

// distance メソッドをオーバーライド
Point3D.prototype.distance = function (p) {
  return Math.pow(Math.pow(p.x - this.x, 2) + Math.pow(p.y - this.y,2) + Math.pow(p.z - this.z, 2), 0.5);
};
質問4

コンストラクタなんか呼ばなくても、 EventEmitter を(utill.inherit で)継承した時点で、
event はちゃんと動くんじゃないの?なんで call を呼ばないといけないの?

回答4(izaacs)(訂正)

今の実装では、実際は何もしない(つまりコンストラクタに機能が無い)。
でももしいずれ、何かの機能がEventEmitterのコンストラクタに実装された場合、
きっとそれを使いたくなるよね。(だから call() しておく方がいい。)
だから、もし必要なかったとしても、これは良い習慣なんだ。

質問5

なるほどわかった!


解説

本当は質問が続くけど、スレッドは質問5のところでストップしています。
とりあえず所々補足できそうなところを解説します。

質問内容

そもそも最初の質問に出てきているソースは、

http://nodejs.jp/nodejs.org_ja/api/util.html#util.inherits

にあるソース。

var util = require("util");
var events = require("events");

function MyStream() {
    events.EventEmitter.call(this);
}

util.inherits(MyStream, events.EventEmitter);

MyStream.prototype.write = function(data) {
    this.emit("data", data);
}

var stream = new MyStream();

console.log(stream instanceof events.EventEmitter); // true
console.log(MyStream.super_ === events.EventEmitter); // true

stream.on("data", function(data) {
    console.log('Received data: "' + data + '"');
})
stream.write("It works!"); // Received data: "It works!"


このソースの中にある EventEmitter の継承について彼は質問している。

require('event').EventEmitter

EventEmitter とは、 Node でのイベント駆動プログラムの根幹をなす重要なオブジェクトで、
自分で作ったオブジェクトがこれを継承することで、イベントを用いた操作、つまり ``on()`` や ``emit()`` が使えるようになる。

その際に、この EventEmitter を継承する際によく行われるのが以下のイディオム。

function MyStream() {
  events.EventEmitter.call(this);
}
util.inherits(MyStream, events.EventEmitter);

これで自作の MyStream から生成したインスタンスは、 EventEmitter を継承する。

call(this)

ここが、質問の中心になっていた部分。
EventEmitter.call(this) で自分自身を this として EventEmitter のコンストラクタを実行する。このへんは上の回答にある通り。 JavaScript の共通イディオム。

util.inherits()

説明としては、 http://nodejs.jp/nodejs.org_ja/api/util.html#util.inherits にある通りで、 util モジュールにある通り便利メソッドなんだけど、実装を見ると実は結構短い。

こんだけ。コメントは長いのではしょった。ソースはこちら

/**
 * Inherit the prototype methods from one constructor into another.
 *
 * (略)
 *
 * @param {function} ctor Constructor function which needs to inherit the
 *     prototype.
 * @param {function} superCtor Constructor function to inherit prototype from.
 */
exports.inherits = function(ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
};

つまり先ほどのこれは、

// events.EventEmitter は EventEmitter に書き換えると。
util.inherits(MyStream, EventEmitter);

MyStream.super_ というプロパティに(あとで便利なように) EventEmitter を加え、
MyStream.prototype に EventEmitter をプロトタイプとするオブジェクトを加えている。
つまり EventEmitter を継承したオブジェクトを MyStream のプロトタイプにしている。
(なかなか言葉にしづらい。中間オブジェクトがあるイメージ。)

次に Object.create() と constructor プロパティも補足しておく。

Object.create()

Object.create() は ECMA5 のメソッドで第一引数に受け取ったオブジェクトをプロトタイプに持つオブジェクトを生成します。
シミュレートすると以下のような感じ。

Object.create = function(p) {
  function f();
  f.prototype = p;
  return new f();
}

しかし、 Object.create() は null を受け取る事もできるため、「なにも継承しない」オブジェクトも作る事が可能です。
これは上のようにシミュレートすることはできません。

constructor プロパティ
ctor.prototype = Object.create(superCtor.prototype, {
  constructor: {
    value: ctor,
    enumerable: false,
    writable: true,
    configurable: true
  }
});

さっきのこれは、 superCtor.prototype をプロトタイプにしながら、さらに一つ constructor というプロパティをセットしている。

constructor プロパティを指定しないと、 Object.create() したオブジェクトの constructor プロパティは、第一引数に渡したコンストラクタになる。
それを防ぐために、明示的に constructor プロパティをセットしている。ここでは ECMA5 のプロパティディスクリプタによって、プロパティの細かい属性も指定されていて、

value
プロパティの値=ctor
enumerable
列挙(for ~ in でイテレート)可能か=false
writable
値を更新可能か=true
configurable
Property Descripter を変更可能か=true

とそれぞれ指定されている。
プロパティディスクリプタについて、詳しくはこの辺が参考になる。 ECMAScript 5の"Object" - JavaScriptで遊ぶよ - g:javascript


つまり、このオブジェクトは ctor をコンストラクタとして生成されたことにしている。
という感じ。

まとめ

最後に、そもそもの質問に立ち返ると

var util = require("util");
var events = require("events");

function MyStream() {
  // ここの意味がよくわからん、これは `new MyStream` と同じに見えるんだけど違うの?
  events.EventEmitter.call(this);
}

util.inherits(MyStream, events.EventEmitter);

var steam = new MyStream();


これで、 MyStream() から生成したオブジェクトは MyStream を this として EventEmitter のコンストラクタを実行して各初期化を行い、events.EventEmitter がプロトタイプチェーンにあるから、メソッドやプロパティが参照できる。
つまり MyStream() を EventEmitter を継承させたということでした。


これは自分でイベントを必要とするオブジェクトを作る際に使うイディオムなので、覚えておくと参考になると思います。

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 を用いたスケールアウト構成についてです。

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