Block Rockin’ Codes

back with another one of those block rockin' codes

"リアルタイム Web" に関するプラクティスのアウトプット

追記

11/12/26
MLのスレッドへのリンクが間違っていたので修正。

introduction

WebSocket なんかをつかって、従来のステートレスな処理以外に、コネクションを継続するステートフルな処理が可能になりました。
これを利用すると、これまで実装が難しかったリアルタイムな表現を Web に持ち込むことができます。


そして、 WebSocket を用いたプログラムを作成する上で、Node.js と Socket.IO を用いる方法について、
今年はこのブログでも何度か紹介してきました。


今日は今年一年の集大成として、自分が色々試しながら得たリアルタイム Web に関する知識、技術などを、
ここにまとめてアウトプットしたいと思います。


今回お話しするのは、 東京Node学園 3時限目 : ATND で発表した下記内容の抜粋です。


Node Academy | "About SlideStream & Tips for Realtime"


この資料は、今年10月に開催した 東京Node学園祭2011 の自分のセッションで使った、
SlideStream というスライドツールに関する発表で使ったものです。
この発表では、

について話しましたが、この記事ではそれを元にして、主に「リアルタイム Web」をいかに開発するか?
に絞ってお話しします。長いです。

Caution

  • そろそろ、Socket.IO 初心者向けエントリは飽きたなぁ、くらいの人を対象にしています。
  • 「リアルタイム Web 」= 「WebSocket を多用した使った Web」 くらいの意味で取っていただいていいですが。
  • 実装に Socket.IO on Node.js を使っていることが案に前提になってしまっています。
  • 生の WebSocket や別の言語、ライブラリを使う場合は、必ずしも全てが当てはまるとは限らないです。
  • Socket.IO のセキュリティというあまり出てない話に触れています。これは読み手に実益/実害をもたらす可能性があるかもしれませんが、一切責任は取りません。
  • 脆弱性という言葉を使いますが、知る範囲で実例はありません。可能性の話をしていると思っていただき、自分で検証してください。
  • SlideStream は東京Node学園祭の前に割と急いで作ったので、この実装や、そこでした判断が「完璧な解」では絶対にありません。が話はその時の実装がベースです。
  • 間違っている可能性は大いに有ります。気づいたら教えて下さい。

SlideStream

前提として、 SlideStream について、簡単に紹介したいと思います。

SlideStream は

  • Presenter がスライドのページを送ると、 Viewer のページも一緒に進む。
  • Viewer がページを動かしても、他の誰にも影響しない。
  • Presenter のマシンのコマンドライン出力などが、リアルタイムに Viewer のスライドに表示される。(ライブコーディングならぬ、リアルタイムコーディングと言ってます)
  • HTML ベースなので画像、動画などを iframe, link などで埋め込み、他のリソースを共有できる。
  • ベースは deck.js

というものです。

構成は以下のようになっています。



Node-Ninja にデプロイし、プロセスは一つで、 Redis を一部使ってます。
他に特別なことはしてません。
他、詳細はスライドを参照してください。

Auth(enticate|orize) of Socket.IO

まずは、認証(Authentication)と認可(Authorization)の重要性の話です。

ページ遷移と Presenter の識別

最初の機能として、 Presenter (つまり自分) がスライドのページを送ると、 Viewer のスライドもページが移ります。
これは、自分がスライドを動かす時、そのイベントを deck.js の API から補足し、Socket サーバに送っているためです。
Socket サーバは broadcast で、そのことを全 Viewer に伝え、 Viewer のブラウザは deck.js の API を叩いてスライドのページを変えます。


これは、特に難しくも何ともないんですが、 Presenter - Viewer が一方向ということを忘れて実装すると、
接続してる全ての Viewer のローカルでのページ切り替えが、全て別のユーザに broadcast されて、
それはもう大変なページ切り替えの嵐で、地獄絵図必至です。


正しく Presenter のみの切り替えが伝わるようにするには、以下のようにします。

  • Socket サーバは、各クライアントのセッションを識別できるようにする。
  • その中で Presenter の識別だけは認証しておき、「この接続は Presenter だ」と分かるようにしていおく。
  • 全ての Viewer と Presenter はページ遷移を Socket サーバに送るが、 「broadcast するのは Presenter のメッセージだけ」にする。


これで地獄絵図は避けられます。自分はプレゼンテーションの前にひっそり自分だけ認証をしていました。(認証ページ)
これは単純な HTTP アプリの cookie ベースのセッション認証と同じです。


WebSocket は、一旦 HTTP でリクエストした後、 upgrade を経て WebSocket にプロトコルを切り替えており、
Socket.IO はその時、 HTTP が持ってた情報を WebSocket に handshakeData というオブジェクトにして引き継ぐ仕組みを持っています。
ここに Cookie も入っているので、あらかじめ HTTP で「認証」しておけば、 WebSocket のコネクションも「認可」ができます。


SlideStream では以下のようになってます。

まず、 Express で認証しセッションデータに session.admin = true というフラグを立てておきます。
WebSocket への切り替え時は、それをそのまま handshakeData に渡しておきます。

io.configure(function() {
  io.set('authorization', function(handshakeData, callback) {
    if (handshakeData.headers.cookie) {
      var cookie = handshakeData.headers.cookie;
      var sessionID = parseCookie(cookie)['connect.sid'];
      // check the express session store
      sessionStore.get(sessionID, function(err, session) {
        if (err) {
          // not found
          callback(err.message, false);
        } else {
          // found
          handshakeData.session = session;
          callback(null, true);
        }
      });
    } else {
      return callback(null, true);
      // socket.io-client from node process dosen't has cookie
      // return callback('Cookie dosen\'t found', false);
    }
  });
});


ブラウザでページを切り替えると、 go イベントが発生するので、
それを補足し、同じ go イベントで Socket サーバに Presenter, Viewer 全員がページ(to)を送ります。

Socket サーバは、 go イベントを受け取ったら、セッションのフラグを確認し、 Presenter のものだったとき(admin フラグが有ったとき) だけ、自分以外に broadcast します(認可)。

socket.on('go', function(to) {
  if (!socket.handshake.session) return false;
  if (!socket.handshake.session.admin) return false;
  socket.broadcast.emit('go', to);
});


細かい実装方法は、以下の二つを参照して下さい。

Practice

Socket.IO については上述の方法で認証ができます。つまり同等の仕組みで WebSocket についても可能です。
WebSocket は通信の自由度が高い(Client/Server 共に push できる、クロスドメイン etc) なので、
その各セッション/イベントなどの粒度で「適切な認証/認可」を行うことは重要です。


例えば、上述の 「Presenter によるページ切り替え」がもっと重要な操作だった場合どうでしょう?
セッションを乗っ取られれば、 broadcast 対象にあたる全てのクライアント(ここでは Viewer)に、
不正なメッセージを送ることができます。


また、具体的な乗っ取りは、上記方法では Cookie の乗っ取りと等価になると思います(未検証)。
つまり、 Cookie に大してはおおむねこれまでと同等に、重要視して扱う必要があるということです。
(「WebSocket でコネクション保持だから、Cookie とかもうどうでもいい。」ではないということ。)


また、実装方法によっては、 SessionStrage にあたる部分の、参照が劇的に増える可能性があります。
今回は Redis を使っていますが、十分な速度が出ていたし、 Socket.IO / Express との相性はいいと思うので、お勧めします。

broadcast injection

Socket.IO では、イベント名にも気をつけた方が良いかもという話です。

WebSocket 時代の脆弱性(?)

入門向けサンプルだと、例えば以下のようなエコーサーバの例があると思います。

// Server
io.sockets.on('connection', function (socket) {
  socket.on('msg send', function (msg) {
    socket.emit('msg push', msg);
    socket.broadcast.emit('msg push', msg);
  });
});
// Client
socket.on('connect', function() {
  $('#msg').click(function() {
    socket.emit('msg send', 'data');
  });
  socket.on('msg push', function (msg) {
    display(msg);
  });
});


これは、ブラウザ上で操作していると、 $('#msg') である何かをクリックしないと発生しないように作ったつもりだと危ないです。
なぜなら、その画面で開発コンソールなどを開けば、以下のようにすると任意のメッセージを broadcast に載せることができます。

socket.emit('msg send', '悪意のあるメッセージ');


Socket.IO あまりよくわからなくても、「そりゃそうだろ」と思う程度かもしれませんが、
意識しないと、これも脆弱性になる可能性があります。
もちろん Chat のようにそれでも良い場合や、「送れたところでなに?」ということもあると思います。
でも、「broadcast 対象全員に任意のメッセージを送れる」状態には、気を付けないといけないと思います。

SlideStream の場合

SlideStream であった、ちょっと具体的な例を出しましょう。


SlideStream では、 Presenter (自分)がローカルの Emacs で入力した文字を、
ファイルから読み取り Socket サーバに渡し、 broadcast してリアルタイムコーディングしていました。
入力は browser ではなく、 ローカルに立てた Node のプロセスから送っていたので、
実際は Socket.IO(Socket サーバ) と Socket.IO-Client(自分の Mac ) の間でのプロセス間通信です。


ところがこの時実は Socket.IO-Client のリクエストは Cookie を持たないので、前述した認証が出来ませんでした。


すると単純には、以下のようなコードになります。(例)


