Block Rockin’ Codes

back with another one of those block rockin' codes

WebSocket サーバの実装とプロトコル解説

intro

なんだかんだ WebSocket を使ってるのに、 WebSocket サーバを自分で書いたことが無かったので、RFC も落ち着いてきたここらで、仕様を読みながら実装してみようと思いました。


"WebSocket サーバ 実装" とかでググると、 Socket.IO とか pywebsocket で WebSocket アプリ作って、「WebSocket サーバを実装」みたいなタイトルになってることが多いみたいですが、
(ApachePHP で HelloWorld して、「HTTP サーバ実装しました」とは言わないよね。)


この記事では、 WebSocket プロトコルをしゃべるサーバ自体を実装します。
といっても、全部やるのはちょっと大変だったので、基本的なテキストメッセージのやりとりの部分だけやって、エコーサーバができるところまでやりました。


完成版のソースは以下です。本文を読みながら、合わせて見ていただけると良いと思います。

WebSocket Server Sample Impliment · GitHub

仕様

今回実装するのは、IETF RFC6455 に準拠したサーバと思ったのですが、全部やるのは大変だったので、サーバとクライアントでそれぞれ単純な
テキストをやり取りできるところまでにします。

RFC 6455 - The WebSocket Protocol

ちなみに、 API の方は W3C になります。

The WebSocket API


具体的には C->S, S->C それぞれ 'test' という文字列をやり取りします。

// Client
var ws = new WebSocket("ws://localhost:3000/", ["test", "chat"]);
ws.onopen = function() {
  ws.send("test");
  ws.onmessage = function(message) {
    console.log(message.data); // test
  };
}

使ったのは以下

(FireFox でも動いたみたい)

HTTP Server

まず、今回は HTTP サーバで配信した HTML に含まれる JS から、同じサーバの上にある WebSocket サーバにコネクションを要求、確立する
感じにします。


そのため、まずは Node.js で HTTP サーバを立て、HTML を配信します。

var clientScript = function () {
  var ws = new WebSocket("ws://localhost:3000/", ["test", "chat"]);
  // var ws = new WebSocket("ws://localhost:3000/", "test");
  ws.onopen = function() {
    console.log(ws);
    ws.send("test");
    ws.onmessage = function(message) {
      console.log(message.data);
    };
  }
}

var server = http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = '<html><head><title>wsserver</title>' +
  '<script type="text/javascript">' +
    '(' + clientScript + ')();' +
  '</script>' +
  '</head>' +
  '<body>hello world</body>' +
  '<html>';
  res.end(html);
});

シンプルな HTML の中に、生の WebSocket を使う JS を埋めています。


ブラウザで WebSocket オブジェクトを new すると、引数の WebSocket サーバに接続しにいきます。


この時、ブラウザは HTTP1.1 で upgrade リクエストを投げます。
そこで WebSocket を指定することで、通信プロトコルを WebSocket に upgrade する要求をができるので、サーバはこのリクエストを受け取り、ヘッダを解析します。

HTTP Upgrade Header

upgrade リクエストのヘッダは以下のようなものです。
1.2. Protocol Overview


