Socket.IO 1.0の紹介 (翻訳)

Socket.IO 1.0がリリースアウトされました。Socket.IO v1.0が出るといわれてから一年半以上経過しましたが、やっと出ました。Node.jsに関わる方であれば一度はお世話になっていると思います。今回はSocket.IO 1.0の紹介を作者であるGuillermo Rauchがしているので、それを日本語で翻訳して紹介します。

TL;DR

翻訳していたらすごく面白かったのですが、文字だらけですごく長くなってしまったので、最初と最後にまとめを載せました。興味があれば全部目を通してみてください。

  • モジュール分割が進み、新しくEngine.IOが作られています。これはSocket.IOのトランスポート層プロトコルを調律する役割を担っているライブラリです。
  • Engine.IOが行っている処理の一つで、最初に接続できる可能性が高いXHRやJSONPで確立し、websocketにupgradeするという方式を採用しています。これにより、従来のtimeoutによるfallback形式よりも初期接続の高速化が見込まれます
  • バイナリを送信することが可能になりました、これにより、画像のようなテキストじゃない形式のデータもemit可能になります。
  • socket.io-redisでのスケーラビリティを確保するための方式がドキュメントになりました。今までもredisを使った色んなhackがあったんですが、きちんとやり方が掲載されました。
  • socket.io-emitterを利用することでバックエンドのサーバーからsocket.ioに対してイベントを発行できるように成りました。socket.io-emitterのPHPバインドを利用すれば、PHPのサーバからSocket.IOにイベントを送ることができるようになり、バックエンドとの通信が簡単になりました。

上述した話が機能や性能として大きく変わった事で、他にもテスト効率化させる改善を導入していたり、コンソールへのログ出力方針が変わったりしています。

それでは紹介していきます。

Socket.IO 1.0の紹介

Socket.IOのfirst versionはNode.JSが初めて登場した時から作られました。私は長い間、サーバーからクライアントへのデータプッシュを簡単に実現するためのフレームワークを探しており、サーバーサイドJavaScriptに対して色んなアプローチを試してきました。

当時、標準化プロセスの真っ最中だったWebSocket APIに対応するインタフェースは注目を集めていました。私は(Socket.IOに関して)Node.JSのcreatorを含む多くのコミュニティからたくさんのフィードバックを受けることができて非常に幸運でした。

Socket.IOはこのようにしてweb上でのEventEmitterになってきました。今日、私は(Socket.IOが)1.0になるまでに行ったことに関しての紹介をします。

Socket.IO 1.0に関して行ったことは沢山ありますが、読者の方々に時間がないなら興味のあるパートまで飛んでしまって構いません。

New engine (新しいengineの導入)

Socket.IOのコードベースはもはやtransport層とブラウザの互換性を調整するだけの役割しかありません。今までやっていた処理は WebSocket-likeなAPIを実装したEngine.IOと呼ばれる新しいモジュールに委譲しています。


このモジュール分割の利点は非常に大きいです。

  • Socket.IOエンドユーザーにとっては、ほぼ変更はありません。新しいバージョンに変更するだけです!
  • コードベースの行数とtest面に対して、かなり単純化が進みました。
    • Socket.IO サーバーは1234行しかありません。
    • Socket.IO クライアントは976行しかありません。
  • 将来に渡って使い続けられる柔軟性
    • もしWebSocketだけがあなたが将来サポートしたい唯一のtransport層のプロトコルになったとしたら、(多くのbrowser hackやworkaroundを保持している)Engine.IOは削除することができるようになるでしょう。
    • 将来的にNode.JSのpure TCP socketやGoogle Chrome Socketsといった代替可能なtransportプロトコルがEngine.IOに実装される可能性もあるでしょう。

昔のwebではUser Agentを単純に見て、使えるAPIや有効にできる振る舞いを決めていました。信頼性を最大化させるためには予期された振る舞いかどうかを確認するためにAPIを直接テストし、振る舞いを明らかにしていくことが必要でした。それがJavaScriptのコードベースをより複雑に、かつ成熟させていきました。