まず、 codeStream という EventEmitter から、 Emacs に書いたコードが流れてきます。
これを、 code というイベント名で、 Socket サーバに送ります。

// Presenter Local Process
var socket = require('socket.io-client').connect('http://' + host + ':' + port);
socket.on('connect', function() {
  codeStream.on('code', function(data) {
    socket.emit('code', data);
  });
});


Socket サーバは、 code イベントで得られたデータを、 Viewer に broadcast します。

// Socket Server
socket.on('code', function(data) {
  socket.volatile.broadcast.emit('code', data);
});


ブラウザは code イベントから結果を受け取り、表示処理をします。

// Viewers Browser
socket.on('code', function(data) {
  codeRender.rawRender(data);
});


なんとなくわかるでしょうか?

そしてこの場合、 Viewer はブラウザから

socket.emit('code', 'm@(^_^) pugya~~~');

で、リアルタイムコーディングをみごと台無しにすることができます。


認証ができないけど、放っては置けないのでどうしたかというと、 Socket サーバのイベントはソースを見ない限りはわからない & 発表時はまだソースは公開していない(というかこれもあってしなかった) ので、イベント名を表からは推測しにくいだろうものに変えて、対応しました。

Practice

初心者向けサンプルを dis るつもりはありません(というか上のは自分の記事からです)。ただ、もちろんちゃんとやるなら気をつけるべき点は、そこには書かれていないこともあるでしょう。


あと、v0.6? くらいまでは、 Socket.IO は 'message' というイベント名しか使えませんでしたが、今は任意の文字列が指定できます。


まずなによりも、認証できるならして、認可によって制限することが大事です。
そうでもなく、上記のようになんらかの理由から認証/認可での制限が難しいとか、
たとえ Cookie を盗まれてもこのイベントだけは任意に叩かれるとまずい、
というようなものは、推測しにくいイベント名に変えましょう。
イベント名は、送る側と受ける側だけ同じであれば良いです。
上の例のように、たらい回しにするところを全て同じにしたら、簡単に推測できます。



あなたの作った MMO ゲームは、 socket.emit() だけで画面の端までワープできたりしませんか?



ただでさえ、クライアントが自由にクロスドメインから接続できて、 emit() だけで手軽にデータを送りつけられてしまうので、
重要な API はランダム文字列などを用いる方が良い場合が有るかもしれません。


もちろんソースを公開してしまうと元も子もないので、DB のパスワードとかと同じように、別ファイルに出して .gitignore などの隠蔽工作も場合によっては必要です。イベント名は単なる文字列なのでやりようはいくらでもあります。


今後は、このイベント名をうまく隠蔽する仕組みが恐らく色々出てくるでしょう。
フレームワークレベルでの解決は有りなケースだと思います。、


自分も後で述べる自作のフレームワーク内ではこの問題を扱いたいと思っています。

volatile & diff

話は変わってパフォーマンス系の話です。

例えば、リアルタイムコーディングで、書いたソースコード文字列を broadcast するとなると、ちょっと大きなソースになった時
「Diff を送って、ブラウザで復元すれば効率がいいのでは?」という考えが浮かびます。
JS には google-diff-match-patch というそのものズバリなライブラリがあるため、
実際自分もそれを考えたんですが、それはやめて、「一文字入力する毎に、ソース全体を送る」を選びました。


採用はしなかったけど、実装はしたので、その残骸が実は http://d.hatena.ne.jp/Jxck/20111112/1321079097 だったりします。
Both-sides Javascript 自体は便利なのでそっちで出しました。


やめた判断は、 Socket.IO の 「Volatile の性質」と、後述する「CPU と I/O のバランス」から来る物です。

volatile オプション

volatile オプションについては、ここの「揮発性メッセージ」を読んで下さい。
簡単に言えば TCPUDP の関係です。送達確認の有無です。ちなみに送達確認(ACK) についても同じ記事に書いてあるので、合わせて読んでおいて下さい。


さて、 volatile オプションを付けて diff を送ると、 diff の送達が欠損した場合、きれいに復元できない場合があります。
例えば、 「foo を書き、foo を消して、 bar にする」という処理が、 3 つの diff になったとします。

+ foo
- foo
+ bar

期待する結果はもちろん

bar

でももし volatile がついていたとして、真ん中の -foo が欠損したら、以下の結果になります。

foo
bar

もちろん、 volatile を付けなければ、恐らくリトライして正常にいくと思います。
いかなくても、送達(ACK)を確認しエラーを検知して全部取り直すとかやりようはあります。

volatile 有無の判断

いくつかの要因から volatile を使う方向で実装することにしました。


まず、リアルタイムコーディングはページ切り替えなんかよりよっぽどリアルタイム性が高く、
SlideStream では1文字1イベントで設計しています。
この場合、どこかで詰まると、コーディングの表示が止まる可能性があります。
つまり volatile を付けないと詰まった場合の制御も考える必要がでます。


逆に volatile を付けて、かつ毎回全文送ってしまえば、一文字欠損しても、
その次のイベントで全部のコードが来て、全部書き直せば、 Viewers には2文字の表示が速かったように見えるだけです。


また、当日、会場のネットワークがどの程度のものなのか、想定できなかったのもあります。


なによりも、「全部送って、丸っと書き直す」スタイルは、実装がとても楽だったこともあります。

Practice

今は、リアルタイムと一言で語られていますが、リアルタイムにも程度があります。
一分に一回のページ切り替えと、一文字一回のコーディングでは、イベントの頻度が違います。


後者の場合、例えばゲームでの座標移動や、マウスカーソルの動き、 Twitter のストリームなどを扱う場合、
それが「多少消えてもいい」ものであれば、 volatile を使うことが Socket.IO のドキュメントでも推奨されています。


volatile をつけなければ、送達の確認もできますが、上の例ではそれは適切ではないでしょう。


また、実装を読むと、 volatile は socket に書き込めなかったら捨てるという実装のようです。
(つけないと、その時 ACK のコールバックを実行して、リトライ/タイムアウトなど)


それがどの程度の頻度でおこるかは、当日のネットワークとクライアント PC の状態に依存すると考えられます。
つまり、その性質上、事前判断はむずかしく、現実的に 「その時 Socket に書き込めるかどうか?」を、
判断基準にするのは難しいだろうことを付け加えておきます。

Domain Specific implementation

これは、賛否わかれる問題かもしれませんが、率直にあったこと、思ったことを書きます。
今までのライブラリ、特に「汎用性」をもったものは、リアルタイムに不向きな可能性もあるということ。

謎のボトルネック

長いので簡単に話すと、 SlideStream を本番前に何人かに接続してもらって試したら、だんだんもの凄く重たくなってしまい、使い物になりませんでした。
原因を探るために、 node のログや、プロセスなんかを色々調べても、特に問題はない。
ローカルで試す分には出なかったのに、本番数日前に気づいたので、とても焦りました。
結論から言うと、重たかったのはブラウザの自滅で、サーバもネットワークも全く関係なかったのです。

シンタックスハイライトと従来のライブラリ

恥ずかしいですが、あったまま話すと、原因は意外にも、シンタックスハイライトでした。
今回、コードのシンタックスハイライトは、使い慣れた SHJS - Syntax Highlighting in JavaScript を使って、 BashJavascript を色付けしてました。
このライブラリは、 pre タグの中の文字列を一気に書き換えるので、volatile を選んだ今回の場合「コードが毎回全部送られてきて、それを毎回ごそっとハイライト」になります。
で、これをリアルタイムコーディング中もの凄い回数繰り返していると、ブラウザでメモリがもの凄く消費され、スライドがもの凄く遅くなっていました。
Node.js でリアルタイムということで、すぐにデータの転送量や、 Node プロセスを疑ってしまったのですが、Chrome のコンソールを開けば一発でわかることでした。


シンタックスハイライトがメモリリークしていたのかを調べて、直しても良かったんですが、自分にはとにかく時間がありませんでした。
しかし、 SHJS が他言語のハイライトに対応するために、オブジェクト指向の流儀でクラスを分割し、拡張性、汎用性を高めていることにより、
生成されるオブジェクトに無駄があることは明白でした。


今回の発表はストーリーも概ね決まってて、ハイライトしたいソースも分かっています。
また、実はハイライトって、それっぽく色がついてると、それっぽく見えるものです(標準が無いですし)。


そこで、以下のように急いで作った正規表現で、SHJS の css だけ借りて、だいたい SHJS と同じようにハイライトするものを以下のように書きました。
見ると嘲笑されてしまいそうな、恥ずかしいコードですが、あえて張ります。

