socket.ioは、設定オプションの豊富さと考え抜かれたメソッド設計によって、非常に柔軟に利用できるライブラリ。
リモートサーバ接続者の数をリアルタイムで更新するウィジェットを作成して、socket.ioの器用さを試してみる。このウィジェットをライブオンラインカウンタ(LOC)と名づける。
どのWebサイトにも簡単に設置でき、どのユーザでも簡単に使えて、動作させるために特別な知識を必要としないよう、LOCのインターフェイスはとてもシンプルにする。scriptタグでWebページにロードし、initメソッドを呼び出す(初期化の前にプロパティを読み込むことができる)。
準備
新しいディレクトリを作成し、
新しいファイル
widget_server.js
widget_client.js
server.js
index.html
を準備する。
ここにsocket.ioモジュールをインストールしておく。
ターミナル
$ npm install socket.io
加えて、socket.io-clientモジュールをインストールしておく。
このモジュールを使ってsocket.ioのクライアント側のコードをカスタマイズする。
ターミナル
$ npm install socket.io-client
手順
必要なインターフェイスをindex.htmlに記述する。
index.html
widget_client.jsにウィジェットを記述する。
widget_client.js
ウィジェットをテストするために、複数のドメインからサイトにアクセスする必要がある。そこで、index.htmlを配信するHTTPサーバを生成する。このサーバはローカル環境で実行すると、http://localhost:8080とhttp://127.0.0.1:8080として接続できるため、擬似的に複数ドメインからの接続をテストできる。
リモートサーバ接続者の数をリアルタイムで更新するウィジェットを作成して、socket.ioの器用さを試してみる。このウィジェットをライブオンラインカウンタ(LOC)と名づける。
どのWebサイトにも簡単に設置でき、どのユーザでも簡単に使えて、動作させるために特別な知識を必要としないよう、LOCのインターフェイスはとてもシンプルにする。scriptタグでWebページにロードし、initメソッドを呼び出す(初期化の前にプロパティを読み込むことができる)。
準備
新しいディレクトリを作成し、
新しいファイル
widget_server.js
widget_client.js
server.js
index.html
を準備する。
ここにsocket.ioモジュールをインストールしておく。
ターミナル
$ npm install socket.io
加えて、socket.io-clientモジュールをインストールしておく。
このモジュールを使ってsocket.ioのクライアント側のコードをカスタマイズする。
ターミナル
$ npm install socket.io-client
手順
必要なインターフェイスをindex.htmlに記述する。
index.html
<html>
<head>
<style>
#_loc {color:blue;} // ウィジェットのカスタマイズ項目
</style>
</head>
<body>
<h1>俺のWebページ</h1>
<script src="http://localhost:8081/loc/widget.js"></script>
<script>locWidget.init();</script>
</body>
</html>
widget_client.jsにウィジェットを記述する。
widget_client.js
window.locWidget = {
style: 'position:absolute;bottom:0;right:0;font-size:3em',
init: function () {
var socket = io.connect('http://localhost:8081/', {resource: 'loc'});
var style = this.style;
socket.on('connect', function () {
var head = document.getElementsByTagName('head')[0];
var body = document.getElementsByTagName('body')[0];
var loc = document.getElementById('_lo_count');
if (!loc) {
head.innerHTML = '<style>#_loc {' + style + '}</style>' + head.innerHTML;
body.innerHTML += '<div id="_loc">オンライン:<span id="_lo_count"></span></div>';
loc = document.getElementById('_lo_count');
}
socket.on('total', function (total) {
loc.innerHTML = total;
});
});
}
}
ウィジェットをテストするために、複数のドメインからサイトにアクセスする必要がある。そこで、index.htmlを配信するHTTPサーバを生成する。このサーバはローカル環境で実行すると、http://localhost:8080とhttp://127.0.0.1:8080として接続できるため、擬似的に複数ドメインからの接続をテストできる。
server.js
var http = require('http');
var http = require('http');
var clientHtml = require('fs').readFileSync('index.html');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/html'});
response.end(clientHtml);
}).listen(8080);
最後に、widget_server.jsにウィジェットサーバを記述する。
widget_server.js
最後に、widget_server.jsにウィジェットサーバを記述する。
widget_server.js
var io = require('socket.io').listen(8081);
var sioclient = require('socket.io-client');
var widgetScript = require('fs').readFileSync('widget_client.js');
var url = require('url');
var totals = {};
io.configure(function () {
io.set('resource', '/loc');
io.enable('browser client gzip');
});
sioclient.builder(io.transports(), function (err, siojs) {
if (!err) {
io.static.add('/widget.js', function (path, callback) {
callback(null, new Buffer(siojs + ';' + widgetScript));
});
}
});
io.sockets.on('connection', function (socket) {
var origin = (socket.handshake.xdomain) ? url.parse(socket.handshake.headers.origin).hostname : 'local';
totals[origin] = (totals[origin]) || 0;
totals[origin] += 1;
socket.join(origin);
io.sockets.to(origin).emit('total', totals[origin]);
socket.on('disconnect', function () {
totals[origin] -= 1;
io.sockets.to(origin).emit('total', totals[origin]);
});
});
2つのターミナルウィンドウを開いてテストする。最初のウィンドウでウィジェットサーバを立ち上げる。
ターミナル
$ node widget_server.js
2つ目のウィンドウでHTTPサーバを立ち上げる。
ターミナル
$ node server.js
ブラウザでhttp://localhost:8080にアクセスし、別のタブかウィンドウを開いて同じくhttp://localhost:8080にアクセスする。すると、カウンタが1増加するのがわかる。どちらかを閉じると、カウンタが1減少する。さらに、別のタブかウィンドウで別ドメインのhttp://127.0.0.1:8080にアクセスする。http://localhost:8080のカウンタとは関係なく、独自のカウンタの数字が表示される。
解説
widget_server.jsがこのLOCの中心的役割を果たしている。このスクリプトでは、最初にsocket.ioをrequireで呼び出して、listenメソッドを使って8081番ポートでクライアントの接続を待機する。ここでは、listenメソッドにhttpServerインスタンスを渡すことはせず、ポート番号を直接渡す。ws://localhost:8081は、ウィジェットを通してクライアントが接続するサーバ。
次に、socket.io-clientをrequireでsioclientに呼び出している。sioclientを通してsioclient.builderメソッドにアクセスし、socket.io.jsファイルをカスタマイズして生成する。
sioclient.builderメソッドには2つの引数を渡す。最初の引数はio.transport()で、WecSocketやxhrポーリングなど、リアルタイム通信を実現する様々な手段の配列。配列の先頭に近いほど優先度が高い手段。listenメソッドでオプションを設定していないため、メソッドの戻り値はデフォルトの配列(websocket、htmlfile、xhr-polling、jsonp-pollingの順)。
2つ目の引数はコールバック関数。siojsパラメータにsocket.io.jsの内容が渡されるので、これをコールバック内で使うことができる。次に述べるio.static.addのメソッドで、siojsの内容がwidget_client.jsを読み込んだwidgetScript変数の内容とつなげられて、両方のJavaScriptコンテンツを含んだ1つのファイル(widget.js)が生成される。これらのスクリプトがお互いに影響しないよう、間にセミコロンをはさむ。
socket.ioは、クライアント用JavaScriptファイルを配信するためのHTTPサーバを内部に持っている。HTTPサーバを新たに生成する代わりに、io.static.addメソッドを使ってsocket.ioの内部HTTPサーバに新しいURLルートを設定する。io.static.addの2つ目のパラメータにはコールバック関数が渡される。
このコールバック関数は、追加されたURLルートで配信するコンテンツを設定する。最初の引数は物理ファイルを参照するが、今回は動的にコードを生成するのでnullを渡す。2つ目のパラメータは、(widget.jsとして配信される)siojsとwidgetScriptのコンテンツをつなげた内容をBufferに入れて渡している。これで/widget.jsルートが設定された。
io.configureでresourceプロパティを変更すると、socket.ioが生成する内部HTTPサーバのルートディレクトリを変更することができる。ここではスクリプトファイルへのアクセスを、デフォルトのhttp://localhost:8081/socket.io/widget.jsではなく、ウィジェットの名前(LOC)をパスに含んだhttp://localhost:8081/loc/widget.jsでできるように、resourceプロパティの値を設定している。
クライアントから/loc/widget.jsに接続するために、widget_client.jsのinitメソッド内のio.connectの2つ目のパラメータにoptionsオブジェクトを渡しておく必要がある。optionsオブジェクトにはファイルへのパスを示すresourceオプションが設定されているが、(widget_server.jsのオプションに設定したように)スラッシュがついていないことに注意する。サーバ側ではスラッシュが必須だが、クライアント側ではスラッシュを入れてはいけない。
これでようやくソケット通信を始める準備が整った。widget_server.jsは、io.socketsのconnectionイベントを待機している。そのイベントハンドラでは、まだ紹介していないsocket.ioの機能を使っている。
WebSocketは、クライアントHTTPリクエストを通してハンドシェイクを要求し、サーバがその要求に答えることによって確立する。sockt.handshakeはそのハンドシェイクのプロパティを格納している。
socket.handshake.xdomainは、ハンドシェイクが同じサーバと確立されたかどうかを示す。このプロパティをsocket.handshake.headers.originのhostnameを取得する前にクロスドメインのハンドシェイクかどうか確認するために使っている。
同じドメインへのハンドシェイクのoriginはnullもしくはundefined(ローカルファイルのハンドシェイクか、localhostのハンドシェイクかによる)。undefinedやnullはurl.parseに渡すとエラーが発生するので、同じドメインのハンドシェイクにはlocalという文字列を与える。
originの値を抽出するのは利用しているWebサイトを区別するためで、これによりサイトごとのリアルタイム訪問者カウントを行う。
totalsオブジェクトを使ってカウントを保持する。新しいoriginが発生するたびに、originの値を初期値0のプロパティとしてtotalsオブジェクトに追記する。connectionイベントが発生するたびに、totals[origin]の値に1を追加する。disconnectイベントも待機しておき、発生するたびに1を引く。
totalsの値がサーバでしか使えない場合は、このソリューションは完成しているとは言えない。これらの値をサイトごとに、サイトに接続しているブラウザに提供する方法が必要。
socket.ioのバージョン0.7以降では、socket.joinメソッドを使って複数のソケットのグループを部屋(Room)に分ける便利な機能を用意している。接続中のソケットをoriginの文字列が名前に与えられた部屋に分けて、io.socket.to(origin).emitメソッドを使って、イベントをemitする際に同じsitesの部屋に入ったソケットにのみイベントを送信するよう、socket.ioで設定する。
io.socketsのconnectionイベントとsocketのdisconnectイベントそれぞれの発生時にtotalカスタムイベントを発生させて、それぞれのサイトの訪問者のソケットにトータル接続数を伝える。
widget_client.jsはdivを生成し、totalイベントを受信するたびにdivの内容を書き換える。
Nodeクックブック p.153
2つのターミナルウィンドウを開いてテストする。最初のウィンドウでウィジェットサーバを立ち上げる。
ターミナル
$ node widget_server.js
2つ目のウィンドウでHTTPサーバを立ち上げる。
ターミナル
$ node server.js
ブラウザでhttp://localhost:8080にアクセスし、別のタブかウィンドウを開いて同じくhttp://localhost:8080にアクセスする。すると、カウンタが1増加するのがわかる。どちらかを閉じると、カウンタが1減少する。さらに、別のタブかウィンドウで別ドメインのhttp://127.0.0.1:8080にアクセスする。http://localhost:8080のカウンタとは関係なく、独自のカウンタの数字が表示される。
解説
widget_server.jsがこのLOCの中心的役割を果たしている。このスクリプトでは、最初にsocket.ioをrequireで呼び出して、listenメソッドを使って8081番ポートでクライアントの接続を待機する。ここでは、listenメソッドにhttpServerインスタンスを渡すことはせず、ポート番号を直接渡す。ws://localhost:8081は、ウィジェットを通してクライアントが接続するサーバ。
次に、socket.io-clientをrequireでsioclientに呼び出している。sioclientを通してsioclient.builderメソッドにアクセスし、socket.io.jsファイルをカスタマイズして生成する。
sioclient.builderメソッドには2つの引数を渡す。最初の引数はio.transport()で、WecSocketやxhrポーリングなど、リアルタイム通信を実現する様々な手段の配列。配列の先頭に近いほど優先度が高い手段。listenメソッドでオプションを設定していないため、メソッドの戻り値はデフォルトの配列(websocket、htmlfile、xhr-polling、jsonp-pollingの順)。
2つ目の引数はコールバック関数。siojsパラメータにsocket.io.jsの内容が渡されるので、これをコールバック内で使うことができる。次に述べるio.static.addのメソッドで、siojsの内容がwidget_client.jsを読み込んだwidgetScript変数の内容とつなげられて、両方のJavaScriptコンテンツを含んだ1つのファイル(widget.js)が生成される。これらのスクリプトがお互いに影響しないよう、間にセミコロンをはさむ。
socket.ioは、クライアント用JavaScriptファイルを配信するためのHTTPサーバを内部に持っている。HTTPサーバを新たに生成する代わりに、io.static.addメソッドを使ってsocket.ioの内部HTTPサーバに新しいURLルートを設定する。io.static.addの2つ目のパラメータにはコールバック関数が渡される。
このコールバック関数は、追加されたURLルートで配信するコンテンツを設定する。最初の引数は物理ファイルを参照するが、今回は動的にコードを生成するのでnullを渡す。2つ目のパラメータは、(widget.jsとして配信される)siojsとwidgetScriptのコンテンツをつなげた内容をBufferに入れて渡している。これで/widget.jsルートが設定された。
io.configureでresourceプロパティを変更すると、socket.ioが生成する内部HTTPサーバのルートディレクトリを変更することができる。ここではスクリプトファイルへのアクセスを、デフォルトのhttp://localhost:8081/socket.io/widget.jsではなく、ウィジェットの名前(LOC)をパスに含んだhttp://localhost:8081/loc/widget.jsでできるように、resourceプロパティの値を設定している。
クライアントから/loc/widget.jsに接続するために、widget_client.jsのinitメソッド内のio.connectの2つ目のパラメータにoptionsオブジェクトを渡しておく必要がある。optionsオブジェクトにはファイルへのパスを示すresourceオプションが設定されているが、(widget_server.jsのオプションに設定したように)スラッシュがついていないことに注意する。サーバ側ではスラッシュが必須だが、クライアント側ではスラッシュを入れてはいけない。
これでようやくソケット通信を始める準備が整った。widget_server.jsは、io.socketsのconnectionイベントを待機している。そのイベントハンドラでは、まだ紹介していないsocket.ioの機能を使っている。
WebSocketは、クライアントHTTPリクエストを通してハンドシェイクを要求し、サーバがその要求に答えることによって確立する。sockt.handshakeはそのハンドシェイクのプロパティを格納している。
socket.handshake.xdomainは、ハンドシェイクが同じサーバと確立されたかどうかを示す。このプロパティをsocket.handshake.headers.originのhostnameを取得する前にクロスドメインのハンドシェイクかどうか確認するために使っている。
同じドメインへのハンドシェイクのoriginはnullもしくはundefined(ローカルファイルのハンドシェイクか、localhostのハンドシェイクかによる)。undefinedやnullはurl.parseに渡すとエラーが発生するので、同じドメインのハンドシェイクにはlocalという文字列を与える。
originの値を抽出するのは利用しているWebサイトを区別するためで、これによりサイトごとのリアルタイム訪問者カウントを行う。
totalsオブジェクトを使ってカウントを保持する。新しいoriginが発生するたびに、originの値を初期値0のプロパティとしてtotalsオブジェクトに追記する。connectionイベントが発生するたびに、totals[origin]の値に1を追加する。disconnectイベントも待機しておき、発生するたびに1を引く。
totalsの値がサーバでしか使えない場合は、このソリューションは完成しているとは言えない。これらの値をサイトごとに、サイトに接続しているブラウザに提供する方法が必要。
socket.ioのバージョン0.7以降では、socket.joinメソッドを使って複数のソケットのグループを部屋(Room)に分ける便利な機能を用意している。接続中のソケットをoriginの文字列が名前に与えられた部屋に分けて、io.socket.to(origin).emitメソッドを使って、イベントをemitする際に同じsitesの部屋に入ったソケットにのみイベントを送信するよう、socket.ioで設定する。
io.socketsのconnectionイベントとsocketのdisconnectイベントそれぞれの発生時にtotalカスタムイベントを発生させて、それぞれのサイトの訪問者のソケットにトータル接続数を伝える。
widget_client.jsはdivを生成し、totalイベントを受信するたびにdivの内容を書き換える。
Nodeクックブック p.153