ちょっとSocket.ioを導入する機会があったので、色々調査したのをメモしておきます。
Socket.ioとは
node.jsのnpmとして提供されている、WebSocketを手軽に扱えるモジュールです。 多分すごい有名なので、だいたいみなさん知ってると思います。
他にはwebsocket-serverとかもあるんですが、こっちの方が有名ですかね。 特徴として、クライアントサイドのトランスポートがクロスブラウザなところでしょうかね。とても助かります。 詳しいところは公式サイトを見るといいと思います。
すんごい出たばっかりの頃にも触ったことがあったんですが、今使ってみると機能がすごい増えててびっくり。 機能の紹介とかは他のサイトを見てもらうとして、備忘録的に自分がやった所なんかをまとめておくことにします。うろ覚えながら書いてるとこもあるので間違ってたらごめんなさい><
構成
それほど大掛かりなものではないですが、socket.io + pm2で複数プロセス起動するパターンです。で、socket.ioのデフォルトはオンメモリなのでプロセス間でコネクションの共有ができず。RedisのPubSubで共有するようにしています。このあたりのstoreもサポートしてるあたりすごいですね。手法は後述。
接続
サーバ側はロードしてlistenすればOK。
server
``` var io = require('socket.io').listen(8124); ```Client
```<p>以前はhttpのラッパーで動作していたようですが、単体で動かせるようになってますね。あと、クライアント側はサーバ側のlistenがスタートしていないとスクリプトがServeされないので注意</p>
<h4>設定周り</h4>
configureまたはsetメソッドで設定を変更。または、listenメソッドの第二引数にオプションを渡せるみたい。<p></p>
// listenの第二引数で設定 var io = require(‘socket.io’).listen(8124, { host: ‘localhost’ });
// setメソッドで設定 io.set(‘log_level’, 1);
// configureメソッドで追加 io.configure(‘development’, function() { io.set(‘log_level’, 1); });
<p>configureメソッドで設定する場合、第一引数にはnode環境変数に対応した設定が記述できるみたい。上記の場合、</p>
NODE_ENV=development
<p>とか設定して起動すると、その値に応じた設定で起動できる。ログレベルの変更とかにいいですかね。普通にsetメソッドだとグローバルな設定になるようです。デフォルトではログがすんごいダーってでるので、私はlog_levelを変更しています。</p>
<h4>RedisでPub/Sub</h4>
<p>socket.ioのデフォルトストアはオンメモリなので、複数間プロセスでのコネクション共有にはRedisが使える。ライブラリがバンドルされているので公式サポートなのかな?</p>
<h5>Server</h5>
var io = require(‘socket.io’).listen(8124); var redis = require(‘socket.io/lib/stores/redis’); var redisConf = { host: ‘127.0.0.1’, port: 6379 };
// storeタイプをredisに変更 io.set(‘store’, new redis({ redisPub: redisConf, redisSub: redisConf, redisClient: redisConf }));
<p>Pub/Sub/Clientそれぞれで違うサーバを使えるみたい(未検証)。実際にRedisサーバを起動してやってみたらちゃんと共有されているようでした。</p>
<h3>コネクションセグメントの設定</h3>
<p>of()メソッドでコネクション毎にセグメンテーションを切ることができるようになっていた。(内部ではroomって書いてあるので、チャットルームみたいなイメージかも)。今回は、管理者サイドと顧客サイドでセグメントを切ってコネクションを管理。指定がなければof('/')とルートになるみたい。</p>
var room = io.of(‘/room’); // roomもまたServerインスタンスになる
<h3>認証</h3>
<p>authorization()メソッドで認証が可能。</p>
room.authorization(function(handshake, callback) { // 何か認証するコード });
<p>io自体にauthorizaton()メソッドをコールすると、グローバルな認証になる。</p>
io.authorization(function(handshake, callback) { // 何か認証するコード });
<p>引数がクロージャになっており、handshakeにはリクエスト情報が入る。callbackは何者かよくわからないけど、callback('メッセージ'undefined 結果[true/false]);とすることで、認証の成功/失敗が行える。handshakeにはリクエストヘッダとかoriginしか入らないので、厳密にはID/PASSな認証は難しかった。今回は別途認証フローを作ることで対応した。このへんもうちょっと調査したほうがいいかも。</p>
<h3>配信(シングル応答、ブロードキャスト、コネクション指定</h3>
<p>接続後はsocketインスタンスにイベントを設定していって、任意のタイミングで配信するようになっている。</p>
room.on(‘connection’, function(socket) { // 引数のsocketが接続完了後のソケットインスタンス // 接続終了後のイベントを設定 socket.on(‘disconnect’, disconnect); });
<p>イベント周りはおなじみEventEmitterを継承しているのでそれっぽい感じみたい。で、配信はsocketのプロパティでいろんな配信形式が取れる。</p>
// シンプルに自分へのメッセージ送信 socket.emit(‘message’, ‘hoge’);
// ブロードキャストはbroadcastプロパティからemitする socket.broadcast.emit(‘message’, ‘hoge’);
// 揮発性メッセージ(UDPのように送信しっぱなしで再送信はしない) socket.volatile.emit(‘message’, ‘hoge’);
<p>socketに対して行うと接続毎に配信、ioに対して行うとグローバルな配信(ブロードキャストみたいな感じ)になる。</p>
<h3>再接続に関して</h3>
<p>今回ハマったのはここ。ググって出てくるWebSocketのアプリ(チャットとかがほとんど)は、基本的にページを開いた瞬間にはんどシェイクしてそのままなので問題はなさそうなんですが、今回は任意のタイミングで接続をオープン/クローズでき、再接続も可能な感じにする必要がありました。色々試した所、</p>
<h4>一度io.connect()が呼ばれると、内部でsocketインスタンスが生成され、二回目はそのインスタンスが使いまわされる</h4>
<p>という感じの挙動でした(内部までコード見てないので、あくまで推測)。これはどういうことかというと、</p>
var socket;
function startConnection() { socket = io.connect(‘localhost:8124’);
socket.on(‘connect’, function() { console.log(‘connected’); // 3秒後に接続遮断、再接続 setTimeout(function() { // 接続を切る socket.disconnect();
// 再接続
startConnection(); // -> ??
}undefined 3000);
}; }
// 接続開始 startConnection(); // -> connected
<p>ちょっとわかりづらいですが、接続完了後、3秒時間を置いて接続を遮断、再接続を試みるコードです。
これを実行すると、最初は「connected」と出るんですが、2回目からは何も表示されない、つまりconnectイベントがemitされません。
この辺りのドキュメントがあんまり探せなかったんですが、結論からいうと、</p>
<h4>io.connect()は初回呼び出しの場合だけ接続をスタートする</h4>
<p>という感じなのかなーと。つまり上記のコードでは、2回目の再接続にもio.connect()を使っているため、イベントが発火されなかったんですね。で、代わりにどうするかというと、2回目は1回目の接続に使用したsokcetをconnectするようです。</p>
var socket;
function startConnection() { socket = io.connect(‘localhost:8124’);
socket.on(‘connect’, function() { console.log(‘connected’); // 3秒後に接続遮断、再接続 setTimeout(function() { // 接続を切る socket.disconnect();
// 再接続
socket.connect(); // -> connected
}undefined 3000);
}; }
// 接続開始 startConnection(); // -> connected
<p><strong> socket.connect(); </strong>とすることで、再接続ができ、connetcイベントも発火するようになりました。まとめると、一度接続したsocketはどこかにプールしておき、2回目はそれを使いまわすようにする、ってことですね。io.connect()はシングルトンなんでしょうかね。</p>
<h3>まとめと感想</h3>
<p>ざっと触りだけ書いてきましたが、確かに接続周りはすんごく便利ですね。しかしながら、イベント駆動なアプリになってしまうので、アプリケーション側はしっかり設計しないとカオスな感じになると思います。Deferred使うとか、flow.js使うとか、フロー制御できるライブラリと一緒に使うといいと思います。私は自作のメッセージングクラスを作って、socket.ioとアプリケーションの中間レイヤーをコントロールするようにしてなんとか構造を保っている感じでした(;´Д`)</p>