WebSocketを触ってみる

どうもこんばんは、south37です。
今回は、ちらほら噂を聞くWebSocketってやつを触ってみたいと思います。

WebSocket is 何?

さて、そもそもWebSocketって何なんでしょうか。Wikipediaによると、

WebSocket(ウェブソケット)は、コンピュータ・ネットワーク用の通信規格の1つである。インターネットの標準化団体であるW3CIETFがウェブサーバーとウェブブラウザとの間の通信のために規定を予定している双方向通信用の技術規格であり、APIW3Cが、WebSocket プロトコルはIETFが策定に関与している。プロトコルの仕様は RFC 6455。TCP上で動く。

http://ja.wikipedia.org/wiki/WebSocket

らしいです。

この説明だけだと何のこっちゃな訳ですが、ざっくり言うとAjaxCometに用いられるXMLHttpRequestに置き換わる新しい通信手法です。AjaxやCometにはいろいろ欠点があった為、その欠点を解決する事を目的として考案されたっぽいです。

と、書いてみた訳ですが、AjaxはともかくそもそもCometって何やねんって感じですよね?って事で、そのへんも説明してみます。

AjaxとかCometとか

Ajaxは、皆さんご存知の通りページの遷移やリロードをせずにページの一部を書き換えるアレです。Ajaxでは、バックグラウンドでサーバに非同期のrequestを送って、返ってきた結果を使ってJavaScriptで動的にページに反映させていきます。この時使われるrequestがXMLHttpRequestです。
Cometってのは、サーバからクライアントへのPush通知のようなものを実現する為に考案された方法です。CometでもXMLHttpRequestを用いるのですが、サーバーがrequestを受け取ってもすぐにresponseを返さず、サーバー側で何らかのイベントがあってからresponseを返す事で、擬似的に通知の機能を実現します*1。バックグラウンドでXMLHttpRequestを投げておく事で、サーバーから通知がある時にresponseの形でクライアントに通知を送る事が出来る訳です。
ただ、Cometには欠点もあります。サーバー側でHTTPコネクションが繋ぎっぱなしになる為、同一サーバに接続する他のアプリケーションの動作に影響を及ぼす可能性があります。また、通知の度にコネクションを張り直す無駄も生じます。

WebSocket登場

そこで、それらの問題点を解決する為に考案されたのがWebSocketです。Wikipediaによると、

WebSocketでは、サーバとクライアントが一度コネクションを行った後は、必要な通信を全てそのコネクション上で専用のプロトコルを用いて行う。従来の手法に比べると、新たなコネクションを張ることがなくなる・HTTPコネクションとは異なる軽量プロトコルを使うなどの理由により通信ロスが減る、一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ないなどのメリットがある。

http://ja.wikipedia.org/wiki/WebSocket

らしいです。問題点まるっと解決ですね!

ただ、WebSocketはHTML5から標準化された仕様である為、全てのブラウザで実装されている訳では無いという欠点があります。これに関しては、IBMの記事で

推奨事項
WebSocket は、Comet よりも多くのメリットをもたらします。日常的な開発作業では、WebSocket をサポートするクライアントのほうが速度に優れ、生成されるリクエストの数も少なくなります (そのため、帯域幅の使用量も少なくなります)。ただし、すべてのブラウザーが WebSocket をサポートしているわけではありません。そこで、最適なリバース Ajax ライブラリーとなるのが、WebSocket のサポートを検出する機能を備え、WebSocket がサポートされていない場合には Comet (ロング・ポーリング) にフォールバックできるライブラリーです。

http://www.ibm.com/developerworks/jp/web/library/wa-reverseajax2/

みたいな記述があったので、Cometと適宜使い分け出来る仕組みがあるといいらしいです。

前置きが長くなりましたが、いよいよWebSocketを使ってみたいと思います。

socket.ioを使ってみる

WebSocketを使う方法はいろいろある訳ですが、その中でもnode.jsでsocket.ioを使うのが一番楽そうだったので、試してみたいと思います。socket.ioはnode.js(&クライアントサイドjs)用のライブラリで、ブラウザ毎に実装方法が異なっていたりするWebSocketの細かな違いをまるっと吸収してくれるむちゃくちゃ便利な奴です。WebSocketという技術自体がnode.jsと相性がいい事もあり、広く使われている印象です。

それでは、実際にコードを書いていきましょう。前回紹介したexpressをベースにしてみます。まずは、expressコマンドでアプリケーションのひな形を作ります。

express install -t jade chat

これで、railsのscaffoldの様に必要っぽいファイルが一気に生成されます。
ディレクトリ構成は

── app.js # URLディスパッチ処理
├── node_modules
│   ├── express
│   └── jade
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── routes
│   ├── index.js # /が呼ばれた時、呼び出されて、index.jadeにパラメータを渡す
│   └── user.js
└── views
├── index.jade # レイアウトの<%- body ->の部分
└── layout.jade # 全体のレイアウト

こんな感じになります。

次に、

npm install socket.io