render.prototype.higlightJS = function() {
  this.cache = this.cache
    .replace(//g, '>')
    .replace(/"(.*?)"/g, '\"$1\"')
    .replace(/'(.*?)'/g, '\'$1\'')
    .replace(/(var|function|static|true|false)/g, '$1')
    .replace(/([A-Za-z0-9]*?)\(/g, '$1(')
    .replace(/^\/\/(.*)/g, '//$1')
    .replace(/[^:]\/\/(.*)/g, '//$1')
    .replace(/\/\*\*(.*)/g, '/**$1')
    .replace(/\*(.*)/g, '* $1')
    .replace(/(\d{2,4}?)/g, '$1')
    ;
};

render.prototype.higlightBash = function() {
  this.cache = this.cache
    .replace(/(ls|cd|tree|rm|\sexpress\s|npm|\snode\s)/g, '$1')
    .replace(/(create)/g, '$1')
    .replace(/(Jxck\$)/g, '$1')
    .replace(/(\d{4}?)/g, '$1')
    ;
};


思いついた順に replace をつなげただけのコードなので、中身はすぐ分かると思います。
しかし、これに置き換えることで、 SlideStream はあのリアルタイム感を得ることができました。

Practice

SHJS はもちろん悪く有りません。ただしこれは、 SHJS が
「ブラウザの最初の表示で一回だけ動作」することを想定している、
つまり従来の Web での範囲に最適化されたものだというだけの話で、
その範囲では、言語の定義を追加することも簡単な、拡張性のあるすばらしいライブラリです。


しかし、こうした既存のライブラリが、リアルタイムな場面では適さない作りになっている場合がある、
という可能性を痛感した場面でした。


もちろん SHJS の拡張性をそのままに、リアルタイムに有った形に改善することも出来るでしょう。
でも、ここではそれとは別にもう一つ、この拡張性を捨てればもっと速くなるかも?という解も出てくると思います。


ちょっと飛躍しますが、上手く言えないですが、

「従来の Web を、現状の全ての仕様を満たしたままリアルタイムにする」

というような要件も、今後出るかもしれません。
この記事に上がってること、上がってない他の要因含めて、自分は難しいと思います。
そこでは「何かを捨てる」というより、もっと積極的に「必要最小限にする」が重要になる場面がある気がしてます。


今回はまだ良いとしても、リアルタイムゲームくらいの世界になれば、



と、光の速さが問題に上がるくらい、「速さ」が重要になります。


例えば、今回の場合、「使う言語」「使うキーワード」が分かってました。それ以上に対応しても、遅くなるだけでした。
だとしたら、美化する訳ではありませんが、自分が書いた replace チェインは、あながち悪い方法では無かったということも、できなく無いのではないでしょうか?
(もっと速くできるとかは別として。)
高速化、これは今までもあった話です。でも High Performance Web Sites なんかに載っていたのは、
ブラウザの読み込みサイズを減らすとか、最初の表示をいかに速くするかとか、確かそういう話でした。
しかし、リアルタイムな場合、最初の表示も大事ですが、表示してからが勝負です。
とにかくリアルタイムに見せたい場面が、リアルタイムに見えることが重要だと思います。
そのためには、光の速さまで気を使って、恥ずかしい感じのコードになってでも最小限の実装の方がいいかもしれません。


自分はそれを踏まえて、発表のときはこのような話をしました。

important for realtime is looks like realtime

ちょっと間抜けな文な気がしますが、そういうことです。
さすがに replace チェインは、単に汎用性を失ってて、それ自体は規模の大きい開発の場面では「悪」であるでしょう。
しかし、「そのくらい割り切った実装も、速さを重視するには必要かも」くらいには思っておくべきだと自分は思いました。
スピード狂な方々が、存分に真価を発揮する場面と言えるかもしれません。

balance btw CPU Heavy & I/O Heavy

ここは、リアルタイムに限らず、 Node.js を使う上で理解しておくべき点です。
しかし、Non-Blocking I/O やイベントループの話は理解できてる前提で書きます。

ロジックとイベントループ

この話は、具体例として先ほどの Diff の件をとって説明します。
さっきの話を読んで、


「自分なら volatile がついていようと、復元も含めて diff でばっちりロジックを実装できるぜ!」


と思った方、それはすばらしいですが、あなたが組むロジックは、イベントループを止めませんか?


Node.js をシングルプロセスで動かして、 nextTick などのテクニックを使わずに、実直に複雑なロジックを組んだ場合、
そのロジックが CPU を占有している間、「イベントループが止まる」状態になる可能性があります。
つまり、そこをボトルネックとして、他の処理がブロックされます。
これが Node.js で CPU Heavy(= CPU をごりごり使う処理)を書かないように気をつけるべき理由です。


対して、 I/O Heavy(= Disk, NW への読み書きを、がつがつ行う処理) については、それがどんなに重たくてもブロックしません。
だから、 SlideStream のようにたった1プロセスで動いているスライドツールに、2-300アクセスがあって、 broadcast を繰り返してもちゃんと動きます。
一方自分の Mac は一文字入力する毎に Disk, NW にアクセスしても、ファンがうなり声を上げて恥を書くこともありません。
1プロセスの1スレッドなのでメモリも使わないし、C10K 問題もありません。(これは、ちょっとしたベンチマークを最後に載せます。)


これが Node.js のメリットであり、リアルタイムと相性がいいという理由の一つです。


そのメリットは、「ブロックしちゃうような処理とか書かないし!!」と顔を真っ赤にして言う人があらわれる理由の一つです。


Event-loop harassment Pan-da

さて、こうして「ブロックしちゃうような処理」を書いて、イベントループを止めてしまうことを、
元Joyent のチーフエバンジェリストで、今は Node.js のパフォーマンスのコンサルタントをやっている、 @ はこう表現しました。

「イベントループハラスメントパンダ」です。(元ネタは JSConf2011 のこの資料 P29)



元ネタはサウスパークの「セクシャルハラスメントパンダ」という日本ではおよそ通じないネタです。


Diff に話を戻しましょう。


Diff 取る処理と、それを戻す処理は CPU 演算です。
それを、 volatile を付けずに実現して、リカバリのロジックを追加していったとすると、
おそらくどんどん CPU Heavy になる可能性があります。


一方、全部まるっと送るよう単純化した処理は、ほとんどが I/O での受け渡しです。
そして、その「単純にするという最適化」は、イベントループを滑らかに回し、
よりリアルタイム性を増す場合があると言えます。


つまり Node.js は、


「下手に最適化しようとごりごり演算ロジックを書くほどに、イベントループを止めて遅くなる可能性がある」


ということです。


これは、実はブラウザでも同じです。WebWorker や setTimeout などを使う手もありますが、それもバランスです。
それを踏まえると、先ほどのハイライトのreplace() も、イベントループの停止を最小限に抑えているという評価も可能になってきます。

simple is fast, fast is realtime


発表では、こんなふうにまとめちゃったけど、その時はこういうことを話していました。



馬鹿みたいに単純な処理の方が速い場合が有ります。
というか、それを「馬鹿みたいに単純な処理」ではなく、


「CPU Heavy なのか? I/O Heavy なのか?」


という観点で見る必要が有ります。

特に、サーバは処理が集中するので、ロジックによっては、これは顕著に出る場合が有ります。
たとえば最近、Nodejs_jp の ML にあがったこれなんかその典型と言えるでしょう。


もちろん、全てを I/O でカバーするにも限界があるのも事実なので、
重要なのはバランスです。


サーバはクラスタを組むことも出来ます。将来的にスレッドのような仕組みも入る予定です。
でも、本来の Node.js の性質を知ることの方が先だと思います。


そうすれば、 フィボナッチが遅い理由も分かると思うし、
自分が Jxck's OutPut - Nodeにテンプレートエンジンはもういいかな と考えた理由も伝わるかもしれません。
発展させると シングルページっぽいアプローチ(SSP) がよさそうなことや、
このメリットが生かせないアプリは、 Rails とかでやれば良いのでは?というそもそも論もきちんとできます。


じゃあ、バランスってどこで見るのか?それは難しですが、次にもう一つ話をしたら一つのアプローチを紹介します。

Client <> Server

SlideStream の場合、メインのクライアントはブラウザです。
そしてブラウザの性能も、PC のスペックも、どんどん良くなっています。
その上、 HTML5 で出来ることが増え、Node.js の特性でもある、
「クライアントとサーバで同じ言語が使える=Both-Sides JavaScript」の助けもあって、
選択肢は広がりました。


「この処理は、クライアントとサーバどちらにやらせるか?」


は十分に考えて、「従来のサーバが全部やってブラウザは表示だけ」は幼稚園までなのはもちろん、
Ajax でデータを非同期に Pull して表示」も小学生までなのは言うまでもありません。
ロジックをきっちり持たせ、場合によってはクライアントだけで完結する時代ですしね。

Practice

これもバランスです。ここまでの話を総括すると、 Node.js ベースのアプリには、
このバランスを取る軸として大きく四つの選択肢があることがわかります。



今回 SlideStream で言えば、青い印あたりを狙ったことになります。
なるべく I/O に寄せています。主に Socket.IO の broadcast サーバです。
また、サーバでハイライトしてから、クライアントに送るという処理もできますが、
例え単純な replace でもサーバではやりません。クライアントにやらせます。


この表で、サーバオンリーでCPU演算ごりごり(赤い印)ってどうでしょう?
たぶんそういう要件なら、 「Rails でいいのでは?」という意味が伝わるでしょうか?
まさかとは思いますが、 Rails を dis る気はさらさらないです。というか Django でも Cake PHP でもいいです。


それぞれメリット/デメリットをちゃんと理解し、使い分けをちゃんとすればいいだけです。
そうすれば、誰かが顔を赤くしたりして残念なことになることもありません。そういうの見るのはあまり好きではないんです。
(蛇足ですが、一部がリアルタイムな Web の構成として、「Node+Railsで連携して両方のいいところ取る」というのはありだと思っています。)


この辺の考えがわかると、あとはアプリ毎に最適化するだけです。
ちなみに出しておいて言うのもなんですが、このマトリクスにもとらわれない方がいいです。例です。

ベンチマーク

SlideStream については、このスライドを https://node-ninja.com/ に上げて、
FirstServer さん協力のもと、大量のブラウザからアクセスするという、負荷テストをする機会がありました。


結果は以下です。あくまでも SlideStream の結果であって、それ以上のものを示す意図は有りません。



グラフは DTrace(Joyent の Bryan Cantrill が考案したトレースツール)が吐き出したデータをグラフ化したもので、
Node Ninja の Analytics 画面から見れる(一部は見れないものもある)ものです。


このグラフから、どういったことが読み取れるかについては、中の人である @ さんに色々教えてもらいました。
ほとんどその受け売りになってしまいますが、書きたいと思います。

テスト条件
  • 30分にわたって接続数を増やしていき、最終的には 1000 程度の WebSocket コネクションを張った。
  • その中で SlideStream を使ったリアルタイムコーディングのセッションを、リハーサルした。
  • Node.js のプロセスは一つ、バックに Redis を利用。利用していないが MongoDB も起動していた。
結果

結果は以下です。なお、このテストの間、リハーサルの Viewer には、スライドが問題ないリアルタイム感で見えていたことを確認しています。


グラフ詳細(各グラフは毎秒単位)

左上より

[TCP] Accepts / remote IP(TCP接続確立数 / IP)
単位時間毎の接続数なので、デモ中に接続がどんどん増えていっている。
[Network interface] VNIC bytes sent and received / sent and received(データ送受信バイト数 / 送信・受信)
約 500KB/sec ぐらいのデータが送られている。ほとんどがテキストデータなのでボトルネックになるような量ではなかった
[Filesystem] logical read/write operations / app name(論理的な読み書き数 / プロセス名)
ファイル I/O の回数は少なく、ボトルネックになるような量ではなかった

右上より

[CPU] thread executions / app name(CPUスレッドの実行数 / プロセス名)
mongod が定常的に何かを実行し、Node が上限に振れている。Redis はほとんど CPU を使っていない。跳ねているのはutmpd という監視プロセス
[CPU] aggregated CPU usage / zone name(CPU利用率 / 仮想マシン)
サーバ全体で定常的に 25% 程度使われている
[Memory] resident set size / zone name(メモリ使用バイト数 / 仮想マシン)
サーバ全体で 200MB 程度。(その中で Node+Redis が消費したメモリは 50MB 程度であったとのことです。)
考察

CPU, ディスクI/O, ネットワークI/O の負荷は定常的に低く、ボトルネックになるような状態には至っていない。
またメモリも 1000 コネクションで、サーバ全体が 200MB 程度なのに対し、 Node(+ Redis)が自体が消費したメモリは 50MB 程度だった。
MongoDB や MySQL を要するようなデータの永続化とそのキャッシュなどが、 SlideStream には無かったため、メモリや File I/O の負荷が少ないのは当たり前と言えば当たり前ではある。
しかし Apache の場合 1 コネクションで 5MB ほど消費する(親プロセスや読み込むモジュールなどによって大きく変わる) とのことで、そうした環境で SlideStream を動かし 1000 程度のリクエストがあったら、もっとメモリを消費したと考えられる。


少なくとも全体的にリソースの消費は対したことなかったと言えるのではないでしょうか。

Stream Based Application

最後に今後の話です。

今回作った SlideStream ようなツールは、ソースに重複が多く有ります。
これは、クラスっぽくメソッドが抽象化できても、
そこにイベントをうまく抽象化しきれていないあたりが大きいなぁと感じていました。


また、統一したインタフェースがあると、色々と捗ることは間違い有りません。
そこで、 Node.js のもつ Stream が活用できると思います。

Node.js の Stream はあまり話が出ていなかったので、アドカレで書きました。
こちらを読んで下さい。


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


そこに書いたように、データの流れを統一したインタフェースで扱うための仕組みです。
SlideStream のソース中も xxStream というオブジェクトがいくつか出てきますが、
あの頃はまだ Stream の理解が浅く、インタフェースに則ってはいません。
しかし、やりたいことは割と同じ感じにはなっているので、今なら書き換えも難しくはないでしょう。


Stream 自体はまだまだ議論の余地のあるものですが、個人的には重要になってくると思っています。

ステートレスとステートフル

これまでは 「HTTP はステートレスである」という前提が、全ての Web アーキテクチャの根幹に有ったと思います。
Cookie ベースのセッションも、画面遷移という設計も、基本その前提の上でつくられ、
Web Application Framework (WAF)なんかもそれに最適化されていたと思います。


REST もすばらしい考え方です。


Ajax が出てきても、基本はそのリソースを非同期に取得するといった使い方から始まって、
まだその枠を出ない場面も多いです。


しかし、 WebSocket はコネクションを保持するステートフルなプロトコルです。
これで実装するアプリケーションを、従来の WAF の枠で考えることは、
ちょっと難しのかなと言うか、もし枠を拡大するなら、実装を変えないといけないかもしれない。


今の自分では、まだ結論は出せていませんが、ここで Stream は一つ重要なキーワードになると思います。

Practice

Stream を用いて、以下のように考えを転換します。


「全てのリソースは、 Stream である」


リソースは REST などの議論に出るあのリソースです。
それはチャットでも twitter Streaming でもいいし、「クライアント」「サーバ」という粒度もあり得ます。
とりわけ今回のように連続してデータを生成するものに当てはめると見えてきます。


そこで、全てを Stream として抽象化して扱う、というモチベーションで考えたのが拙作の Stream.IO です。


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


まだまだ、これからですが、ここからは今後リアルタイム Web を考察する上で、
重要なことを色々気づけると期待しているし、
成功すれば、 SlideStream 的なものが Stream ベースのアーキテクチャで簡単に作れるかもしれません。


まだまだ、実装はだせるところまで行ってないので、
これについては、また別で書きたいと思います。

conclusion or afterword

すっかり長くなってしまいましたが、
Node.js と Socket.IO を用いた SlideStream の実装から得られた、
「"リアルタイム Web" に関するプラクティス」のアウトプットでした。
Node.js と Socket.IO から Stream.IO に至まで、
今年1年間取り組んできたことを棚卸しする良い機会になったので個人的にはよかったです。


また、今回の考察には、多分に議論の余地が残っているし、色々間違えもあると思います。
指摘、コメントなどなるべくフィードバックをもらえると、自分も先に進む糧になると思うので、できればお願いしたいです。


そしてこうした記事が、まだまだ発展途上な 「リアルタイム Web」 について、
議論して行くたたき台となればと思います。

Stream.IO というものを作ってます。

追記

12/5/5
Stream, EventEmitter などを移植して chat サンプル動きました。Jxck's OutPut - Stream.io の example として Stream だけの Chat #nodejs_jp

本文

この記事は、JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース) とは関係ありませんw


ただ、前回そのアドベントカレンダーで書いた Node.js の Stream API で「データの流れ」を扱う方法 - Block Rockin’ Codes で扱った Node.js の Stream API について、そこからも色々考える機会がありました。


その結果、思ってたアイデアがあったので、形にしてみようと思い、ごそごそやり始めました。
(お前、そんなことやってる場合か?という異論は、う、、受け付け、、ごめんなさいorz)

イメージシート

リポジトリ

まだサンプルしかありません。
https://github.com/Jxck/stream.io/

Stream.IO のモチベーション

要するに Web アプリにおける Server と Client を Stream で抽象化してしまおうということです。
すると、リアルタイムなデータのやりとりを、全て pipe() で繋ぐだけでできるようになります。

Client の Stream 化

例えば、サーバでファイルを読み込みながら、それを全てクライアントに WebSocket で送るような場合。
(イメージシートの上段)

  • Server = Readable Stream
  • Client = Writable Stream

的な。

//  server side
var client = stream.io.client();
var stream = fs.createStream(/* ... */);

stream.pipe(client);
// client side
var server = stream.io.server();
var output = $('impliment by jQuery etc..');

server.pipe(output);

これで済むようなイメージ。
クライアントと接続して、 emit(), on('data') していた部分が抽象化されます。

一応さっき作ってみたけど、こんな感じ。
https://github.com/Jxck/stream.io/tree/master/example/logstream
(注: sample.log という大きめのファイルを自分で用意してください。大きかったからコミットしなかった)

Bi-side の Stream 化

これはいわゆる Chat の例です。(イメージシートの下段)

  • Server = Duplex Stream
  • Client = Duplex Stream

かな?

//  server side
var client = stream.io.client();
client.pipe(client);
var server = stream.io.server();
var output = $('impliment by jQuery etc..');
var input = $('impliment by jQuery etc..');

input.pipe(server);
server.pipe(output);


こっちは、後述する理由でまだできていない。
https://github.com/Jxck/stream.io/tree/master/example/chat

MiddleWareFilter

Stream なので、いわゆる Express などで使われていた middleware は、
readable かつ writable な Filter になります。
Filter は API がわかってるので、みんが作って行くでしょう。
それを活用できます。


Stream 経由で読み込んだファイルを、行ごとに直すフィルターがこんな感じ。
https://github.com/Jxck/stream.io/blob/master/example/logstream/readLineFilter.js


これを使うと、ファイルから読み込んでクライアントに流す時、以下のように行単位に出来る。

readable.pipe(readline).pipe(client);

TODO & 課題

まず、 chat の方が出来ていない理由として、

  • ブラウザに Stream がない
  • ブラウザに EventEmitter がない

だけど、 EventEmitter はありました。
https://github.com/Wolfy87/EventEmitter

なので、まずこれを使って、 Stream の移植かな。

あとは、まだまだ思いついただけなので(npm は押さえたけど)、
とりあえずサンプルを作りながら、何が必要かと、
どういう API がいいかを煮詰めて行きたいです。

Stream ベースのリアルタイムアプリケーション

これは、最近自分の中では重要なキーワードです。
Stream.IO は作っている中で得られるものが多そうなので、
まああまりうまくいかなくても、ちょこちょこやって行きたいと思います。
そこで得られたことも、ここに書いていきたいと思います。

Node.js の Stream API で「データの流れ」を扱う方法

追記

11/12/6
少し誤字脱字を修正、加筆
11/12/7
koichik さんにコメントで頂いたリンクと、その内容について追記
11/12/7
edvakf さんに頂いた指摘を修正

本文

この記事は、JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース) の 4 日目の記事です。


