ファイル構成
ソースファイルアーカイブはzipで圧縮してあります。展開すると「httpserver」というディレクトリを頂点としたディレクトリ階層ができます。すぐに実行できるようにコンパイル済みのクラスファイルも添付してあります。また、ソースファイルはすべてシフトJISでエンコードしています。
「httpserver」ディレクトリ
- build.xml
- clean
- compile
- run
application.argsプロパティで引数を指定することができます。コマンドラインからjavaコマンドを利用して直接jarを実行する場合は、このディレクトリをカレントディレクトリとして次のように実行してください。
java -jar dist/httpserver.jar
「httpserver\html」ディレクトリ
- chat.html
「httpserver\html\css」ディレクトリ
- chat.css
「httpserver\html\js」ディレクトリ
- prototype.js
「httpserver\nbproject」ディレクトリ
NetBeans 5.0のプロジェクト設定ファイルが格納されています。
「httpserver\src\com\example\http」ディレクトリ
前回作成した非ブロッキングIOを利用するHTTP簡易サーバです。Cometを処理できるように、以下のファイルが更新されました。
- HttpServer.java
- Request.java
- Comet.java
「httpserver\src\com\example\chat」ディレクトリ
- ChatApplication.java
mainメソッドはこのファイルに定義してあります。「httpserver\test\com\example\http」ディレクトリ
- HttpServerTest.java
解説
チャットアプリケーションの処理シーケンス
ここで実装したチャットアプリケーションは次のように動作します。なお、説明文中の太字の数字は、図のメッセージ番号に対応しています。