でsocket.ioをインストールします。socket.ioが入ったら、app.jsとindex.jadeを書き換え、クライアントサイドのjsコードを書きます。

まずは、app.jsでサーバーサイドの処理を書きます。

// app.js
  1
  2 /**
  3  * Module dependencies.
  4  */
  5
  6 var express = require('express');
  7 var routes = require('./routes');
  8 var user = require('./routes/user');
  9 var http = require('http');
 10 var path = require('path');
 11
 12 var app = express();
 13
 14 // socket.ioをrequire
 15 var server = http.createServer(app);
 16 var io = require('socket.io').listen(server);
 17
 18 // all environments
 19 app.set('port', process.env.PORT || 3000);
 20 app.set('views', path.join(__dirname, 'views'));
 21 app.set('view engine', 'jade');
 22 app.use(express.favicon());
 23 app.use(express.logger('dev'));
 24 app.use(express.json());
 25 app.use(express.urlencoded());
 26 app.use(express.methodOverride());
 27 app.use(app.router);
 28 app.use(express.static(path.join(__dirname, 'public')));
 29
 30 // development only
 31 if ('development' == app.get('env')) {
 32   app.use(express.errorHandler());
 33 }
 34
 35 app.get('/', routes.index);
 36 app.get('/users', user.list);
 37
 38 server.listen(app.get('port'), function(){
 39   console.log('Express server listening on port ' + app.get('port'));
 40 });
 41
 42 // クライアントが接続してきたときの処理
 43 io.sockets.on('connection', function(socket) { //2
 44   console.log('connection');
 45   socket.on('message', function(data) { //4
 46     console.log('message');
 47     io.sockets.emit('message', { value: data.value });
 48   });
 49
 50   socket.on('disconnect', function() {
 51     console.log('disconnect');
 52   });
 53 });

14〜16行目のsocket.ioをrequireしている部分と、42行目以下のクライアントが接続してきた時の処理を書いている部分が書き足したコードです。

クライアントのjsコードは、

//chat.js
  1 var socket = io.connect('http://localhost:3000'); //1
  2 socket.on('connect', function(msg) { //2
  3   console.log("connet");
  4   document.getElementById("connectId").innerHTML =
  5     "あなたの接続ID::" + socket.socket.transport.sessid;
  6   document.getElementById("type").innerHTML =
  7     "接続方式::" + socket.socket.transport.name;
  8 });
  9
 10 socket.on('message', function(msg) { //5
 11   document.getElementById("receiveMsg").innerHTML = msg.value;
 12 });
 13
 14 // メッセージを送る
 15 function SendMsg() { //3
 16   var msg = document.getElementById("message").value;
 17   socket.emit('message', { value: msg });
 18 }
 19
 20 // 切断する
 21 function DisConnect() {
 22   var msg = socket.socket.transport.sessid + "は切断しました。";
 23   socket.emit('message', { value: msg });
 24   socket.disconnect();
 25 }

となっています。

WebSocketではサーバーとクライアントの処理は同時に見ていった方がいいので、併せて解説していきます。

1. まずはクライアントからio.connectでサーバーにWebSocketの接続を行います。
2. コネクションが確立されるとサーバーではconnectionイベントが、クライアントではconnectイベントが発生します。サーバーではmessageイベントやdisConnectイベントへの処理のバインドが、クライアントではIDや接続方式の画面への表示が行われます。
3. クライアントでSendMsg関数が実行されると、socket.emitによってサーバーへmessageが送られます。
4. サーバーでは受け取ったmessageを、io.sockets.emitで繋がったクライアント全てに一斉送信します。
5. クライアントではサーバーからmessageを受け取るとそれを画面に表示します。

DisConnect関数が呼ばれた時の処理も似たような感じです。

socket.io(WebSocket)では、emitメソッドによってクライアントとサーバでmessageをやり取りしてるのが特徴的だと思います。特に、io.sockets.emitで全てのクライアントに一斉送信を行っており、サーバーを介してですがクライアント同士でリアルタイムな通信が行えるのがいいですね。

この辺の処理の流れはSocket.IO API 解説ってページでそこそこ詳しく解説されています。興味があれば読んでみるといいかと思います。

一応、htmlファイルも書いておくと、

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <link rel="stylesheet" href="/stylesheets/style.css">
  </head>
  <body>
    <h1></h1>
    <div id="co nnectId"></div>
    <div id="type"></div>
    <br>
    <input id="message" type="text" value="">
    <input type="button" value="messageを送る" oncl ick="SendMsg()">
    <input type="button" value="切断する" onclick="DisConnect()">
    <div id="receiveMsg"></div>
  </body>
</html>
<script src="/socket.io/lib/socket.io.js"></script>
<script src="/javascripts/chat.js"></script>

で、SendMsgやDisConnectがボタンにバインドされているのが分かるかと思います。

上記のコードで複数のタブからhttp://localhost:3000に繋いでみると、確かにリアルタイムでメッセージをやり取り出来ました。WebSocketすごいですね!

*1:MosaicAlbumの、画像生成後の通知ってのがCometの処理にむちゃくちゃ近い気がします