node.jsでServer-Sent Eventsを試す

HTML5のServer-Sent Eventsの仕様は、HTTP通信を使ってサーバサイドからのデータプッシュを行うCometを、JavaScriptAPIを定義するなどして使いやすくまとめたものらしい。クライアント側のAPIはSafari5やChrome8では既に実装されているそうなので試してみた。

サーバ側は最近流行のnode.jsで実装してみた。Server-Sent Eventsのサーバ側はCGIやPHPでも実装可能だが、Apacheのようなマルチスレッドでリクエストを処理するタイプのサーバでは、ロングポーリングで停止しているだけの、ほとんど仕事しないスレッドが沢山作られてリソースが無駄になり非効率的だ。ノンブロッキングIOでシングルスレッドでイベント駆動型のnode.jsならば効率的なはず。

サンプル。gitでソースを落として node server.js して http://localhost:8124/ に接続

$ git clone git://github.com/paulownia/sse-sample.git
$ cd sse
$ node server.js

クライアントサイドのコードは非常に簡単。

var source = new EventSource(url);
source.onmessage = function(event) {
  // データ受信時の処理を書く
  // event.data でサーバから送られてきたデータを取得できる
}

次はサーバサイド。イベント処理を行うgeneratorというモジュールを作成。独自のイベント処理を行うにはEventEmitterというnode.jsの組み込みオブジェクトを拡張すればいい。exportsとrequireを使ったモジュールの定義と呼び出し方法はCommonJSという仕様で決まっているらしい。

// EventEmitterを取得
var events = require("events");
var emitter = new events.EventEmitter();

// EventEmitterを拡張
emitter.id = 0;
emitter.data = "";
emitter.start = function() {
    var chars = "node.js";
    var self = this;
    setInterval(function() {
        var pos = Math.floor(Math.random() * chars.length * 2); 
        if (pos < chars.length) {
            self.id++;
            self.data = chars.charAt(pos);
            // generatedイベントとして登録されたリスナを呼び出す
            self.emit("generated", self.id, self.data);  
        }
    }, 1000);
};

// module.exportsに設定されたオブジェクトがrequireの戻り値となる。
module.exports = emitter; 

サーバはリクエストを受け付けてもレスポンスをすぐに返さずに、generatorにイベントリスナを登録して終了。イベントリスナでレスポンス処理を行う。これでgeneratorのイベントが発生するまでロングポーリングされるようになる。

var http = require("http")
http.createServer(handler).listen(8124);

var generator = require("./generator")
generator.start();

function handler(req, res) {

    function response(id, data) {
        generator.removeListener("generated", arguments.callee);
        
        res.writeHead(200, {
            "Content-type": "text/event-stream"
        });
        res.write("id:" + id + "\n");
        res.write("data:" + data + "\n");
        res.write("retry: 1000");
        res.end();
        
    }
    generator.on("generated", response);
}

というわけでnode.jsならばServer Sent Eventsのサーバサイドの実装も簡単だ。