Node.js には Stream という API があります。
Stream はとても重要な技術で、

「Stream を制するものは、 Node.js を制す」

と言っても過言ではありません。

実際、 Stream は Node.js が得意とする I/O の部分を使いこなすために、
押さえておくべき技術なので、今回はこの Stream について紹介したいと思います。

参考
Jxck's OutPut - Node.js の Stream

I/O のおさらい+α

まず I/O について簡単におさらいします。
例えば、ファイルを読む場合、

同期 I/O なプラットフォームでは、大抵以下のようになるでしょう。
(このやり方しかできないというわけではありません、比較として)

data = File.read('path/to/file')
print data

一方非同期 I/O の例として、 Node.js だとコールバックを渡して以下のようになります。

readFile('path/to/file', function(data) {
  console.log(data);
});

非同期なので、I/O の終了を待っている間は、別の処理を行うことができます。
しかし、上のいずれも結果的には「読み込んだファイルの中身全部」をまとめて扱っていることがわかるでしょう。


Node.js ではこの I/O 結果に対する処理を、「全部まとめて」ではなく「破片を読み込むごと」に扱うことができます。
具体的には以下のようになります。

var readableStream = fs.createReadStream('path/to/file', {encoding: 'utf-8', bufferSize: 1});
readableStream.on('data', function(data) {
  console.log(data);
});
readableStream.on('end', function() {
  console.log('end');
});

