ファイル構成
ソースファイルアーカイブは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
解説
チャットアプリケーションの処理シーケンス
ここで実装したチャットアプリケーションは次のように動作します。なお、説明文中の太字の数字は、図のメッセージ番号に対応しています。
![処理シーケンス 処理シーケンス](http://cz-cdn.shoeisha.jp/static/images/article/733/sequence.gif)
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ブラウザを利用して実現できます。