socket.io 1.0と0.9の比較や内部実装について。 node学園#13
Socket.IO 1.0の変更点、内部的な話Node学園#13
View Slide
自己紹介github: nkzawatwitter: nkzawasocket.io, engine.ioなどにコントリビュートしたり、socket.io関連モジュールをつくったり。
アジェンダ1. Engine.IOとUpgrade2. Middleware3. バイナリサポート4. Adapter5. プロトコル
Engine.IO と Upgrade
engine.io● トランスポートの違いを吸収して、websocketと同等の機能を提供するライブラリ。● socket.io 1.0には名前空間やルーム、自動再接続などの高レベルな機能のみ実装され、メッセージの送受信はengine.ioを通して行われる。● socket.io 0.9で採用されていたfallback方式ではなくupgrade方式でトランスポートを切り替える。
fallback1. 最初にwebsocketで接続を試みる。2. 接続できたら終了。3. 接続できなかった場合、初期設定で最低10秒間タイムアウトを待つ。4. タイムアウトしたら、改めてpollingで接続を行う。=> websocketが使えない場合の初回接続速度に問題があった。
upgrade1. httpのgetリクエストを行い、pollingの接続を確立させる。2. upgradeできるか判定。設定次第でupgradeは行われず終了。3. pollingを維持したまま、並列にwebsocketで通信しパケットの交換ができるかどうかまで試す(probe)。4. websocketがつながったらpollingが止まるのを待ち(メッセージのロスを防ぐため)、メインのトランスポートを切り替える。
サポートされるトランスポート● websocket● polling○ xhr○ jsonp※polling時はxhrが優先的に使用される。
flashsocket ?公式にはサポートされなくなった。flashsocketに限らず、別のトランスポートが必要な場合は自作して追加する。“removed flashsocket, moving to userland”https://github.com/Automattic/engine.io/commit/10261c52119f2912b72b55bec4df87d91b180525
upgrade関連オプション(client側)transports: [‘polling’, ‘websocket’]使用するトランスポートを順番に設定する。[‘websocket’] とすると最初からwebsocketで接続することもできる。rememberUpgrade: false一度upgradeに成功した後に再接続すると、upgradeプロセスを飛ばしてwebsocketで接続する。SSLの時などに有効にするとよいらしい。
Middleware
middlewareハンドシェイクから接続までの間に、処理を挿入する仕組み。expressのmiddlewareと似たようなもの。io.use(function(socket, next) {});
0.9では0.9ではauthorizationで同様のことができたが、あくまで認証用の機能で拡張性は低かった。io.set(‘authorization’, function(handshakeData, fn) {// 認証失敗fn(null, false);});
1.0の認証setメソッドとauthorizationは廃止(後方互換のため残されてはいる)。代わりにmiddlewareを使って認証する。io.use(function(socket, next) {// 接続を閉じるnext(new Error(‘not authorized’););});
ハンドシェイクデータハンドシェイク時のrequestオブジェクトにアクセスでき、そこからcookieのパースやセッションの復元などが可能。io.use(function(socket, next) {console.log(socket.request);next();});
// expressのmiddlewareを使ってcookieをパースするvar cookieParser = require(‘cookie-parser’)(‘my secret’);io.use(function(socket, next) {var req = socket.request;var res = {}; // ダミーのレスポンスcookieParser(req, res, next);});io.on(‘connection’, function(socket) {console.log(socket.request.cookies);});
ネームスペースmiddlewareはネームスペース単位。// デフォルトネームスペース。次の二つは同じ意味。io.use(function(socket, next) {});io.of(‘/’).use(function(socket, next) {});// foo ネームスペースio.of(‘/foo’).use(function(socket, next) {});
実行順序全てのクライアントは、デフォルトネームスペース(’/’)へ必ず最初に接続され、’/’ のmiddlewareが実行される。cookieパーサのように、常に先に実行したいmiddlewareは‘/’ へ登録する。
// クライアントvar socket = io(‘http://localhost:3000/foo’);// サーバio.use(function(socket, next) {console.log(‘first’); // 先に実行されるnext();});io.of(‘/foo’).use(function(socket, next) {console.log(‘second’); // 後で実行されるnext();});
バイナリサポート
バイナリサポート1.0からバイナリデータを送受信できるようになった。オブジェクトや配列に埋め込むんだり、複数バイナリを同時送受信も可能。socket.emit(‘event’, new Buffer([0, 1, 2]));
examples// オブジェクトに埋め込みsocket.emit(‘event’, {data: new ArrayBuffer(10)});// 複数バイナリsocket.emit(‘event’, [binary1, binary2]);// acknowledgesocket.on(‘event’, function(callback) {callback(new Buffer([1, 2, 3]));});
0.9でバイナリを扱う0.9でもバイナリをbase64エンコードした文字列として送受信することはできた。ただし、エンコード/デコード処理が入り、データサイズも増加するため効率はよくない。socket.emit(‘event’, buffer.toString(‘base64’));
サポートされるデータ形式● Buffer (node)● ArrayBuffer (node, browser)● Blob, File (browser)
バイナリ送信できるトランスポート● ◯ websocket● ◯ XMLHttpRequest Level 2● ☓ 古い仕様のwebsocket● ☓ XMLHttpRequest● ☓ JSONP※バイナリ送信をサポートしないトランスポートでも、base64エンコードされた文字列として透過的にデータを送受信できる。
バイナリの扱いに注意データサイズに制限はないが、メモリへ読み込むので、大きなファイルの配信などには向かない。この問題を解決するために、別モジュールによるstream送信のサポートが計画されている。
Adapter
Adapter複数のサーバ間でメッセージをbroadcastする時に使用される。0.9にあったStoreの代替。io.adapter(Adapter);
socket.io-redis0.9にあったRedisStore相当のadapter。socket.io本体には同梱されていないので、別途インストールが必要。var redis = require(‘socket.io-redis’);io.adapter(redis({host: ’localhost’, port: 6379}));
Storeとの違い● Storeのget/set 相当のデータ共有機能がなくなりシンプルに。Adapterが扱うのはbroadcastのみ。● 内部的に使っていたpublish/subscribeがなくなってスケールしやすい設計に。
socket.io 0.9のサーバ間共有設計クライアントの接続データ(handshakeデータ, 所属ルーム等)は、Storeを通して他のサーバへ通知され保存される。それぞれのサーバが、他サーバに接続しているものを含む全てのクライアントデータ(の一部)を処理し、保持するためスケールしにくい。
例: socket.io 0.9サーバ10台に、それぞれ1万クライアントが同時接続する場合。各サーバそれぞれで10万クライアントのデータが処理・保持される。サーバを増やしても、一つのサーバが処理するデータ数は変わらず、同時接続数に比例して増える。サーバA: 10万サーバB: 10万…※あくまでデータの一部なので全くスケールしないわけではないと思われる。
socket.io 1.0のサーバ間共有設計各サーバは一切データ共有を行わない。1.0のAdapterでは、broadcastしたパケットとそのオプションデータだけが通知され、サーバには何も保存されないためスケールしやすい。
例: socket.io 1.0サーバ10台に、それぞれ1万クライアントが同時接続する場合。各サーバは自身に接続している1万クライアントのデータのみを処理・保持する。サーバを増やすことで、一つのサーバが処理するデータ数を減らすことができる。サーバA: 1万サーバB: 1万...
プロトコル
プロトコルの変化socket.ioの構成変更や機能追加に伴い、プロトコルの大幅な更新が行われた。● トランスポート層(engine.io)の分離● バイナリサポート
0.9のパケットコロン区切りのメッセージ[type] : [id] : [namespace] : [data]例: 5::/nsp:{"name":"foo","args":["hi"]}
0.9のペイロードpollingで一度に複数のパケットを一括送信するのに使用される。\ufffd 区切りでデータ長 +パケットを連結して繰り返す。\ufffd [length] \ufffd [packet] ...例: \ufffd32\ufffd5:::{"name":"foo","args":["hi"]}\ufffd ...
1.0のパケット区切り文字とJSONのキー名がなくなり効率的に。データはutf8エンコードされる。[engine.io type] [type] [namespace] , [data]例: 42/nsp,["foo","hi"]
1.0のバイナリを含むパケットバイナリ数が追加。バイナリデータはプレースホルダで置き換えられ、別のパケットとして送られる。[engine.io type] [type] [binary数] - [namespace] , [data]例: 451-/nsp,["foo",{"_placeholder":true,"num":0}]
1.0のペイロードバイナリ送信をサポートするtransportでは、文字列も全てバイナリに変換される。パケットがutf8エンコードされるのはこのため。[0(string), 1(binary)] [length(一桁づつ)] 255 [packet] ...例: [00 02 05 ff 34 32 2f 6e 73 70 2c 5b 22 65 63 68 6f 2062 61 63 6b 22 2c 22 68 69 22 5d]
1.0のペイロード(文字列)バイナリ送信をサポートしないtransportでは、文字列のまま送信。バイナリはbase64エンコードされる。[length] : [packet] ...例: 19:42/nsp,["foo","hi"]
プロトコルの変更による影響socket.io 0.9のプロトコルを実装していた別言語のsocket.ioサーバ・クライアントは一切使用できない。=> ユーザが0.9から1.0にアップデートする際の最も大きな障害になると思われる。
まとめ● engine.ioでより安定したsocket通信ができます。● middleware便利。モジュールを作って公開しよう。● バイナリも送受信できるようになった。● adapterでスケールしやすいアーキテクチャに。● 0.9から1.0への移行は難しくない。プロトコルの違いには注意。
thanks!