data イベントが発生するごとに、データの破片がコールバックに渡されます。
この破片は chunk と呼ばれます。

この chunk は、例えば読む対象がテキストファイルだからといって、「一行ごと」になったりはしません。
基本的にはサイズを想定せず、行単位で扱うなら、一旦ためてから改行コードを見るなどの処理が、コールバックの中で必要になります。

ちょっと面倒に思えるかもしれませんが、これが Stream 。上の例は、 ReadableStream の例です。

Stream とは

Stream は「データの流れ」を抽象化するためのインタフェース。というような位置づけになります。
EventEmitter を継承し、読み込み用の ReadableStream と、書き込み用の WritableStream があります。

ちなみに process.stdin(標準入力), process.stdout(標準出力) もそれぞれ、 ReadableStream と WritableStream です。


インタフェースなので、 Stream が実装すべきイベント、メソッドなどは決められており、マニュアルに記載されています。


http://nodejs.jp/nodejs.org_ja/docs/v0.6/api/all.html#streams


今回使う、一部の最低限重要と思うものを挙げます。
説明は砕いて書くので、正確な定義はマニュアルを参照してください。

Event of ReadableStream
'data'
読み込んだデータの発生
'end'
読み/書き込みデータの終了
Method of ReadableStream
resume
'data'イベントの到着を開始/再開
pause
'data'イベントの到着を中断
pipe
ReadableStreamの結果をWritableStreamに繋ぐ
Event of WritableStream
'drain'
書き込みが再開可能
'pipe'
パイプされたことがわかる
Method of WritableStream
write
書き込む
end
溜まってるデータも全部書き出しておわり

pipe/util.pump

ある ReadableStream から読み込んだデータを、そのまま WritableStream に渡す例を考えます。
例えば、大きなファイルを読み込み、HTTP のレスポンスとして返すような場合です。
この場合は、ReadableStream でファイルの中身を chunk で読み込み、その chunk を WritableStream で Socket に書き込む感じです。


しかしこの例のように、デバイスの I/O 速度によって ReadableStream からの読み込みが、 WritableStream の書き込みよりも速い場合があります。
すると調節のために、書き込みが間に合わなかった場合に、読み込みを一旦止めるといった処理が必要になります。


この場合、

  1. バッファがいっぱいになった時、 WritableStream.write() が false を返す。
  2. ReadableStream.pause() で読み出しを一旦止めることができる。
  3. その後再び書き込みできる状態になったら、 WritableStream で drain イベントが起こる。
  4. ReadableStream.resume() は読み込みを(開始ではなく)「再開する」メソッドである。

ということをふまえて、以下のような感じで調整する必要が出ます。

// 読み込み開始(正確には止まってるから再開する)
readableStream.resume();
readableStream.on('data', function(data) {
  if (writableStream.write(data) === false) { // 書き込みがいっぱいいっぱいです
    // 一旦止める
    readableStream.pause();
  }
});
// 
writableStream.on('drain', function() { // 書き込み再開できます
  // 書き込み再開
  readableStream.resume();
});

こんな感じ。


しかしこうした処理は、 Stream 同士を繋いでデータを「流す」ような Node.js のアプリでは頻出します。
そこで、 ReadableStream には pipe() というメソッドがあり、引数に WritableStream を渡せば、上記のような調整を自動で行い、データの流れを上手く調整してくれます。


util.pump() というメソッドも同等なことをしてくれますが、これは将来廃止される予定なので、使わない方が良いでしょう。
ただし実装を読む分には参考になるので、紹介します。


Stream の実装

データを上手く「流れ」に乗せるために、何らかのデータ生成源を、Stream で抽象化したい場合があります。
Stream は先ほど言ったようにインタフェースなので、必要なメソッド等を実装すればよいことになります。

WritableStream

まず簡単な WritableStream を実装してみましょう。
ここでは、 process.stdin が ReadableStream であることを利用し、そこからデータを受け取って、まとめて出力する、簡単な MyStream を実装してみます。


色々はしょってやると以下の通り、 stream.Stream を util.inherits で継承して、必要なメソッドを実装するだけです。とりあえず write と end、あと writable プロパティあたりがあれば動きます。

var stream = require('stream')
  , util = require('util')
  , log = console.log.bind(console)
  ;

// 本来は 'drain','error','close','pipe' イベントが必要
function MyStream() {
  this.writable = true;
  this.buf = [];
}

// 継承、詳細は util.inherits を参照
util.inherits(MyStream, stream.Stream);

MyStream.prototype.write = function(data) {
  var data = data.toString().trim();
  log('write:', data);
  this.buf.push(data);
  return true;
};

MyStream.prototype.end = function(data) {
  log('end:', data);
  if (data) this.write(data);
  this.writable = false;
  log('\nresult:', this.buf.join(''));
};

MyStream.prototype.destroy = function() {};
MyStream.prototype.destroySoon = function() {};

module.exports = MyStream;

if (require.main === module) {
  var mystream = new MyStream();

  // 標準入力をパイプする
  process.stdin.pipe(mystream);
  // 読み込み開始
  process.stdin.resume();
}

実行してみます。実行したら、キーボードから文字を入力し、最後に ctl-D で止めます。

$ node mystream.js
a
write: a
b
write: b
c
write: c 
end: undefined // ctl-D

result: abc

ReadableStream

ここでキレイに ReadableStream の例が書ければ良いのですが、なんかあまりいいものがうかばず。。
とりあえず、カウントを走らせてそれを垂れ流す、 TimerStream なるものをでっち上げてみました。

var stream = require('stream')
  , util = require('util')
  , log = console.log.bind(console)
  ;

// 本来は 'data', 'end', 'error', 'close' イベントが必要
function TimerStream() {
  this.readable = true;
  this.t = 0;
  this.timer = null;
  this.piped = false;
}

// 継承、詳細は util.inherits を参照
util.inherits(TimerStream, stream.Stream);

TimerStream.prototype.resume = function() {
  this.timer = setInterval(function() {
    this.t++;
    if (this.t > 4) {
      return this.emit('end');
    }
    this.emit('data', this.t.toString());
  }.bind(this), 1000);
};

TimerStream.prototype.pause = function() {
  clearInterval(this.timer);
};

TimerStream.prototype.pipe = function(dest) {
  this.piped = true;

  // ここでは stream.Stream.prototype.pipe.apply(this, arguments); もok
  this.on('data', function(data) {
    dest.write(data);
  });
};

TimerStream.prototype.setEncoding = function(encoding) {};
TimerStream.prototype.destroy = function() {};
TimerStream.prototype.destroySoon = function() {};

module.exports = TimerStream;

if (require.main === module) {
  var timerStream = new TimerStream();
  timerStream.pipe(process.stdout);
  timerStream.resume();
}

実行すると時間が出力されます。終了は ctl-C

$ node timerstream.js
1234^C

デバッグ

途中で以下のような謎のエラーが出たんですが。。

$ node timerstream.js
Assertion failed: (Buffer::HasInstance(args[0])), function Write, file ../src/stream_wrap.cc, line 289.
zsh: abort      node 02.timerstream.js