例えば、JSON が global に存在するかどうかのチェックはJSONが存在するかどうかだけではなく、JSON.stringifyが動くかまでチェックしないと意味がありませんでした。ユーザーは自身のglobal空間に独自のJSONを定義し、実装を壊すことができてしまうという事を意味します。

Socket.IOはWebSocketが動作するということを想定していません。動作しないことが実際にはよくあるからです。WebSocketの代わりに、(接続する可能性が高い)XHRやJSONPで正しく接続を確立させ、それからWebSocketの接続にupgradeを試みます。timeoutによるfallbackと比較して、エンドユーザーの体験が落ちることは全く無いでしょう。

Binary support (バイナリサポート)

WebSocketがbinary dataの送信をサポートしてから、しばらくの間、Socket.IOのユーザーからbinary dataを送信する事ができるのかといった質問が寄せられてました。

主な問題はもし私達がWebSocket APIの後で無理矢理binary通信をサポートした場合、その有用性はほとんど限られてしまうだろうという事でした。WebSocketは通信相手に対してSocketがstring modeなのか binary modeなのかを選択させることを要求します。

var socket = new WebSocket('ws://localhost');
socket.binaryType = 'arraybuffer';
socket.send(new ArrayBuffer);

low-levelなAPIとしては良いです。Engine.IOでbinaryTypeもサポートしています。ただしアプリケーションデベロッパーはblobsしか送りたくないとか、もしくはデータを送る前に全てをblobとしてencodeしてから送信したいというユースケースが想定できます。

Socket.IO は Buffer (Node.jsのBuffer)やBlob, ArrayBuffer や Fileといったデータ構造のemitをサポートしました。

var fs = require('fs');
var io = require('socket.io')(3000);
io.on('connection', function(socket){
  fs.readFile('image.png', function(err, buf){
    // it's possible to embed binary data
    // within arbitrarily-complex objects
    socket.emit('image', { image: true, buffer: buf });
  });
});

binaryをサポートしたことでの効果を検証するために、Twitch Plays Pokemonを100% JavaScriptで複製しようと決めました。JavaScript gameboy emulatornode-canvassocket.ioを使ってサーバーでレンダリングした協力型のゲームを完成させました。これにより、IE8でも動作可能です。

http://weplay.io

画像データをこんな感じで送っています:

self.canvas.toBuffer(function(err, buf){
  if (err) throw err;
  io.emit('frame', buf);
});

次の実験は栄誉ある引退をした Windows XP のイメージをQEMUインスタンスで再現することでした。各playerは15秒間だけマシンのコントロールをすることができます。デモはこちらです。そしてこれが典型的なシナリオの動画です。

このデモのキーとなる部分は、QEMU VNC serverに接続し、RFB protocolを再現しているところです。Node.jsでの場合は、その解決策はnpm search rfbとするだけで済みました。

特に、レイテンシを最小化し、最高のパフォーマンスを出すために、変更されたスクリーンの一部だけをクライアントに通知するのがベストでした。例えば、マウスを移動させた場合、カーソルの周囲のスクリーンだけをブロードキャストすれば済みます。node-rfb2のモジュールは以下のようなオブジェクトを持って変更があったrectイベントを通知してくれます。

{
  x: 103,
  y: 150,
  width: 200,
  height: 250,
  data: Buffer
}

結果、binary dataをサポートすることが非常に有用であることが証明できたため、私としてはすごくクリアになりました。結局私がした事はio.emitを呼び出してobjectを送信するだけで、後はSocket.IOに残りを任せられました。

これは単なる趣味ですが、私の好きなFPSゲームをインストールして実行しているビデオです:

Automated Testing (ブラウザテスト自動化)

Socket.IOに対しての各コミットはAndroidiOSも含めた25のブラウザでmatrixにテストが実行されます。
(訳者注:よく見ると分かりますが、このautomationはzuul使ってますね。)

