はじめに
Internet Explorer 10(以下、IE10)では、HTML5や関連APIの実装が大きく進みますが、中でもWebSocketに対応することはアプリケーション開発者にとって非常に重要です。
WebSocketは、HTTPリクエストで確立した接続を「繋ぎっぱなし」にすることで、サーバとクライアントの双方向で効率的な通信を実現する技術です。WebSocketは新しい通信プロトコルです(RFC)が、HTTPと同じポート番号を使用するためファイアウォールを越えやすいのと、JavaScript APIに関する仕様もあらかじめ用意されており、Webアプリケーションとの親和性が非常に高いのも特徴です。
WebSocketは、IE10だけではなくGoogle Chrome、Firefox、Safariと言ったブラウザでもサポートされています(Operaでは、デフォルトで無効にされていますが、実装は提供されています)。
ここでは、Developers Summit 2012の「次期Internet Explorer、IE10とHTML5 API」というセッションで使用したWebSocketのサンプルを題材に、WebSocketの可能性について知っていただくことを主眼としています。
このデモンストレーションのソースコードは、こちらからダウンロードすることができます。
このサンプルでは、WebSocketのAPIを直接使用するのではなく、Node.jsとSocket.IOを使用して実装しました。Node.jsは最近人気が急上昇しているプラットフォームで、JavaScriptを使用してサーバやスタンドアローンのプログラムを作成することのできる環境です。Socket.IOはNode.jsの上で動作するフレームワークで、WebSocketプロトコルの実装と使いやすいAPIを提供してくれます。
また、このサンプルはWindows Azure上でホスティングされています。Windows Azureは昨年からNode.jsアプリケーションを実行できるようになりました。
Node.jsとSocket.IOを使ってこのサンプルの実装を行った結果、そのパフォーマンスの高さと開発生産性に驚きました。こうした、2つの意味での「速さ」についても、この記事から伝われば幸いです。
デモの説明
今回作成したデモンストレーションは、複数ユーザで同時編集可能な「オンライン黒板」です。
この黒板の最大の特徴は、複数人で同時編集可能なことです。もちろんその機能の実装にはWebSocket(Socket.IO)を使用しています。
チョークで書く、黒板消しで消す、と言った操作だけではなく、他のユーザのマウスポインタの位置もリアルタイムに把握することができます。
Canvasを使用したUIの作成
まず黒板の部分はHTML5 Canvasを利用して実現しています。チョークで書く、黒板消しで消す、マウスポインタの位置を共有するといった機能については、マウス移動のイベントを捕捉して、それぞれの処理に分岐しています。
その部分のコードを抜粋します。細かいところを省略してしまえば、処理の流れは非常に単純です。
// マウス移動のイベントを捕捉 canvas.mousemove(function(e) { // Canvas上での現在のマウス座標を取得 var curPos = posOnCanvas(e.pageX, e.pageY); var currentX = curPos.x; var currentY = curPos.y; // マウス移動をサーバに送信する COMMAND_OPS.mouseMove({...}, true); if (!drawing) { return; } // 黒板消しで消す if (eracing) { COMMAND_OPS.erase({...}, true); } // チョークで書く else { COMMAND_OPS.drawLine({...}, true) } prevX = currentX; prevY = currentY; });
CanvasのAPIを使用しているコードの例として、黒板に線を書く部分を見てみましょう。線書き出しの開始位置(start)は前回のマウス座標、終了位置(end)は今回のマウス座標となります。
var start = param.start, end = param.end; // 線のスタイル(色に応じた画像を使用) ctx.strokeStyle = LINE_PATTERNS[param.color]; ctx.lineWidth = lineWidth; // 線の太さ ctx.lineJoin = "round"; // 線の結合部分を丸める ctx.lineCap = "round"; // 線の端を丸める ctx.beginPath(); // パスの書き出し開始 ctx.moveTo(start.x, start.y); // 開始位置に移動 ctx.lineTo(end.x, end.y); // 終了位置に向けて直線を引く ctx.stroke(); // パスの書き出し ctx.closePath(); // パスを閉じる
ここでは、CanvasのAPIを素直に使用しているだけですので、説明は省きます。
WebSocketによる共有
そしてこのサンプルの一番のポイントである、Socket.IOを使用したユーザ体験のリアルタイムな共有です。
このサンプルではマウスポインタが移動した」「黒板消しで消した」「線を書いた」と言った操作をJSON形式でサーバに送信しています。それを受け取ったサーバは、それを送信元以外のクライアントに対してブロードキャストします。各クライアントはそれを受け取ってその操作を黒板に対して実行する、という実装を行っています。
実際のコードで見てみましょう。図で言うと(1)の処理を行うコードです。
(1)クライアントからサーバへのデータ送信をSocket.IOで行う
例えばマウスポインタが移動した際、以下のようなコードでsendCommand()というメソッドを呼び出しています。
mouseMove: function(param, share) { if (!share) { return; } // サーバへのデータ送信 sendCommand({ type : "mouseMove", param : param }); }
sendCommand()メソッドは、Socket.IOのAPIを使用しています。クライアント側におけるSocket.IOは、io.connect(URL)によって生成される「ソケット」というオブジェクトを使用します。
var socket = io.connect(location.protocol + '//' + location.host + '/chalkboard');
ソケットが持つ「emit()」というメソッドを用いて、サーバに対してJSONオブジェクトを送信することができます。第一引数はイベント名で、イベントの種別を識別するために汎用的に利用できます。
sendCommand = function(command) { socket.emit('command', command); };
(2)Node.jsとSocket.IOを使用したサーバプログラム
次に掲載するコードは、サーバサイドで(2)の処理を実行する箇所です。Node.jsを利用しているため、サーバサイドのコードもJavaScriptで記述しています。
サーバは、クライアントから送られてきたコマンドを受け取って、送信元以外にブロードキャストします。サーバプログラムの主要部分を以下に掲載します。
const COMMANDS_MAX = 2000; var commands = []; // クライアントからの操作ログを配列に保持する function storeCommand(command) { if (commands.length === COMMANDS_MAX) { // 古い操作ログは削除する commands.shift(); } commands.push(command); } // Socket.IOを使用し、/chalkboardというURLを待ち受ける var sockets = io.of('/chalkboard').on('connection', function(socket) { // 累積したコマンドをクライアントに向けて送る socket.emit('init', commands); // クライアントからコマンドを受け取る socket.on('command', function(command) { // コマンドにセッションIDを紐付ける command.sessionId = socket.id; // mouseMoveイベントは保存しない if (command.type !== 'mouseMove') { storeCommand(command); } // 送信元以外のクライアントにブロードキャスト socket.broadcast.emit('command', command); }); // 接続が切断されたら、全クライアントに通知 socket.on('disconnect', function() { socket.broadcast.emit('leave', socket.id); }); });
中でもポイントとなる部分を抜粋します。なんと、クライアントから受け取ったコマンドを送信元以外のクライアントにブロードキャストする処理を、一行で記述できています。
Socket.IOのAPIが非常に使いやすいことがお分かりいただけるのではないでしょうか。
// クライアントからコマンドを受け取る socket.on('command', function(command) { ... // 送信元以外のクライアントにブロードキャスト socket.broadcast.emit('command', command); });
(3)サーバからコマンドを受け取り、黒板を更新する
(3)はクライアント側のJavaScriptコードになります。"command"イベントを受け取ってCanvasへの描画を行う処理になりますが、Socket.IOを使用すると以下のようなコードになります。
// "command"イベントの監視 socket.on('command', function(command) { ... processCommand(command); });
サンプルのパフォーマンスについて
このサンプルのコードをもう少し読むと、パフォーマンスを高めるような努力が一切行われていないことがわかります。マウスイベントが発生するたびにサーバに対して送信を行っているため、非常に頻繁にサーバとの通信を行っています。
しかしWebSocketのパフォーマンスが非常に良いため、これほどシンプルな実装でも、5,6人程度の同時接続数であれば、全く遅延なくアプリを利用できます。今回は本格的なサービスではなく、あくまでデモンストレーションですので、このレベルのパフォーマンスで十分と判断しました。
まとめ
この記事でお伝えしたかったことは以下の2つです。
- Node.jsやSocket.IOの生産性の高さ:サンプルのコア部分は、非常にシンプルなコードで実現できています。
- WebSocketのパフォーマンスの高さ:全くパフォーマンスチューニングを行っていないにもかかわらず、一定のリアルタイム性を備えています。
Node.jsとSocket.IOを使えば、リアルタイムなアプリケーションを非常に容易に実現できます。そこにHTML5の表現力が加われば、魅力的なアプリケーションを短期間で実装することができます。
今後やってくるリアルタイムWebのイノベーションに備えて、今からWebSocketに親しんでおくとよいのではないでしょうか。