stream_wrap.cc を読んだら、誤って buffer が渡っていたようです。
TimeStream.prototype.resume の中の emit の引数。

  - this.emit('data', this.t);
  + this.emit('data', this.t.toString());

このエラーは分かりにくすぎるだろさすがに。。

pipe でつなぐ

ためしに二つをつなぐならこんな感じ。
(end 周りが半端ですが)

var TimerStream = require('./timerstream')
  , MyStream = require('./mystream')
  , ts = new TimerStream()
  , ms = new MyStream()
  ;

// パイプで繋ぐ
ts.pipe(ms);
// 読み込みを開始
ts.resume();

実行は ctl-C で止めます。

$ node pipesample.js
write: 1
write: 2
write: 3

Stream の使い道

データの流れを上手いこと Stream に抽象化できれば、それを pipe で繋ぐだけでやり取りができます。
また、 writable かつ readable な Stream を実装することもできます。


たとえば、あるファイル A.js 内でデータが生成され、それを他のファイル B.js に渡したい場合、次々発生するデータは module.exports だけでは対応できません。
この場合 B.js から A.js に writable かつ readable な Stream を渡し(逆も可)、 A.js 内では発生したデータを write() でどんどん書き込む。B.js はそのストリーム経由でデータを受け取るといったことができます。もちろん B.js ではそれをまた pipe で繋ぐ、といったこともできます。
([追記] ここで言ってるのは、最後に紹介する Filter のパターンです。)


多分こんな感じ。(一度やろうと思ったけど、結局やらなかったので未検証)

// A.js (書き込む側)
var stream = require('B');
stream.write(data);
// もちろんここで pipe してもいい
// B.js (書き込ませたい側)
var stream = /*snip*/;
module.exports = stream;
stream.on('data', /*snip*/);


また、 File や Socket 系の主な API は、すでに Stream になってることが多いので、そのストリームをそのままやり取りすればいいです。
こうすると、普段何気なく使っていた fs や net、 http モジュールの取り回しの幅が広がるでしょう。
それ以外で生成したデータを Stream にしたい場合は、上で紹介したように自分で実装すれば良いでしょう。

Stream の今後

piscisaureus が書いた Node v0.8 roadmap には、 StreamAPI についての改善が上がっていました。
主に自分で Stream オブジェクトが定義しやすくなるようにするようです。担当は Isaac。

https://gist.github.com/1346745


しかし、 Google Group に Ryan があげたロードマップには、これがなくなっています。

https://groups.google.com/forum/#!topic/nodejs/eVBOYiI_O_A/discussion


つまり、内部的に話は上がっているけど、他が優先になってるのかな。

ちなみに TJ のインタビューでも、最後の方で Stream API について触れられています。

原文
http://www.infoq.com/articles/nodejs-in-action
翻訳
http://d.hatena.ne.jp/vwxyz/20111014/1318568665

また、コメントで頂いたリンクについては、最後に別途紹介させていただきます。

Stream ベースのアプリケーション

また、アプリケーション単位でもサーバを ReadableStream 、クライアントを WritableStream とみたてて(もちろん逆もあり)、今までのデータが「渡される」感じよりも、「流れている」感じを出して行くことが、リアルタイム Web な流れの中で一つのポイントになると考えます。


この時、 WebSocket といった通信方法以外に、この考えを取り込んだ Web アプリケーションのアーキテクチャの研究が必要です。
そこでは、 Bi-Side JavaScript (両方 JS で書ける)ということが、より有利に働く場面かも知れませんね。


もともと SocketStreamFlatIron がそうしたストリームベースな部分を意識して作られてはいるんですが、正直これらもまだ完成系とは言えず、この分野はまだまだ研究の余地が多分に残っていると思います。


Stream ベースなリアルタイムアプリ。夢は広がりますね。

補足

  • サンプルは API 通りに網羅した実装にはなってません。
  • 同じく、エラー処理も、まともな終了処理もしてません。
  • 今回は buffer の話は省略しました。
  • 本当は自作 Stream のコンストラクタ関数内で stream.Strem のコンストラクタ読んだ方がいい気がしなくもない。(今は影響無いはずだけど。)
  • Stream の独自実装については、この辺が参考になります。https://github.com/mikeal/morestreams
  • 今回使ったコードはこちらにあげてます。https://gist.github.com/1426284

追記

コメントで「Stream の今後に」と頂いたリンクですが、ここで紹介します。

Streams2

https://github.com/joyent/node/pull/1681

まずこちらは、 mikeal の pull request です。
内容は以下の三つです。

  • Make stream.Stream a read/write stream with proxy methods by default.
  • Make readable streams in lib use stream.ReadStream.
  • Add stream.createFilter.

要するに上述した「Stream を使いやすく」の具体的な提案です。
「master のテストが落ちてるから merge はおいといて、先に API について話そう」
ということで 2011/09 に始まりました。

途中、 isaacs, ry も参戦して、
「今はとにかく API の議論が大事だ。 node の今後を左右する」という勢いで、
慎重な議論が行われてますね。コンスタントに続いて 11 月頭に一旦落ち着いたようですが、
3日前にまだ投稿があるので、まだ続くのかもしれませんね。
それだけ、大事だということを物語っているととれると思います。

Spec for streams

https://gist.github.com/1241393

時期的には上の議論の初期に izaacs によって書かれたものです。
コードは無く API ベースの提案です。
メソッドをオーバーライドして独自 Stream を実装できるような、Stream の基本クラスを用意するさいの、仕様のドラフトです。
ここから実装される Stream として、今回紹介した ReadableStream と WritableStream 以外にあと二つ書かれています。
以下の二つはいずれも ReadableStream かつ WritableStream ですが、動きが少し違います。

Filter Streams
write() すると self.emit('data', ...) する感じ。
Duplex Streams
write() と self.emit('data') が独立して行われる感じ。


Filter は、write() がそのまま emit('data') するので、以下のように pipe の途中に挟む事ができます。
これにより RS から WS に渡るまでの間に色々な処理(暗号化, Gzip etc)を挟む事ができます。

RS.pipe(Filter).pipe(WS);

Duplex は、 write() でデータを受け取る事も、 emit('data') することもできるけど、二つには関連は無く独立しています。
代表例が Socket で、つまり相手に「データを送る(write)」も「データを受け取る(emit)」もできます

エコーサーバならこうなります。

net.createServer(function (socket) {
  socket.pipe(socket);
});

Filter を組み合わせると、例えば リクエストに対して何かしら処理をしてレスポンスを返すサーバは
(リクエストが全部揃ってなくてもレスポンスが始められるる感じならたぶん)
以下みたいにも書けるってことになるのかな。(未検証)

net.createServer(function (socket) {
  socket.pipe(requestHandleFilter).pipe(socket);
});


そして、これらに考えられる問題として、 pipe() の途中でエラーが出た時の処理についてが上がっています。

この辺が今後 Stream を扱い、 API を洗練していく過程で大事になってくるんだろうと思います。

参考

関連の issue など。


今自分が追えているのはこのくらいまで。
Stream はまだまだ奥が深いので、後は使いながら考えて行きたいと思います。


引き続き、指摘、質問、コメント歓迎です。

クライアントとサーバの両方で使える JS コードの書き方

追記

11/12/25
Bi ってそんなに一般的ではない、 Both-Sides JavaScript の方が、ということでまた変更しました。(side でなく side's')
11/12/04
Both Side JavaScript は変ということで、 BSJS=Bi-Side JavaScript に変更しました。

本文

CSJS と SSJS で両方同じ言語で処理が書けるメリットの 1 つとして、
書いた処理の共有があげられます。
(そこにメリットを感じない人もいるかも知れませんが。)

例えば

  • Validater を共有
  • クライアントの状態をサーバで再現

などがあります。前者はそのままですね。
受け取った入力のバリデーションはサーバでは必須で、フィードバックを速くするためにクライアントでも同じように行う場合があります。
今まではサーバで書いたバリデーションと同等のものを JS に直していたでしょう、サーバもJSならそのままです。

後者はゲームのチート対策などで、送られてくるメッセージからクライアントの状態をサーバで再現して、
ユーザからあり得ない情報が送られてないか(ズルしてないか)を確認したりできるそうです。
今までは別の言語で同じロジックを組んでいたところ、クライアントのロジックがそのままサーバで動けば少なからず嬉しいと思います。


他にもいくつかの場面で有効かもしれません。


今回はそんな Both-Sides JavaScript の書き方と、テストのコツを紹介します。


本文中のエイリアス

CSJS(Client Side JS)
ブラウザで動くJS
SSJS(Server Side JS)
Node.js で動くJS
BSJS(Both-Sides JS)
その両方で動くJS

という前提で進めます。

題材としては、google-diff-match-patch - Diff, Match and Patch libraries for Plain Text - Google Project Hosting を使って一方がパッチを作って送り、
他方がパッチを受け取って文字列を更新する、といった用途のためのちょっとしたラッパー関数を作って、それを両サイドでテストします。
そのため、ここで作成する JavaScript は DOM に一切依存しないものとします。

基本方針

SSJS と CSJS では、それぞれスクリプトの読み込み方と変数空間が少し違います。
ブラウザではスクリプトはファイルごとの変数空間を持たないため、
自分より先に読み込まれたファイルの変数にアクセスできます。
一方 Node.js では、スクリプトはファイル毎の変数空間を持ち、
それを export/require を使って公開/読み込みする機能を持ちます。