これはマシンに一時的なポートを開けてリバーストンネルを設定した上で外からアクセスできるようにしてシームレスなテストを行えるようにしています。実際にはSauce Labs クラウド上でテストが走る形になります。仮想化された全ブラウザの環境で実行されます。


Scalability (スケーラビリティ)

私達はroomや複数nodeサーバーでのscalabilityに対してのアプローチを驚くほど単純化しました。node間でデータの保存やレプリケーションをする代わりに、Socket.IOは周囲にイベントを発行する事だけに専念しています。

もしSocket.IOを複数Nodeでスケールしたいなら、2ステップでシンプルに実現できます:

  1. Sticky load balancing( 例えばIPアドレスによるbalancing)を有効にします*1。こうすることで、例えばlong-pollingの接続時にリクエストを確認し、メッセージのバッファの保存先が常に同じnodeに向くようにしています。
  2. socket.io-redisのアダプタを実装します。
var io = require('socket.io')(3000);
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));

パケットはエンコードされて、他のNodeに分散されて、いつでもブロードキャストすることができます、storageに対して処理を入れる必要はありません*2

この方式を取ることで、他のバックエンドと統合する、という次の節に書かれた目標を捉えやすくすることができます。

Integration (バックエンドとの統合)

Node.jsに制限されず、既存のアプリケーションは様々な言語やフレームワークで作られている事が多いです。Node.js で全てが作られていたとしても、いくつかのポイントにおいて、別プロセスでアプリケーションを実行し、分割したいと思う事もあるでしょう。

例えばプロセスの一つはSocket.IOサーバーをホスティングし、接続を許可し、認証を実行するといった役割を担当するでしょう。また他のプロセスはバックエンドがメッセージを生成して、Socket.IOとやりとりをする形になったりするでしょう。

そこで、socket.io-emitterプロジェクトを紹介します。これは、socket.io-redisにフックさせて、どこからでもブラウザにイベントを送れるようにするためのものです。

var io = require('socket.io-emitter')();
setInterval(function(){
  io.emit('time', new Date);
}, 5000);

Tony Kovanenがsocket.io-emitterのPHP実装を既に作っています。

<?php
$emitter = new SocketIOEmitter(array('port' => '6379', 'host' => '127.0.0.1'));
$emitter->emit('event', 'wow');
?>

このモジュールによって既存のアプリケーションをリアルタイムアプリケーションに簡単に変更することができるようになります!

Better debugging (より良いデバッグ)

Socket.IOは小さく、非常に強力なユーティリティである TJ Hallowaychukが作ったdebugモジュールを呼んでいます。

以前のSocket.IOサーバーはコンソールにすべてのログがデフォルトで出力されていました。多くのユーザーからうるさくて冗長だと言われてました(何人かからは使いやすいとも言われてましたが)。さらにUnix Philosophyの"沈黙は金なり"というルールを破っていました。

沈黙は金なり
開発者は不必要な出力をしないようにプログラムを設計するべきであるとする考え方。このルールは他のプログラムや開発者に冗長なparse処理をしなくてもプログラムの出力から必要とする情報だけを取り出しやすくすることを狙っている。

基本的な考え方はSocket.IOが異なるdebuggingスコープを提供することで各モジュールの内部での理解をしやすくするという考え方です。デフォルトでは、すべての出力は抑制されます、そして環境変数(NODE_ENV)にDEBUGを入れれば、デバッグメッセージが見えるようになります。また、browserでは、localStorage.debug propertyにDEBUGを入れる事でメッセージが見えるようになります。

私達のホームページ上では例えばこんな感じのメッセージが見えるようになります。

Streamlined APIs (APIの合理化)

socket.ioモジュールは直接functionをexportsしています(以前は.listenでした)。
これにより、HTTP serverとsocket.ioを結びつける事が多少簡単になりました。

var srv = require('http').Server();
var io = require('socket.io')(srv);