HttpServerクラスにChatApplicationクラスをプラグインとして登録し(2)、サービスを開始します(3)- ブラウザは、ユーザーが入力したURLに従って「html/chat.html」を読み込みます。また「chat.html」で指定された「prototype.js」と「chat.css」をロードします(4)
- ブラウザはロード完了イベント(
body要素のonloadイベント)を受けてロングポーリング用XmlHttpRequestを作成し、サーバにリクエストを送ります(5) - サーバは3.をCometアプリケーションのロングポーリング用リクエストと認識し、作成されたチャネルに対して応答せずにCometアプリケーションクラス(
HttpServerのインナークラスのCometAppクラス)の応答待ちチャネルリストに登録します - ユーザーがブラウザ上でメッセージを入力します
- 「chat.html」はフォームをサブミットする代わりに、メッセージ送信用の
XmlHttpRequestを作成し、サーバにメッセージを送信します(6) - サーバは6.のメッセージを受信すると、Cometアプリケーションのイベント用リクエストのチャネルと認識し
ChatApplicationに制御を移します(7)。ChatApplicationは 4.で応答待ちチャネルリストに登録された各チャネルに対して受信したメッセージを送信します(7.1)。送信が完了したチャネルはクローズします - サーバは7.で作成したイベント用リクエストチャネルに対して空の応答を返してからチャネルをクローズします
- ブラウザは7.で送られてきたメッセージを表示します
- ブラウザはロングポーリング用
XmlHttpRequestを作成し、サーバにリクエストを送ります(8) - 5.へ戻ります
以降では、上記処理シーケンスのうち3.、6.のクライアント側でのAjaxを利用した呼び出しについて解説します。
クライアント側処理
処理シーケンス 3.ロングポーリングの実行
HTMLのロード完了時に発生するbody要素のonloadイベントで呼び出しているlongPoll関数では、「prototype.js」が提供するAjax.Requestクラスのインスタンスを作成して、サーバに対するロングポーリングを実行します。
function longPoll() { new Ajax.Request( "/chat/poll", { method: 'get', onSuccess: function(req) { new Insertion.Top('list', '<li id="message">' + decodeURIComponent(req.responseText.replace(/\+/g, ' ')) + '</li>'); longPoll(); }, onException: function(req, e) { new Insertion.Top('list', '<li id="error">recv failed: ' + e + '</li>'); } }); } …… <body onload="longPoll();"> …… <div id="output"> <ul id="list"> </ul> </div>
Ajax.Requestクラスはコンストラクタの呼び出しで自動的にサーバへの接続とリクエストの送信を実行し、応答待ちとなります。
Ajax.Requestのコンストラクタには、呼び出し先のURLと、オプションをプロパティに設定したオブジェクトを与えます。
ここでURLに指定している/chat/poll/は、実際のファイル(「/html/chat.html」など)ではなく、ChatApplicationクラスが起動時にHttpServerに登録した仮想的なパスです。なおCometプラグインの仮想的なパスの登録処理については特に示しません。CometインターフェイスとHttpServerクラスでの利用方法を参照してください。
このプログラムではオプション指定用オブジェクトの生成にオブジェクトイニシャライザを利用しています。オブジェクトイニシャライザでは、生成するオブジェクトのプロパティと値のペアを{}で囲んで記述します。プロパティと値は「: 」で区切り、各ペアは「,」で区切ります。なお、最後のペアの後ろに「,」を付けることはできません。
従って、ここではAjax.Requestクラスのコンストラクタ呼び出しのオプションとしてmethod、onSuccess、onExceptionの3つのパラメータを登録していることになります。
methodは、HTTPのgetを呼び出すかpostを呼び出すかの指定です。
onSuccessには、サーバからOKステータスの応答を受信後に呼び出されるイベントハンドラを指定します。イベントハンドラの引数はAjax.Requestが呼び出しに利用したXmlHttpRequestオブジェクトです。このオブジェクトのresponseTextプロパティを利用してサーバからの応答を参照できます。
ここでは、onSuccessに対して、メッセージの出力後に再度ロングポーリングを実行する処理を与えています。この関数内では、表示前にメッセージ中に出現する+をスペースに置き換えてからURLデコードしています。これはサーバがメッセージをURLエンコードして送信するため、メッセージの表示前にURLデコードして元のメッセージを取り出すためです。なお、最初に+をスペースに置き換えるのは、JavaのURLEncoderのエンコード方法(空白は+に置換)とJavaScriptのdecodeURIComponent(+は+のまま残す)で+の扱いが異なることが理由です。
onExceptionには、例外発生時に呼び出されるイベントハンドラを指定します。引数はonSuccessと同じくXmlHttpRequestオブジェクトとそれに加えて第2引数として例外オブジェクトが与えられます。
ここではonExceptionに対してエラー発生の表示を行います。また呼ばれた場合には、ロングポーリングの再実行は行いません。これにより、エラー時には、XmlHttpRequestの2個制限に関わらずユーザーはブラウザをリロードして処理をやり直せます。
「prototype.js」が提供するInsertion.Topクラスのコンストラクタは2つの引数を取ります。最初の引数は第2引数で指定したHTML片などを挿入する要素のIDです。ここではIDにlistを指定しているため、「chat.html」のul要素(リスト表示用タグ)の最初の子要素として第2引数の値が挿入されます。「prototype.js」には同様にInsertion.Bottom(指定HTMLを最後の子要素の後ろに挿入)などのDOMを操作するための関数が複数用意されています。
処理シーケンス 6.メッセージのサーバへの送信
サーバへメッセージを送信するのに、ここではフォームのサブミットではなくAjaxを利用します。もしフォームのサブミットを利用するとこれまで受信して動的にHTMLに追加したメッセージが消えてしまいます。また、ロングポーリングしているXmlHttpRequestが次のページに進むため無効になってしまいます。そもそもAjaxを利用すれば画面全体の再描画を行わないで済みます。そのため、サーバ側の処理が必要最小限で完了します。
次のリストは、サブミットボタンクリック時に発生するform要素のonsubmitイベントで呼び出されるaddMessage関数です。
function addMessage() { new Ajax.Request( "/chat/add", { method: 'post', postBody: $H({name: $F('name'), talk: $F('talk')}).toQueryString() }); $('talk').value = ''; $('talk').focus(); return false; } …… <form name="form" id="form" method="POST" onsubmit="return addMessage();">
longPoll関数と同様に、ここでもAjax.Requestオブジェクトを作成します。異なる点はmethodにpostを与えてPOSTメソッドを呼び出しに利用することと、postBodyで送信するデータを指定していることです。なお実際にサーバからの応答を処理する必要があるのは、ロングポーリングしているXmlHttpRequestのほうなので、ここではonSuccessイベントハンドラは登録しません。またonExceptionについては無視しています。
ここでpostBodyの組み立てに利用している$H()は引数で指定したオブジェクトをハッシュ(Javaのjava.util.Mapと同様にキーと値のペアを操作するメソッドを持ちます。ハッシュもprototype.jsによって定義されているオブジェクトで、与えられたオブジェクトのプロパティをキーとして利用します)に変換する関数です。ここではハッシュのtoQueryStringメソッドを呼び出して送信用データを作るために利用しています。ハッシュのtoQueryStringメソッドは、ハッシュに格納されたキーと値をURLエンコードしてから=で結合し、さらに各キーと値のペアを&で連結した文字列を返します。
prototype.jsには$H()の他にも、ここでも利用している$()、$F()や、リストを配列に変換する$A()などのコードを簡略化するための関数が用意されています。
$F()は引数で指定したIDを持つHTML要素の値を取り出す関数で、document.getElementById(引数).valueと記述したのと同等です。
$()は同様に引数で指定したIDを持つHTML要素を返します。ただし、該当IDが1つしかない場合にはdocument.getElementById(引数)と同等ですが、2つ以上ある場合には要素の配列が返ります。
これらの関数を利用するとコードがすっきり書けます。ただし注意点として、JavaScriptの性格上、呼び出しの都度DOMの走査が必要になるため、多用するとパフォーマンスに悪影響を与えることが挙げられます。そのため、複雑なHTML上で繰り返し参照する要素などについては、ここでの$('talk')の例のように$()を常に利用するのではなく、最初に取り出した要素を変数に代入して、以降はそれを利用するといった考慮が必要となることもあります。
なお、ここではサブミットボタンをクリックして呼び出されるaddMessage関数がフォームデータの送信を行うため、ブラウザによるフォームのサブミットは止めなければなりません。フォームのサブミットを行わないようにするには、ここで示したように、onsubmitイベントに対してfalseを返します。
まとめ
Cometは、クライアントからのリクエストをサーバ側でイベントが発生するまで保持することで、クライアント―サーバ間の無駄なトラフィックを減らすと同時に、ユーザーに対してイベントの発生を速やかに通知する仕組みです。
本記事では、簡単なCometの仕組みを実装してチャットアプリケーションを作成しました。Cometを利用すると、このようなリアルタイムに情報をクライアントへ送り届けるための仕組みを通常のWebブラウザを利用して実現できます。