また、 HTTP のヘッダ周りはこちらを見ると参考になると思います。
studyinghttp.net - 

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Host
接続先の HOST です。
Upgrade
HTTP1.1 を別のプロトコルへ切り替える要求です。
Connection
「HTTP/1.1 では、"Upgrade が HTTP/1.1 メッセージに存在する時は常に、upgrade というキーワードを Connection ヘッダフィールド (section 14.10) の中に与えなければならない" とされている事に注意せよ。」だそうです([http://www.studyinghttp.net/security#SwitchingProtocolFromHTTP:title=link)
Sec-Websocket-Key
クライアントの認証や、セキュリティのためにクライアントで生成されるキーです。
Origin
接続を要求するオリジンを示します。
Sec-WebSocket-Protocol
サブプロトコルのリストです。
Sec-WebSocket-Version
プロトコルのバージョンです。最新は13。


で、これらの解析なんですが、実は Node.js の場合は先程立てた HTTP サーバが勝手に解析してくれます。(なんというチートw)


サーバに Upgrade リクエストが来ると、Server オブジェクトで upgrade イベントが発生し、コールバックの第一引数に解析されたヘッダがオブジェクトとして渡されます。

server.on('upgrade', function(req, socket, head) {
  console.log(req);
  //  { host: 'localhost:3000',
  //  upgrade: 'websocket',
  //  connection: 'Upgrade',
  //  origin: 'http://localhost:3000',
  //  'sec-websocket-protocol': 'test, chat',
  //  'sec-websocket-extensions': 'x-webkit-deflate-frame',
  //  'sec-websocket-key': 'NblXHeIwGDpoQ2GFAGzwzw==',
  //  'sec-websocket-version': '13' }
});


現在、 Chrome では Sec-WebSocket-Extensions ヘッダも送られています。
このへんかな

が、これはあくまで拡張なので、今回は無視します。


gist に貼ったソースでは、このあと、わかってる範囲で古いヘッダでの条件分岐などを書いていますが、最新のプロトコルのヘッダだけに対応するならこの時点で半分は終わりですw

HTTP Upgrade Response Header

リクエストからレスポンスを生成します。
レスポンスヘッダはこのようになります。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

レスポンスは HTTP1.1 Switching Protocol を返すことで、upgrade が受け入れられたことを示します。


この時、 Sec-WebSocket-Accept フィールドは、リクエストにあった、 Sec-WebSocket-Key の値から以下のように算出された値を使用します。

  1. Sec-WebSocket-Key(key) の末尾の空白を覗いた値を準備
  2. key に固定値 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" を連結
  3. sha1 を取得
  4. base64 に変換

です。


Node.js では標準の Crypto モジュールを使います。

key = require('crypto')
       .createHash('sha1')
       .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
       .digest('base64');

Sec-WebSocket-Protocol は、クライアントから提示されたサブプロトコルからどれか(もしくは無し)を選んで返します。
そもそもサブプロトコルはオプションで利用できるアプリケーションレイアのプロトコルなので、今回はネゴシエーションしてるけど、使いません。

Data Frame(C->S)

ヘッダを返したら、 WebSocket 通信が確立します。


まずは、クライアントからメッセージを送ってみましょう。

ws.send('test');

送られるメッセージは 4byte の ASCII 文字列です。


この文字列はバイナリ形式のデータフレームでサーバに送られます。

Node.js の場合は、メッセージが届くと 'data' イベントが Socket オブジェクトで発生し、コールバックに Buffer オブジェクトでこのデータが渡ります。


Buffer オブジェクトは、 Node.js でオクテットストリーム(要するにバイナリデータを 8bit ごとの配列っぽく)扱うためのオブジェクトです。


このフレームの形式は以下のようになっています。


RFC 6455 - The WebSocket Protocol

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+


では、 Buffer の中身をコツコツ解析しながら、各値を解説していきます。

FIN

最後のパケットなら 1, 続くなら 0
今回は 1 ('test' というデータは一回で送れるから)

var firstByte = receivedData[0];
/**
 * fin
 * axxx xxxx first byte
 * 1000 0000 mask with 0x80 >>> 7
 * ---------
 * 1         is final frame
 * 0         is continue after this frame
 */
var fin = (firstByte & 0x80) >>> 7;
RSV1-3

extention を使わないなら0
今回は拡張を使わないので無視。

opcode

Payload Data の説明
Payload Data とはヘッダ等じゃない実データ(ここでは 'test')

  • %x0:continuation frame
  • %x1:text frame
  • %x2:binary frame
  • %x3-7:reserved for further
  • %x8:connection close
  • %x9:ping
  • %xA:pong
  • %xB-F:reserved for further

今回は 0x1

/**
 * opcode
 * xxxx aaaa first byte
 * 0000 1111 mask with 0x0f
 * ---------
 * 0000 0001 is text frame
 */
var opcode = firstByte & 0x0f;
MASK

1 ならPayload Data がマスクされている。されていなければ 0。

Payload はブラウザが送るときは "必ずマスクする"
サーバが送るときは "絶対にマスクしない"
という決まりがある。

今はブラウザからだから 1

var secondByte = receivedData[1];

/**
 * mask
 * axxx xxxx second byte
 * 1000 0000 mask with 0x80
 * ---------
 * 1000 0000 is masked
 * 0000 0000 is not masked
 */
var mask = (secondByte & 0x80) >>> 7;
if (mask === 0) {
  assert.fail('browse should always mask the payload data');
}
Payload Length

Payload Data の長さ
最初の 7 bit を読んだとき

  • 0-125:そのままそれが Payload の長さ
  • 126:それより長いから、後続の 16bit が UInt16 として Payload の長さを表す
  • 127:それよりも長いから、後続の 64bit が UInt64 として Payload の長さを表す

ここでは、処理するデータは 'test' と決め打ちにして、 7bit だけ読む実装にした。
だから、 Pyload Length = 4 になる。

/**
 * Payload Length
 * xaaa aaaa second byte
 * 0111 1111 mask with 0x7f
 * ---------
 * 0000 0100 4(4)
 * 0111 1110 126(next UInt16)
 * 0111 1111 127(next UInt64)
 */
var payloadLength = (secondByte & 0x7f);
if (payloadLength === 0x7e) {
  assert.fail('next 16bit is length but not supported');
}
if (payloadLength === 0x7f) {
  assert.fail('next 64bit is length but not supported');
}
Masking Key

Payload をマスクしているキーデータ


Payload Length の後に続く、 32bit のデータがこれになる。
MASK=1 だった場合は必ず付与される、後で Payload を複合するのに使う。

/**
 * masking key
 * 3rd to 6th byte
 * (total 32bit)
 */
var maskingKey = receivedData.readUInt32BE(2);
Payload Data

実データの部分、実際は Extention Data + Application Data


以降、末尾までのデータが Payload Data になっている。
しかし、実際これは Extention Data + Application Data となっている。


Extention Data とは、ネゴシエーションの過程で、 Extention を使うと決めた
場合に付与されるデータだから、今回は使わない。(というか Payload に含まれない)


Application Data は今回で言う 'test' のこと。
だから、 Payload Data == Application Data と考えていい。


Payload Length が 4byte とわかっているため、
32bit を Big Endian で読んであげればいい(TCP=BigEndian だからだと思ってるけどあってるのかな?)

var applicationData = receivedData.readUInt32BE(6);


読みだしたデータを、先ほどの Masking Key で unmask する。
(Masking Key との XOR をとってあげればいい)

var unmasked = applicationData ^ maskingKey;


この値を UTF-8エンコードしてあげればいいんだけど、
今の時点では Buffer じゃないから、一旦バッファにしてから、
Buffer.toString() をした。 (もっといい方法あるかも)
Buffer Node.js v0.8.26 Manual & Documentation

var unmaskedBuf = new Buffer(4);
unmaskedBuf.writeInt32BE(unmasked, 0);

var encoded = unmaskedBuf.toString();
console.log(encoded)' // test


これで無事、クライアントが投げた値が取り出せました。

Data Frame(S->C)

次はクライアントにデータ 'test' を送ります。
といっても、さっきの逆をやればいいだけです。


大きな違いは、

「サーバからクライアントに送る場合は、マスクしない」

という点です。


各値は以下のようになります。

  • FIN:1
  • OPCODE: 1
  • MASK: 0
  • Payload Length: 4
  • Payload: 'test'

変換を手を抜いて、普通に書き込んだらこんな感じ。
一旦 Buffer オブジェクトに貯めて、 socket.end() に渡すと、クライアントに送ってくれます。
String からは charCodeAt() が使えます。

/**
 * Sending data to client
 * data must not mask
 */
var sendData = new Buffer(6);

// FIN:1, opcode:1
// 0x81 = 10000001
sendData[0] = 0x81;
// MASK:0, len:4
// 0x4 = 100
sendData[1] = 0x4;

// payload data
// send data "test"
sendData[2] = 'test'.charCodeAt(0);
sendData[3] = 'test'.charCodeAt(1);
sendData[4] = 'test'.charCodeAt(2);
sendData[5] = 'test'.charCodeAt(3);

// send to client
socket.end(sendData);

うまくいけば、クライアントでは onmessage イベントが発生し、data フィールドに送ったデータが格納されます。

ws.onmessage = function(message) {
  console.log(message.data); // test
};

まとめ

WebSocket の仕様自体は、全部実装しようとするとちょっと大変だけど、今はまだ読もうと思えば読める程度の量だと思います。


より大きな問題は、ここまでに生まれては消えたプロトコルの実装を持った、古いブラウザとの互換性でしょう。そこを考えなければ、頑張れば実装できなくはないかも。


しかし、現在の RFC にしても、まだまだ仕様が変わる可能性は、無くはないし、途中で出てきた Extention や Subprotocol, また別で進んでる multiplexing などによって、今後もう少し変わる可能性があります。
WebSocket をアプリで使う場合は、おとなしくメンテナンスされている実装を使うのが吉ですが、こうして自分でちょっとでも実装すると得られるものも多いのでお勧めです。


JS はもともとバイナリを処理する文化が無かったため、Node.js は独自に Buffer モジュールを作っていますが、JavaScript には TypedArray という仕様が最近出てきているので、いずれ Node もそっちを使うことになると思います。



もしかしたら、仕様の読み違いなどあるかもしれません、指摘、質問などフィードバック歓迎です。

Trigger と Stream ベースの Reactive スタイルについて考える

intro

先日 Meteor について調べて発表するにあたり色々調べたり、そのあと何人かの方々とお話させて頂いた中で、
思うところが出てきたので、アウトプットしたいと思います。


Meteor について発表してきました。 - Block Rockin’ Codes


Meteor 以前からも思っていたところもありますが、ちょうどいいので Meteor も対比に出しながら書いてみます。

Reactive Programming

@ さんも書いてたように、やはり一番大きいのはここだと思います。


Meteor.js - naoyaのはてなダイアリー


簡単に説明すると、

よく Excel に例えられますが、あるセルに他のセルの値を用いた計算式を埋めておくと、
その参照先のデータが変わった時に、式を埋め込んだセルの値も連動して変わるようなあれです。


プログラミングで見るなら、
例えば以下の場合、 b を出した後、
a の値を変えても、 b の値は変わりませんが、、

var a = 1;
var b = a + 1;

a = 2
console.log(b); // 2

リアクティブプログラミング的には
a が変わると、連動して b も変わる感じ。

var a = 1;
var b = a + 1;

a = 2
console.log(b); // 3

になるという感じです。


Reactive スタイルな Web

"Reactive なプログラミング" 自体は、先ほどの説明の通り「言語自体のサポート」無しでは書けません。
(先のようなコードは、 JS では動かないという意味)


しかし、このアイデアは「データソースに変更があると、 View に反映される」という置き換えを行うことで、
Web アプリ全体のアーキテクチャとして持ち込むことができます。


Meteor の場合は、バックにある DB 内のデータと、フロントの View に埋め込まれた変数を bind しておくと、
DB の変更が WebSocket で通知されたタイミングで、bind 先の View が更新されるようになっています。


こうした動作を Sock.js という Socket.IO みたいなモジュールを使って WebSocket(など) を用いた形で行なっています。
言語自体が Reactive な動作(?) をサポートしているわけではありませんが、全体のアーキテクチャが Reactive なスタイルというイメージです。


Meteor には多くの機能がありますが、自分が一番良いと思ったのは、この Reactive スタイルなアプリ開発を、
うまいこと抽象化して、それなりに簡単に実現できるようにしているところです。

なぜ Reactive か?


「リアルタイム Web」とくくられるアプリケーションに共通しているのは、
おそらく「サーバの変化を、素早く View に持ってきている」パターンだと思います。
チャットなども、 PtoP で無い限り、サーバを介しているので同じように考えられます。


そして、サーバの変化をフロントに持ってくるには、

  1. サーバの変化のイベントを取得
  2. そのイベントでリモートにイベント(やデータ)を Push
  3. クライアントはイベントを受け取ったら View を更新
  4. これら全体のハンドリング

というような形になります。


これを、先ほどの Reactive なスタイルを適応すると、とても素直に実現できます。


ちなみに Meteor では、それぞれ

  1. Pub/Sub をベースとしたイベント通知
  2. Socke.js を用いた Push
  3. liveui を用いた bind による更新
  4. ReactiveContext


を用いて実現しているようです。


ちょっとこれと照らし合わせて詳しく見てみます。

1.DB トリガーの再発見(かどうかはわからないけど)


発表でも言ったんですが、
最近は、 "DB トリガー" があれば、このアーキテクチャの 1 の部分がとてもすっきり書き直せるんじゃないかと思います。


トリガーは、例えばインサートが走った時に、そのクエリの終了時にイベントを発生し、
登録しておいた処理を実行するような機能です。


もし DB がトリガーをサポートしていなかったら、DB へのインサートをフックしたい場合、
プログラム側でイベントを生成する必要が出るでしょう。


変更を取得するために、 DB をポーリングする必要があるかもしれません。
Meteor では、インサートと同時に Pub/Sub でそれを通知しているようです。


こうした処理を、自前でやるのは、イベントの生成、例外処理、場合によっては MQ なども絡んできて割と面倒です。
ポーリングはパフォーマンスの問題も出ます。


しかし、トリガーがあると、インサートが(成功/失敗問わず)完了したことを、DB からのイベントとして取得することができます。
プログラムはトリガーからのイベントを待つだけで、面倒なところの大半をトリガーがやってくれるので、非常に見通しがよくなると思います。


しかし、リアルタイム性の高いアプリに DB の Trigger を持ち込むと、 Trigger 自体のパフォーマンスがかなり重要になってきます。
これらを踏まえた上で、今現実的に「使える」 Trigger を持つ DB がどのくらいあるのかは、自分は把握できていないんですが(多分 Oracle はいけるけど、お高い。。)
今後 DB 自体にこうした機能があるととても嬉しい場面は多いと思います。


感覚的にあまり取り上げられることが少ないように思うこの Trigger という機能が、今こそもっと見直されるべきだと思います。
そして、このままだと、リアルタイム Web のバックとしては、必須の機能になるんじゃないかと、個人的には思います。


そのうち、各 DB のトリガーのパフォーマンスも調べてみたいところです。
(ちなみにブラウザの localStrage なんかも change イベントを発生するんですが、これは別のタブに通知するためらしい。。なぜ。。)


2.通信手段

「リアルタイム Web」などと言われている文脈の中で、WebSocket なんかが一緒に盛り上がってる感じもありますが、
結局 WebSocket でリアルタイム感が出ているのは「サーバが Push をすることができる」という点が重要で、


つまり本当は WS でなくてもできます。ただ、 WS でやったほうが、効率がいい場合が多いということになります。
というか、 WS はそのためのプロトコルだし、これ以外にも ServerSentEvent や SPDY なんかもこれを実現するために使えるでしょう。
すべてを生 WebSocket でやる必要は無いと思います。


純粋な通信以外(認証、ネームスペース etc) はいらないなら、 Socket.IO じゃなくてもいい。


ここは完全に、通信量やパフォーマンスなどで使い分ける必要があると思います。

3.bind

View にバインドするパターンは MVVM 由来でいくつかあるらしいです。
knockout.js や Derby.js あたりがそうみたいだけど、あまり詳しくないのでわからない。

Reactive と Stream


やさしいFunctional reactive programming(概要編) - maoeのブログ から引用すると

Wikidediaから抜粋するとFRPにおいて重要なポイントは次の通りです。

入力はbehavior(振る舞いとかビヘイビア)か時間で変化するイベントストリームとして見える
時間にともない連続的(continuous)に変化する値を扱える(これがbehavior)
時間順に並ぶ離散的(discrete)なevent
時間にともない変化する値は、higer orderかもしれない(要するに普通の値じゃないかもしれない)


その下に behavior と event のわかりやすい説明があります。
ちょっと多いですが、まるっと引用させて頂きます。

behaviorの一番簡単な例は時間です。ある値tが時間を表すbehaviorだとすると、値tは時とともに連続的に変化します。
tを出力するとある時点の時刻が(サンプリングされて)表示されますが、内部的には連続的に変化しています。
他にもユーザが動かしているマウスの座標だったり、日照によって変化するボンネットの温度や、振り子運動しているブランコの速度、
刻々と変化する株価など、behaviorは実にありふれています。

一方で、eventとは時間と値が組になったストリームです。たとえばユーザからのキー入力を表すkeyという値を考えます。
keyはユーザがキーを押した時刻tと、どのキーかを表すcを組にした(t, c)という値のストリームを表しています。
他にもユーザがマウスをクリックするごとに発生するeventや、ボンネットの温度が5度あがるごとに発生するevent、
ブランコの速度が0になるたびに発生するevent、あるいは株式市場で取引の開始・終了を知らせるeventなど、eventもまたありふれています。

さて、これってどこかで聞いたことないでしょうか?

実は、 Node における Stream の概念が、まさしくこれらを抽象化していると見ることができます。


Node の Stream については、かつて以下に書きましたのでご参照下さい。


Node.js の Stream API で「データの流れ」を扱う方法 - Block Rockin’ Codes


簡単に説明すると、 Stream は「時間」と「値」を「データの流れ」として抽象化するためのインタフェース。というような位置づけになります。
連続して変化するデータを、イベントと合わせて扱うことができます。


つまり、 Node の Stream は、Reactive スタイルのアプリケーション開発と非常に相性がよさそうです。


例えば、 DB の変更を Stream として読み込み、データが変更されるたび(data イベントが起こるたびに) WebSocket でそれをフロントに送り、
フロントは WebSocket でデータを受け取るたびに、それを View に反映するといった感じ。


この場合は DB を ReadableStream として抽象化して、 Client (の View) を WritableStream として抽象化したものを、
pipe() で繋いだら完成です。


つまり

ServerReadableStream.pipe(ClientWritableStream);


です。逆も同じ。
つまりアーキテクチャ全体を Stream で抽象化できます。


で、これを 「Stream ベースの Web」として考えたのが、先の Stream の記事の次に書いた、下記の記事です。


Stream.IO というものを作ってます。 - Block Rockin’ Codes


このアイデアから生まれたのが Jxck/stream.io · GitHub です。
ということで、今まで考えてきたことが繋がったなぁと(汗


以上の話で、

  1. DB がイベントを生成するための Trigger
  2. イベント(とデータ) をリモートとやりとりするための WS などの通信手段
  3. View にデータを bind し更新するためのなんらかのエンジン
  4. それらを抽象化、ハンドリング(pipe) するための Stream


があれば、 Meteor とは違った、コンパクトなリアクティブプログラミングフレームワークになりそうです。
でも、3. は knockout.js とか delby.js あたりを使えば良さそうだし、 Stream.IO の責務ではないかな。



実は Stream.IO は、まだブラウザで node の Stream を動かすために EventEmitter, Stream, Assert などを移植して
力尽き 方向性を見失っていたんですが、色々着想が得られたので、もう少し頑張ってみようかなと思います。

outro

そんなことを GW 中考えていました。


リアクティブと合わせてトリガーにちょっと注目して行きたいです。


いつもどおり、ご指摘があればお願いします。

Meteor について発表してきました。

追記

12/5/9
meteorについての感想をすこしだけ追記

introduction

もう結構経ってしまいましたが、 東京Node学園 5時限目 - connpass
Meteor について発表してきました。

slide

スライドは以下です。今回はどうせ寿命の短いスライドだし、面倒なので
PDF -> SlideShare はしていません。(気が向いたらやるかも。)


Node Academy 5 | "shallow inside meteor"


金曜に決まって、急遽土日でひと通り調べて、とにかくわかったことを全部盛り込んだ感じなので、
まとまりは無いです。。
たぶんスライド見ただけではあまり伝わらないかもしれません。
発表は UST の録画があるので、合わせてみていただける方がいいかも。


今回は 10 分でスライド 40 枚だったので、 15 sec/page でやるために、
スライドのめくりを遠隔から忍者さんにお願いしたんですが、
結局自分の喋りたいように喋ってしまい、なんかホントごめんなさいごめんなさい。。

meteor について


で、この記事を書こうと思ってた頃、 naoya さんから先に記事が上がりました。


Meteor.js - naoyaのはてなダイアリー


もうなんか、俺が説明することは何もないです(汗


ざっくり Meteor を読んでみた感想だけ言うなら、

「コンセプトや実装から、学ぶことは多い。でも使うにはちょっと怖い。」


が正直な感想です。


Meteor の良い部分は、この Reactive スタイルなアプリ開発を、 うまいこと抽象化して、それなりに簡単に実現できるようにしていることだと思います。


それ以外にも、とても沢山の機能を Meteor は持っていますが、 モジュラリティが低く、扱いや見通しが悪い感じが否めないなと思うのが、 現時点での Meteor の正直なところです。


最近の Node 周りのプロダクトでは、 例えば (https://github.com/flatiron) なんかだと、 Flatiron で Github のアカウントを作り、 その下に Flatiron の依存モジュールのリポジトリを作成する、 みたいなパターンが多いです。


つまりフレームワークの中身は、単機能に近い形まで分けて、 その中の一部だけでも使えるようにするというパターンです。


Meteor も内部をこういうふうに切り崩して、 モジュールを分けたらもっといいのではと思います。


その中のリアクティブを担当するモジュールなんかは、 他のプロジェクトからも採用されるんじゃないかなと。


また今回 meteor について調べたり、その後その関連で何人かの方と議論したり、
これまで自分がやってきたことを含めて、色々思うことがあったので、
あとでまとめて別エントリをあげようと思います。

outro

多分間違っているところがたくさんあって、もうすでに変わってしまったところもあると思います。(すでに Slide に書いた Meteor のライセンスが GPL から MIT に変わったらしい)
他にも何か気づいた点があったら教えていただけると幸いです。

最後に、場所や運営を提供して下さった nifty の方々、本当にありがとうございました。

Node.js の起動オプション、環境変数、npm start の話

Node は起動時に色々オプションをつけることができます。
面白いもの、有益なものあるんですが、あまり言及されてないので、
ちょっと紹介してみようかと思ってます。


最後の npm start の話は、それ単体で書いても良いかと思っていたんですが、
関連するし良い機会なので書きます。
そして、オプション周り興味がない方も、Node やってる方は最後の npm start の話だけでも、
読んでいただけるとと思ったりします。(知らない方が多いようなので)


ここで紹介している Node のバージョンは v0.7.7 です。しかし v0.6.x あたりでは、
v8 のバージョンが古く、オプションが微妙に違います。そこは v0.6.12 での結果を載せている場合もあります。

-h

まあ、とりあえず全ては -h から始まる。ということで、実行すると以下が出ます。

Usage: node [options] [ -e script | script.js ] [arguments] 
       node debug script.js [arguments] 

Options:
  -v, --version        print node's version
  -e, --eval script    evaluate script
  -p, --print          print result of --eval
  -i, --interactive    always enter the REPL even if stdin
                       does not appear to be a terminal
  --v8-options         print v8 command line options
  --vars               print various compiled-in variables
  --max-stack-size=val set max v8 stack size (bytes)

Environment variables:
NODE_PATH              ':'-separated list of directories
                       prefixed to the module search path.
NODE_MODULE_CONTEXTS   Set to 1 to load modules in their own
                       global contexts.
NODE_DISABLE_COLORS    Set to 1 to disable colors in the REPL

Documentation can be found at http://nodejs.org/

ここから面白いものを抜き出し、ここに無いものもあわせて紹介します。

debug

gdb ライクな debugger を起動できます。

$ node debug sample.js
< debugger listening on port 5858
connecting... ok
break in sample.js:1
  1 var http = require('http');
  2 
  3 debugger;
debug> help
Commands: run (r), cont (c), next (n), step (s), out (o), backtrace (bt), setBreakpoint (sb), clearBreakpoint (cb),
watch, unwatch, watchers, repl, restart, kill, list, scripts, breakOnException, breakpoints, version

help と入力するとコマンド一覧が出ます。
n, s, bt あたりを覚えておけばステップ実行によるデバッグができます。

--debug, --debug-brk

node 組み込みのオプションではない(と思う。。)ですが、
node-inspector を使う場合は、このオプションを付けます。

詳しくは node-inspector の解説に譲ります。

-e

渡した文字列をスクリプトとして実行できます。

$ node -e "console.log('hello world');"

http server

$ node -e "require('http').createServer(function(req, res) { res.end('hello world'); }).listen(3000);"

--max-stack-size=val

set max v8 stack size (bytes)

v8 のスタックの最大サイズを byte 単位で指定。

--v8-options

node というより v8 のオプションの一覧です。

$ node --v8-options

沢山のオプションが出てきます。


見ての通り、色々細かいオプションを指定できたりします。
また、 strict-mode や ES-harmony の機能など、
新しい機能を有効にするオプションもあります。


特に harmony は、ブラウザで使おうとすると互換性の問題がでてきますが、
サーバサイドであれば使いたい放題です。
これまでブラウザという制約上我慢していた方々も思う存分 harmony できますw


たくさんあるので、全部は紹介しません。
ここでは、いくつか知っておくとよいものだけを紹介します。

--strict_mode

allow strict mode directives

strict_mode を有効にします。

ECMA-262 » ECMA-262-5 in detail. Chapter 2. Strict Mode.

--harmony_typeof

enable harmony semantics for typeof

typeof null が "object" ではなく "null" になります。

harmony:typeof_null [ES Wiki]

--harmony_scoping

v0.6.x あたりまでは --harmony_block_scoping でした。

enable harmony block scoping

block scope を有効にします。
block scope については、下記あたりを参照。

harmony:block_scoped_bindings [ES Wiki]
ECMAScript 6th, Harmony と JS++ - hogehoge @teramako の 1.ブロックスコープ あたりです。


--harmony_modules

enable harmony modules (implies block scoping)

module を有効にします。
module については、下記あたりを参照。(正直あまり知らないです、すいません。)

harmony:modules [ES Wiki]

--harmony_proxies

enable harmony proxies

harmony proxie を有効にします。
proxie については、下記あたりを参照。

harmony:proxies [ES Wiki]
ES Harmony の Proxy について #fxdevcon で LT してきました - mooz deceives you

--harmony_collections

v0.6.x あたりまでは --harmony_weakmaps のみがありました、
v0.7.7 では set, map, weakmap がこのオプションですべて有効になります。

enable harmony collections (sets, maps, and weak maps))

harmony set, map, weakmap を有効にします。
これらについては、下記あたりを参照。

harmony:simple_maps_and_sets [ES Wiki]
harmony:weak_maps [ES Wiki]
Let's WeakMap - 枕を欹てて聴く

--harmony

v0.6.x あたりではありませんでした。
これをつけると harmony_typeof 以外の harmony オプションがすべて有効になります。

enable all harmony features (except typeof)

その他

v8-options の一覧に出るオプションのほとんどは、特殊なデバッグやプロファイルなどをしない場合、あまり使う機会は無いと思います。
また、正直「このオプション何ができるの?」と思っても、あまり情報が無い場合も多いです。自分もほとんどのものは使ったことがありません。
ただ、一度は grep なんかを駆使して色々見てみるといいと思います。


見ておきたいもの。

$ node --v8-options | grep log
$ node --v8-options | grep prof
$ node --v8-options | grep trace
$ node --v8-options | grep gc
$ node --v8-options | grep debugg
$ node --v8-options | grep size

なんかおもしろい or 有益なオプションがあったらぜひ教えてください。

環境変数

オプションとは違いますが、起動時にあわせて使います。
基本は、 xxx=yyy という変数をつけて実行すると、ランタイムからはprocess.env.xxx で yyy の値を文字列値として受け取れることを利用しています。
慣習的に NODE_XXX という変数が使われます。例えば独自変数 NODE_MY_FLG を指定したい場合。

$ NODE_MY_FLG=true node server.js

このように指定できます。


また環境変数には、慣習としてみんなが使っているものがあります。
以下に代表的なものを紹介しますので、覚えておくと便利だと思います。

NODE_ENV

実行環境を切り替えます。いわゆる production mode, development mode などの切り替えに使用します。

$ NODE_ENV=production node server.js

これを設定することで process.env.NODE_ENV が "production" になりますので、これを用いた実行モードの切替を行うことができます。
Express や Socket.IO などは、development, production の2つの場合は自動で確認して、モードを切り替えてくれます。

NODE_DEBUG

デバッグをする上で、知っておくと便利です。

Node の標準モージュールと、この作法に則った一部のモジュールは、このオプションにモジュール名を渡すと、デバッグを出力するようになります。
以下は、 http, net モジュールを指定して、作成した http サーバを起動してデバッグ出力を見ています。
複数指定するときの区切りはカンマです。

$ NODE_DEBUG=http,net server.js
NET: bind to 0.0.0.0
NET: onconnection
HTTP: SERVER new http connection
NET: onconnection
HTTP: SERVER new http connection
HTTP: server response shouldKeepAlive: true
HTTP: outgoing message end.
HTTP: server response shouldKeepAlive: true
HTTP: outgoing message end.

こんな感じです。

これは、 NODE_ENV と同様、 process.env.NODE_DEBUG を見て、デバッグの出力をトグルするコードが、モジュールの中に含まれているからです。

標準モジュールで対応しているのは以下。

$ grep NODE_DEBUG ./node-v0.7.7/lib/*
./node-v0.7.7/lib/cluster.js:if (process.env.NODE_DEBUG && /cluster/.test(process.env.NODE_DEBUG)) {
./node-v0.7.7/lib/http.js:if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) {
./node-v0.7.7/lib/module.js:if (process.env.NODE_DEBUG && /module/.test(process.env.NODE_DEBUG)) {
./node-v0.7.7/lib/net.js:if (process.env.NODE_DEBUG && /net/.test(process.env.NODE_DEBUG)) {
./node-v0.7.7/lib/timers.js:if (process.env.NODE_DEBUG && /timer/.test(process.env.NODE_DEBUG)) {
./node-v0.7.7/lib/tls.js:if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) {||<

他にも、この変数をチェックしてデバッグ出力に対応したモジュールもちらほらあります。
自分でモジュールを公開している方は、ぜひ対応してみるといいと思います。

NODE_PATH

これは慣習じゃなくて、仕様です。
モジュールを PATH に追加します。
これにより、標準モジュール以外のモジュールも、相対パスを書かないで require できるようになります。


具体的にはこういうことです。

まず、こんなディレクトリ構成だったとします。

$ tree .
.
├── lib
│   └── my_util.js
├── models
│   ├── index.js
│   └── posts
│       └── index.js
└── server.js

3 directories, 4 files


通常、この構成だと、server.js と models/index.js 、 models/posts/index.js から lib/my_util.js を使おうとしたら、こうなります。

server.js

var my_util = require('lib/my_util.js');


models/index.js

var my_util = require('../lib/my_util.js');


models/posts/index.js

var my_util = require('../../lib/my_util.js');

このように各ファイルで共有したいものも、遠い階層からの参照は、指定が汚くなります。
ここで lib を NODE_PATH に加えると以下のようになります。

$ NODE_PATH=lib node server.js

この方法で実行した場合は、 lib は PATH を解決できるので、全てのスクリプト

var my_util = require('my_util');

で my_util.js を require できるようになります。

以前 nodejs_jp の ML でも少し議論になりましたが、
自分はこの、

「共有したいものはディレクトリにまとめて NODE_PATH に追加」

が割りとすっきりしてていいんじゃないかと思います。
(ここで、 NODE_PATH の指定方法はどうやってユーザに伝えるの?という疑問が出た方は、最後に書くのでもう少し待ってください)


オプションの指定方法

多くのプロジェクトは、例えば git clone してきたら

$ node server.js

などとすることで起動できる、と思ってていいのはオプションがない場合だということがわかったと思います。


もしかしたらこのプロジェクトは

$ NODE_ENV=production NODE_PATH=lib node --harmony_typeof --harmony

で起動する必要があるかもしれません。
まあ、この例では reademe とかに書いてあればいいかもしれませんが、
例えば git push でデプロイするような PaaS なんかに上げて、
自動で起動してもらう場合にこれらをどう指定するかも問題になります。


そこで使うのが npm start です。

npm start

npm には npm start というコマンドがあります。
これは、 package.json の scripts.start に指定された文字列を実行するものです。
npm は、現在 node 本体に入っているので、この起動方法は node の実行環境において妥当な方法と考えています。

"scripts": {
  "start": "NODE_ENV=production NODE_PATH=lib node --harmony_typeof --harmony"
}


ちなみに npm についてまとめてみる - need something more... によると、
start 以外に prestart, poststart も書ける模様。知らなかった。。


Joyent の no.de の環境では、 git push したら npm start が実行されるので、この方法が使えます。
プロジェクトを作成する側は、 package.json をきちんと書き、clone した人は npm start をまず叩く。
README.md に「起動は node server.js で」とか書くより合理的でしょう。


なにより、 Node に対応している各 PaaS 提供業者様におかれましては、
勝手な起動仕様を作るより npm start に対応されることを、おすすめ and/or お願いしたいなと個人的には思っております。


npm start はもっとデファクトになっていいと思うんですが、あまり知られてすらいない気がする。(というか npm がデカすぎるせいなのかも)

まとめ

長かったのでまとめます。

  • node -h
  • node --v8-options
  • NODE_PATH, NODE_DEBUG, NODE_ENV
  • npm start


こうした部分はドキュメント化されているものが少なかったりしますが、
知っていると有益かと思い、とりあえず書いてみました。
細かいところまでは書けていませんが、何かわかったり要望があれば追記します。
情報も募集します。よろしくおねがいします。

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 をきちんと通せるものがなかなか少ない現状だと思うんですが、なにか良い方法を知っていたら教えて頂きたいです。


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

node.js の環境管理ツール nodebrew

intro

nodebrew は バージョンアップの速い node.js を、複数バージョン管理するためのツールです。
ruby の rvm や、 python の virtualenv、 perl の perlbrew などの node.js 版と思ってもらえれば良いです。


自分はこれまで nvm を使っていたんですが、今年初めあたりから全てのマシンで nodebrew に乗り換えました。


今日はこの nodebrew を紹介します。

既存の node.js の環境管理

既存の、ものとしては

  • nvm
  • nave
  • n
  • nodeenv

などがありました。
それぞれにあった問題については、過去に愚痴を書いています。


簡単にまとめると以下です。

nvm
bash向けに書かれてて、zshなどと相性が悪い場合がある。
nave
node へのパスを通した子shellを起動するタイプで、子shellというのが微妙なときが有る。
n
インストール先が /usr/local/n で、そこはいじりたくない。しかも n コマンドでプロセス起動。
nodeenv
python の virtualenv をコピーで、ちょっと過剰な感じ。


じゃあどんなのが欲しいかというと、

  • /usr とかいじらず、 home 以下あたりに入れてパスを通すだけ
  • sudo 無しで完結
  • 簡単にバージョンが切り替えられる
  • sh ではなく言語で書いてプラットフォーム(bash, zsh etc)に依存しない
  • できれば mac に最初から入ってるような言語で書いてある


まあ、「なら自分で作れよ」って話で、実際ちょっと考えてたんですが、
tumblr に記事を上げてからすぐ、 @ さんがまさしくそのままのものを作って公開されました。
hokaccha △!!

nodebrew書いてみた - hokaccha.hamalog v2


こちらで公開されています。

hokaccha/nodebrew · GitHub


特徴はこんな感じ

perl で書かれてる
Mac やサーバ OS に入ってる率高め
curlwgetワンライナーで入れる
入れるのに git とかいらない
パスを指定すれば nodebrew コマンドが使える
インストールに sudo がいらない、 .bashrc/.zshrc とかに書けばいい
.nodebrew 以下に node をインストール
sudo いらないしバージョンの切り替えもパスだけだし、 /usr とかいじらないし、消すのも .nodebrew だけで他を汚さない。


他にも、 ls-remote や clean、 selfupdate など、
欲しい一通りの機能がついています。
なんで無かったんだろう、というくらいコンパクトで使いやすいです。
初心者の方々でも、一番トラブル少なく使えそうかなと思ってます。。
(ただし windows は知りません。)

使い方

最後に簡単な使い方を紹介します。といってもほぼ README のままです。
(必要な perl のバージョンとかはよくわからない。。)

インストール

落として perl に渡すだけ。
ホームに .nodebew ができるだけなので sudo はいりません。

curl
$ curl https://raw.github.com/hokaccha/nodebrew/master/nodebrew | perl - setup
wget
$ wget https://raw.github.com/hokaccha/nodebrew/master/nodebrew
$ perl nodebrew setup

パス

.nodebrew 内に node.js がインストールされるので、そこを PATH に足しておきます。
.zshrc や .bashrc などのファイルにこれを足せば良いだけです。

export PATH=$HOME/.nodebrew/current/bin:$PATH

足したら読み直しなど。

$ source ~/.bashrc

node のインストール

あとは非常に直感的

$ nodebrew install latest # 最新
$ nodebrew install v0.6.x # v0.6
$ nodebrew install 0.6.0  # `v` 無しもおk

バージョンの切り替え

$ nodebrew use v0.6.0

これも .bashrc とかに書いておくと良いかも。
例えばこんな感じ。

# source nodebrew
if [[ -f ~/.nodebrew/nodebrew ]]; then
    export PATH=$HOME/.nodebrew/current/bin:$PATH
    nodebrew use v0.6.0
fi

その他のコマンド

一通り揃ってます。
ls-remote と selfupdate あたり便利。

$ nodebrew help                    Show this message
$ nodebrew install <version>       Download and install a <version>
$ nodebrew uninstall <version>     Uninstall a version
$ nodebrew use <version>           Modify PATH to use <version>
$ nodebrew list                    List installed versions
$ nodebrew ls                      Alias list
$ nodebrew ls-remote               List remote versions
$ nodebrew ls-all                  List remote and installed versions
$ nodebrew clean <version> | all   Remove source file
$ nodebrew selfupdate              Update nodebrew

あとがき

Windows となると話は変わりますが、Mac メインな自分としては申し分ないです。
issue とか、 perl 書ける人はパッチなんか送るときっと喜んでもらえるんじゃないでしょうか。
(自分は perl 書けない。。すいません。。)

最後に、 @ さん、素晴らしいツールありがとうございます、お疲れさまでした!