もしくはportをlistenさせるだけならこうも書けます。

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

以前、接続している全員を参照するには io.socketsを使わなければなりませんでした。いまでは、ioを直接呼び出すだけで実現できます。

io.on('connection', function(socket){
  socket.emit('hi');
});
io.emit('hi everyone');

CDN delivery (CDN配信)

私達が早期に実施した最も良い決定の一つはSocket.IO serverがrealtimeなプロトコルをユーザーに接続させるだけじゃなくて、Socket.IO server自身がクライアントのコードを配信できるようにしたことです。

そのため、あなたが実行することは以下のsnippetを含めるだけで良くなりました。

<script src="/socket.io/socket.io.js"></script>

もしあなたがあなたの近くにいるユーザーにクライアントのコードを渡すことでクライアントへの接続を最適化したり、最大レベルでgzip圧縮されたコードを提供したかったり、クライアントのコードのキャッシュをさせたりしたいのであれば、私達のCDNを使いましょう。無料でbuild-in SSLサポートがついてきます。つまり、以下のように記述しましょう。

<script src="https://cdn.socket.io/socket.io-1.0.0.js"></script>

Future innovation (未来のイノベーション)

コアのSocket.IOプロジェクトは頻繁なリリースとともに継続して改善していこうとしています。信頼性、速度、コードベースを小さく、メンテナンスをしやすさを継続的に改善させることが唯一の目標です。 Socket.IO 2.0 ではおそらく、いくつかの古いブラウザのサポートを廃止するつもりですし、JSON のシリアライザのようないくつかのモジュールを組み込まずにシンプルな設計を行う予定です。

Socket.IOの世界でのイノベーションのほとんどはコアのコードベースの外側で起きるでしょう。私がwatchしている、もっとも重要なプロジェクトは以下の通り:

socket.io-stream

このプラグインを追加することで、Streamオブジェクトを送信することが可能になる予定です。つまり、メモリ効率が良いプログラムを書くことが可能になります。初期の例では、私達はfileをメモリにロードしてからそれをemitしてましたが、可能な限り、以下の様な書き方をするべきでしょう。

var fs = require('fs');
var io = require('socket.io')(3000);
require('socket.io-stream')(io);
io.on('connection', function(socket){
  io.emit(fs.createReadStream('file.jpg'));
});

クライアントサイドでは、data イベントを送る事で Stream オブジェクトを受け取れるようになるでしょう。

Tooling

Socket.IOを使う時、tranports、packet、frame、TCPなのかWebSocketなのかといったことに関してケアする必要はありません。イベントを送信する前後のことだけケアすればよいです。

私達の目標はWeb InspectorやFirefox Developer Toolsのようなツールを作ることによって、どんなイベントが送信されているのか、いつ、なんのパラメータで送受信されたのかを簡単に確認できるようにすることです。


このプロジェクトは 才能豊かな Nick LaGrow, Samaan Ghani, David Cummings によって進められています。

New Languages and frameworks

Engine.IO protocolや Socket.IO protocolに対して多くのテストやドキュメントを書くことを惜しみませんでした。

これの主な目的はNode.JS サーバーやクライアントはリファレンス実装となり、これを基に多くの言語やフレームワークでSocket.IOが実現されることを望んでいます。Socket.IOを中心として巨大なエコシステムを作り、その中で相互運用できることが2014年以降の私達の最も大きな目標です

まとめ

  • Socket.IO 1.0では新しいEngineを採用、初期接続が高速化
  • バイナリサポート
  • ブラウザテストの自動化
  • スケーラビリティ
  • バックエンドの統合
  • デバッグが容易に
  • APIの合理化が進んだ
  • CDN 配信開始
  • 今後のイノベーション(Socket.IO 2.0)について

*1:http://dev.worksap.co.jp/Members/funasaki/2010/06/24/stickysession/

*2:Socket#setとSocket#getのAPIはdeprecatedになります。