BSJS を実現するには、この二つの差を吸収してやる必要があります。

export

では実際にモジュールを書いてみましょう。
まず、あるモジュールを以下のように書きます。

/**
 * diff_launch.js
 */
var dmp = new diff_match_patch();

function make_patch(old_text, new_text) {
  /* snip */
}

function apply_patch(origin, patch) {
  /* snip */
}

このモジュールは二つの関数を持ち、これを外部に公開します。
公開する場合は、 node.js であれば module.exports などを用いるのですが、
module オブジェクトはブラウザにはありません。
しかし、そこで node.js では module.exports === exports === this であることを使います。
(実際は exports はグローバルではなく、モジュールごとのローカルです。詳細はドキュメントを参照。)

this['make_patch'] = make_patch;
this['apply_patch'] = apply_patch;

こうすると、ブラウザの場合は window オブジェクトに対して関数を加えた形になり、
他のスクリプトファイルからも参照することが可能になります。
名前空間を切りたいなら、公開する前にオブジェクトのメンバにすれば良いでしょう。

require

先ほど作成した diff_launch.js は、 diff_match_patch.js に依存しているため、これを解決する必要があります。

ブラウザの場合は単に依存するスクリプトファイルが、自分よりも先に読み込まれていれば良いだけなので、そのように読み込みます。

<script src="diff_match_patch.js"></script>
<script src="diff_launch.js"></script>

一方、 node.js の場合は require() で読み込んでやる必要があります。
しかし、標準では require() はブラウザには有りません。
そこでこれは、「ブラウザではない場合」を想定して以下のような場合分けをする必要があります。

/**
 * diff_launch.js
 */
if (typeof window === 'undefined') {
  var diff_match_patch = require('./diff_match_patch').diff_match_patch;
}

見ての通り、 window オブジェクトが無いことを条件にしています。
しかし、これは「ブラウザ or Not」の条件分岐でしか有りませんので、
もしブラウザと Node.js 以外にも選択肢があり、それが Node.js の require と互換性が無い場合は、
適切な条件などを加える必要があります。

もしくは、全ての環境に互換性のある require() を用意するような方法が必要でしょう。
しかし、今の状況では上の方法が一番手をかけずに要件を満たすと思うので、この方針でいきます。

テストの共有

テストは、テスト対象のスクリプトとは別に用意し、
テスト対象のスクリプトを読み込んで、テストを実行するのが基本です。

BSJS なスクリプトをテストするためには、両環境で動くことを確認するため、テストも単一のものが両環境で実行できることが望ましいです。
これを実現するためには、先ほどの方針と同様、実行された環境に応じて、対象のスクリプトの読み込み方法を変えます。

また、テスティングフレームワーク自体も両環境の実行に対応したものが良いでしょう。
最近はこの用途でのテスティングフレームワークを探していました。
現状では、

  • NodeUnit
  • Jasmine と jasmine-node
  • QunitQunit-tap

などの選択肢があります。


これらのテスティングフレームワークの詳細な比較はまた別で書きたいと思うので、今回は省略します。
自分は今のところ、jasmine がこの用途で一番実装が充実し、使いやすいかと思っています。


今回は Jasmine を使い、BSJS のテストという視点で、
テストを書いてみます。

サーバでテストする場合

ターゲットを読み込み、それに対する検証を記述します。
jasmine-node はコマンドを提供するため、 jasmin-node を require する必要はありません。

/**
 * diff_launch.test.js
 */
if (typeof window === 'undefined') {
  // for jasmine-node
  var make_patch = require('../lib/diff_launch').make_patch;
  var apply_patch = require('../lib/diff_launch').apply_patch;
}

検証は describe(), it(), expect() 等を用いて普通に書きます。

実行は jasmine-node コマンドを使います。

$ jasmine-node diff_launch.test.js
Started
......

Finished in 0.007 seconds
3 tests, 6 assertions, 0 failures
クライアントでテストする場合

jasmine は本来はブラウザでのテストが前提(jasmine-node が後発という意味)なので、
専用の html を用意し、中で必要な CSS, JS を読み込みます。

  • テスティングフレームワークのJS(jasmine.js, jasmine-html.js)
  • テスト対象のテストが依存するJS(diff_match_patch.js)
  • テスト対象のJS(diff_launch.js)


describe(), it(), expect() 等はそのまま使えます。
つまり、サーバのテストで使ったコードは全くそのまま使えるということです。

  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.2/jasmine.css">
  <script type="text/javascript" src="lib/jasmine-1.0.2/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-1.0.2/jasmine-html.js"></script>

  <!-- include source files here... -->
  <script type="text/javascript" src="../public/javascripts/diff_match_patch.js"></script>
  <script type="text/javascript" src="../lib/diff_launch.js"></script>

  <!-- include spec files here... -->
  <script type="text/javascript" src="diff_launch.test.js"></script>

そして、html 用のスクリプトを少し足して完了。

<script type="text/javascript">
  jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
  jasmine.getEnv().execute();
</script>

ブラウザで実行するとこんな感じ。


TODO: スクショ


どちらの環境でも同じようにスクリプトを読み込み、
テストを実行することができました。

isomorphic なコード

BothSideJavaScript であるということを積極的にメリットとしてとらえる動きはあり、
SocketStream でもそうした機能を持っています。

また、Web 全体で見ても従来のサーバサイド MVC のモデルでは、 View がかなり柔軟かつリッチなるため、
クライアントサイドに MVC を持ち込む Backbone.js のようなものもあります。

そして、このふたつをより密接に関連づけた上で、アプリケーション全体のアーキテクチャの再考を計った結果が
下記のエントリに詳しく書かれています。


Scaling Isomorphic Javascript Code — blog.nodejitsu.com


ここでは、BSJS は "isomorphic=同形" なコードと表現されています。

この考えをもとにした実装として Nodejitsu が発表したのが、


Flatiron です。


エントリは

Introducing Flatiron — blog.nodejitsu.com


Node.js がもつ「非同期/ノンブロック」や「シングルプロセス/クラスタ」などの機能や特徴以外に、
こうした BSJS であることをもっと有効に使った考え方も、今後大事になるかなと思います。
そして、 flatiron は玄人向けモジュールの老舗である Nodejitsu の集大成な感じで、面白そうです。

サンプルソース

サンプルはソースはこんな感じです。
(他から切り出したものです)

https://github.com/Jxck/diff_lunch

東京Node学園祭2011

遅くなってしまいましたが、 10/29(土) に「東京Node学園祭2011」のスタッフとして運営と発表をさせいていただきました。

東京Node学園祭

自分の発表は、初心者向けセッションとして、Express と Socket.IO を用いた簡単なアプリの作成をライブコーディングで行うという内容で行わせていただきました。

スライドも多少工夫したんですが、その後に「ウィザード級ハッカー」と称された Guille の素晴らしいセッションの前ではただの子供騙しでしたね、世界の凄さを痛感しました。

当日使用したスライドは下記です。当日使用した HTML には後で読めるようにデモの部分を埋め込んであります。
同じものを SlideShare にもあげています。


今回は、Nodejs_jp としても初めての大規模イベントだったので、運営も色々行き渡らないところがあったかもしれませんが、参加された方に少しでもアップデートがあったなら良かったと思います。

個人的には、 Ryan や Guille と交流が持てたり、自分のセッションでちょっとした挑戦をしたり、後夜祭で色々な方とお話しできたりと、とても得る事が多かったです。

また、今回のスライドツールを作る上で、普段使わない API を使ってみたり、 FirstServer さんに協力頂いて当日の負荷対策ができたりなどとても勉強になりました。

学祭自体には、「チケットが少ない」、「企業色が強すぎるのでは?」などの意見があったことは把握しています。
もし来年も同じように実施できるのであれば、改善できる点は改善していきたいと思います。


ただ自分は Ryan に
「カンファレンスは成功だったと思うか?」

と聞かれた時、
「もちろんだよ!大成功だ!」


と答えました。別に彼を立てる気はなく。そう思ったからです。


素晴らしいスタッフの方々と一緒にやらせていただいて、とても勉強になったし楽しかったです。

来年もまた、ぜひ開催できたらと思います。


このすばらしいカンファレンスに関われたことを誇りに思います。
来てくださったみなさん、支えてくださったみなさん、本当にありがとうございました。

追記

@ が気を利かせて翻訳してくれた。ありがとう Jed。 https://gist.github.com/1330868

Socket.IOv0.8.4 のソースを読んでわかった次期新機能先取り

本文

Socket.IO のソースを眺めていると色々面白いことがわかります。
master がガンガン新機能を実装していて、現時点でもアンドキュメントな API もいくつかあるし、結構ソースは読みやすい方なので勉強になります。

で、最近また最新のを読んでたら色々面白そうな機能があって、「あるんならちゃんとドキュメントに書いてよ。。」などと思ってたら、ついさっきコミッタの 3rdEden がこのスライドを公開してました。

http://www.slideshare.net/3rdEden/going-real-time-with-socketio


まだ未発表の新機能だっただけのようです(でもそういう機能も master にばっちりあるんだな。develop ブランチ一応あるんだけどw)。
おそらく次のバージョンアップは、また割と大きくなると思います。折角なので手元のメモを公開して、今のうちに少し先取りしてみようかと思います。

