CodeZine(コードジン)

特集ページ一覧

CometとAjaxを利用したチャットサーバの実装

Webブラウザのみで行えるリアルタイムチャットアプリケーション

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2006/12/06 00:00

ダウンロード ソースコード (109.3 KB)
ダウンロード Judeファイル (17.6 KB)

目次

ファイル構成

 ソースファイルアーカイブはzipで圧縮してあります。展開すると「httpserver」というディレクトリを頂点としたディレクトリ階層ができます。すぐに実行できるようにコンパイル済みのクラスファイルも添付してあります。また、ソースファイルはすべてシフトJISでエンコードしています。

「httpserver」ディレクトリ

  • build.xml
  • antのビルドスクリプトです。NetBeans 5.0が生成したファイルですが、コマンドラインからantを利用して操作することも可能です。
    主なターゲットに以下のものがあります。
    • clean
    • ビルドしたクラスファイルを削除。
    • compile
    • srcディレクトリ下のソースファイルをコンパイル。
    • run
    • srcディレクトリ下のソースファイルをコンパイルし実行。application.argsプロパティで引数を指定することができます。

 コマンドラインからjavaコマンドを利用して直接jarを実行する場合は、このディレクトリをカレントディレクトリとして次のように実行してください。

java -jar dist/httpserver.jar

「httpserver\html」ディレクトリ

  • chat.html
  • チャットアプリケーションのクライアント用HTMLです。

「httpserver\html\css」ディレクトリ

  • chat.css
  • チャットアプリケーション用スタイルシートです。ほとんど未設定なので好みに応じて修正してみてください。

「httpserver\html\js」ディレクトリ

  • prototype.js
  • Sam Stephenson氏が開発したJavaScriptライブラリです。チャットアプリケーションではブラウザの差異を吸収してXmlHttpRequestを利用するために使用します。

「httpserver\nbproject」ディレクトリ

 NetBeans 5.0のプロジェクト設定ファイルが格納されています。

「httpserver\src\com\example\http」ディレクトリ

 前回作成した非ブロッキングIOを利用するHTTP簡易サーバです。Cometを処理できるように、以下のファイルが更新されました。

  • HttpServer.java
  • Cometアプリケーションプラグインを登録できるように修正しています。
  • Request.java
  • Cometプラグイン実行のためにクライアントに対する応答データ作成処理を内部クラスとして分離しています。
  • Comet.java
  • Cometプラグイン用のインターフェイスを定義しています。

「httpserver\src\com\example\chat」ディレクトリ

  • ChatApplication.java
  • チャットアプリケーションの本体です。HttpServerに対するCometプラグインとして実装しています。チャットアプリケーションのmainメソッドはこのファイルに定義してあります。

「httpserver\test\com\example\http」ディレクトリ

  • HttpServerTest.java
  • 簡易HTTPサーバのテストプログラムのソースファイルです。今回Cometプラグインのテストを追加しています。

解説

チャットアプリケーションの処理シーケンス

 ここで実装したチャットアプリケーションは次のように動作します。なお、説明文中の太字の数字は、図のメッセージ番号に対応しています。

処理シーケンス
処理シーケンス
  1. HttpServerクラスにChatApplicationクラスをプラグインとして登録し(2)、サービスを開始します(3)
  2. ブラウザは、ユーザーが入力したURLに従って「html/chat.html」を読み込みます。また「chat.html」で指定された「prototype.js」と「chat.css」をロードします(4)
  3. ブラウザはロード完了イベント(body要素のonloadイベント)を受けてロングポーリング用XmlHttpRequestを作成し、サーバにリクエストを送ります(5)
  4. サーバは3.をCometアプリケーションのロングポーリング用リクエストと認識し、作成されたチャネルに対して応答せずにCometアプリケーションクラス(HttpServerのインナークラスのCometAppクラス)の応答待ちチャネルリストに登録します
  5. ユーザーがブラウザ上でメッセージを入力します
  6. 「chat.html」はフォームをサブミットする代わりに、メッセージ送信用のXmlHttpRequestを作成し、サーバにメッセージを送信します(6)
  7. サーバは6.のメッセージを受信すると、Cometアプリケーションのイベント用リクエストのチャネルと認識しChatApplicationに制御を移します(7)ChatApplicationは 4.で応答待ちチャネルリストに登録された各チャネルに対して受信したメッセージを送信します(7.1)。送信が完了したチャネルはクローズします
  8. サーバは7.で作成したイベント用リクエストチャネルに対して空の応答を返してからチャネルをクローズします
  9. ブラウザは7.で送られてきたメッセージを表示します
  10. ブラウザはロングポーリング用XmlHttpRequestを作成し、サーバにリクエストを送ります(8)
  11. 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クラスのコンストラクタ呼び出しのオプションとしてmethodonSuccessonExceptionの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ブラウザを利用して実現できます。

参考資料

  1. おとこのCometアプリケーション! 非モテのためのJetty 6 Continuation入門まとめ(前編)
  2. おとこのCometアプリケーション! 非モテのためのJetty 6 Continuation入門まとめ(後編:その1)
  3. おとこのCometアプリケーション! Jetty 6 Continuation入門まとめ(後編:その2)
  4. Comet: Beyond AJAX - by Dr. Phil Windley
  5. Comet: Low Latency Data for the Browser
  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2006/12/10 02:10 JavaScriptのオブジェクトイニシャライザ記法とprototype.jsのハッシュを混同していたのを修正。間違ったことを書いて申し訳ありませんでした。

著者プロフィール

  • arton(アートン)

    専門は業界特化型のミドルウェアやフレームワークとそれを利用するアプリケーションの開発。需要に応じてメインフレームクラスから携帯端末までダウンサイジングしたりアップサイジングしたりしながらオブジェクトを連携させていくという変化に富んだ開発者人生を歩んでいる。著書に『Ruby③ オブジェクト指向とはじめ...

あなたにオススメ

All contents copyright © 2005-2022 Shoeisha Co., Ltd. All rights reserved. ver.1.5