注意

  • Socket.IO@0.8.4 を読んでます。
  • 0.9で正式にアナウンスされると思うので、そしたらこの記事は止めて改めてそっちを書くつもりです。
  • ソース見ながら見つけただけで未検証です。
  • master に入ってるし、 npm で入れても v0.8.4 が入るので、手元で試せるはずです。

Configure

機能が増えればその設定が増えます。
設定項目とデフォルト値は以下、色々増えてますね。

  • origins: '*:*'
  • log: true
  • store: new MemoryStore
  • logger: new Logger
  • static: new Static(this)
  • heartbeats: true
  • resource: '/socket.io'
  • transports: defaultTransports
  • authorization: false
  • 'log level': 3
  • 'close timeout': 25
  • 'heartbeat timeout': 15
  • 'heartbeat interval': 20
  • 'polling duration': 20
  • 'flash policy server': true
  • 'flash policy port': 10843
  • 'destroy upgrade': true
  • 'browser client': true
  • 'browser client cache': true
  • 'browser client minification': false
  • 'browser client etag': false
  • 'browser client gzip': false
  • 'browser client handler': false
  • 'client store expiration': 15

Store

まず、大きいのは Store だと思います。
設定では MemoryStore になってます。
この MemoryStore は Connect のものかと思ったら独自実装でした。

大きいのは、この Store のインスタンスは、 Publish(), Subscribe() のインターフェースを実装することになっていて、
Socket.IO はメッセージを内部で Publish/Subscribe してから broadcast 等を行うようになってます。

で、デフォルトでは MemoryStore と Redis が選べるようになっていて、
MemoryStore の場合は Publish も Subscribe も実装がつぶされていて実際にはただのメソッド呼び出しになっている、
なのに対して、 Redis を選べばちゃんと Redis の Pub/Sub のラッパーになっている。

まとめると

  • MemoryStore

-- デフォルト準備要らず
-- スタンドアローンのローカルでのみ動く、スケールしない。
-- 要するに開発用

  • RedisStore

-- 要 Redis サーバ
-- Redis サーバ共有すれば簡単にスケール


という今まで独自でやっていた事が簡単にできるようになりそう。
また、インタフェースを実装すれば、他のストレージを指す事もできるはず。


これはヤバい。ちなみに実装は「まだ半分くらいまで」らしいです。期待!


データの紐づけ

Socket.IO API 解説 - Block Rockin’ Codes のデータの紐付けで書いたデータは、上述の store に入るよう。だからただスクリプトレベルでグローバルとかに代入した変数とちがって、分散サーバ間でちゃんと共有できる。

そして、 get(), set() 以外に has(), del() の実装を確認。
スケールアウトに結構まじめに取り組んでますね。


Static

/socket.io/hoge のように Socket.IO のサーバから配信すべきクライアントの js やら flash まわりのやつやらがごっそり変わってる。
まず、キャッシュ周りの設定や、 gzip が増えてる。
gzip は実際は node ではなく child-process 経由でローカルサーバの gzip コマンド叩いてるだけだから入ってなかったらならない。

  • 'browser client cache': true
  • 'browser client minification': false
  • 'browser client etag': false
  • 'browser client gzip': false


また、

 Regexp for matching custom transport patterns. Users can configure their own
 socket.io bundle based on the url structure. Different transport names are
 concatinated using the `+` char. /socket.io/socket.io+websocket.js should
 create a bundle that only contains support for the websocket.

とあって、スライドには実例として

  • '/socket.io/socket.io+websocket.js'
  • '/socket.io/socket.io+websocket.js+htmlfile.js'

みたいにすると websocket のみサポートするクライアントなど、URLからクライアントを動的にカスタムビルドできるみたい。
このビルドは node で行われてるな。イニシャルリクエスとだけだろうけどオーバーヘッドとか気になるところ。

ちなみに通信方法が変わったらクライアントのキャッシュは削除されて新しくキャッシュされるっぽい。

HTTPS

今までできたっけ?スタンドアローンサーバに key を渡せば、
HTTPS サーバで立てる事ができる。

  listen(80, {key:***}, function() {
    /* snip */
  });


ところで Socket.IO サーバをスタンドアローンで立てられるようになってからなのかわからないけど、
もし Express みたいな HTTPServer のインスタンスを渡したら、内部では一旦そっちのサーバに実装されたルーティングハンドラを全部奪って Socket.IO サーバが関心のあるリクエスとじゃなかった場合だけもとのサーバに処理されるリクエスとを改めて投げる実装になっているようだ。
ちょっと無駄な気がするのは気のせいかな?

逆に Socket.IO がわから HTTP へのリクエスとを渡す前に色々挟む余裕も有るのかも。(誰得だけど)

ちなみにサーバは必ずデフォルトレスポンスで

  res.writeHead(200);
  res.end('Welcome to socket.io.');

があって。これはサーバが起動してるかのテストに使えたりするよう。

というかもはやクロスオリジンなんだし、 Socke.IO サーバと普通の HTTP サーバ共存させる必要もないかとは思う。

Join

これはスライドにもまだなくて、 ML に流れてたのを @meso さんに教えてもらってから未だにアップデートがないんだけど、ネームスペースに関する結構便利そうな新機能。

まず `join()` を使うとネームスペースに Join できるらしい。

socket.join('room1', function(){
  /* snip */
});


出るのが `leave()`

socket.leave('room1', function(){
  /* snip */
});

メモ

  • hybi07, hybi10 サポート
  • flashsocket のポートが 843 から 10843 に変わってる。これで sudo いらないってことか。
  • send() の下に emit() があるんだけど、実はもう一個ローレベル API として packet() ってのがあって、これが実際は送信している。
  • test/common.js を盗めば大抵のテストが可能っぽい。それもそのうち書こう。

まとめ

かなり精力的で頼もしい。特に最近は Chrome とかの WebSocket の仕様が変更されたり、その Chrome が知らぬまで勝手にアップデートしてたりで色々あったけど、 Socket.IO の対応も早いので最新はわりといける。逆にブラウザも Socket.IO も両方最新じゃないと面倒だったりする。

とにかくまだまだ進化は続くようです。今もう v0.8 だからそろそろ v1.0 とかも視野に入れた話になってくるのかな。

東京Node学園祭2011 ではその辺の話も聞けるといいですね。

node.js の modules wiki を見るときの便利ブックマークレット

表題の通り、Modules · joyent/node Wiki · GitHub を見るときに、便利(主に自分が) になるブックマークレットを書いてみました。

本文

Node.js のモジュールを探す方法としては大きく二つあって、

の二つがあります。(ローカルの npm コマンドでも見れるけど)


後者は npm に登録されているモジュールが検索できるので良いのですが、
Node.js の本家の Wiki にあるだけあって未だに前者のページもよく参照される気がします。


しかし、ここはあくまでも Wiki なので誰でも自分で作ったモジュールを好きに登録する事が出来ます。
そして自分で消しでもしない限り、基本はずっと残ります。


一方で、このページを見てわかる通り一応カテゴライズされていて、同じカテゴリや同じ目的のモジュールが、
結構乱立していたりします。(例えば MySQL のところには 10個くらい登録されています。)


その中から、どれが良さそうか探すには、リンクをたどって API を見たり、実際使ってみたり等有するんですが、
そもそも長い事メンテナンスされてなければフォークする覚悟でもない限り、あまり手を出したくないものです。


あと、そのモジュールが人気があるのかどうかは、 Watch や Fork の数も参考に成ると思います。


ということで、このブックマークレットを使うと、このページのモジュール名の横にそのモジュールのリポジトリのステータスを表示することができ、モジュール探索の参考になるかと思います。

ブックマークレット

ブックマークレットってどうやって貼れば良いの。。?

javascript:for(var i=0;;){var today=new Date,a=$("div#template li a")[i],$a=$(a);i++;if(a.rel!=="nofollow"&&a.href.match(/^https:\/\/github.com\/.*/)&&!$a.html().match(/^<del>.*/)){var href=a.href;$.ajax({url:href,cache:!1,success:function(c){var b=$(c),c=b.find("div.authorship time.js-relative-date").text();today-new Date(c)<2592E6&&(c='<strong style="color:blue">'+c+"</strong>");b=b.find("ul.repo-stats");b=$(b);b.prepend($("<li>").html(c));b.find(".watchers a").text()>100&&b.find(".watchers a").wrap("<strong>").css("color","blue");b.find(".forks a").text()>20&&b.find(".forks a").wrap("<strong>").css("color","blue");c=this.url.split("?")[0];$('a[href="'+c+'"]').after(b)},error:function(){return!1}});if($a.text()==="node-clucene")break}};
注意点あるいは仕様
  • 1ヶ月以内に更新されてれば強調(青)
  • Watcher が 100 人以上いれば強調(青)
  • Forks が 20人以上いれば強調(青)
  • Github 以外のリポジトリは無視
  • master のステータスしか見てない
    • というか Ajax でページ取ってきてるだけなので。
  • 現状は全部いっぺんに取りに行くので最初ちょっと重いです。
  • ついカッとなって書いたのでソースは決してきれいではないです。。
  • Fixjsstyle => Closure Compiler
  • Wiki は id がほとんど振ってないので DOM の特定が超面倒くさい。。
    • ので本来の目的に差し障り無い挙動は無視
  • 要望は Gist にコメントもらえれば気分次第で答えます。
  • Fork is wellcome!!