解説(2/2)
IHttpAsyncHandlerインターフェイス
非同期HTTPハンドラは、System.Web.IHttpAsyncHandlerインターフェイスを実装します。このインターフェイスが定義しているメソッドとプロパティは次のとおりです。
- public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
- public void EndProcessRequest(IAsyncResult result);
- public bool IsReusable { get; }
- public void ProcessRequest(HttpContext context);
| 引数 | 説明 |
| context | このリクエスト処理に関連するHttpRequest、HttpResponseなどのオブジェクトを集約したオブジェクト |
| cb | 処理完了時にASP.NETに通知するためのデリゲート |
| extraData | 付加情報 |
| 引数 | 説明 |
| result | このリクエスト処理の対になるBeginProcessRequestが返したIAsyncResultオブジェクト |
IHttpAsyncHandlerインターフェイスの継承元のIHttpHandlerインターフェイスで定義されているプロパティ。このハンドラのオブジェクトが他のリクエスト処理と共用できる場合にtrueを返す。IHttpAsyncHandlerインターフェイスの継承元のIHttpHandlerインターフェイスで定義されているメソッド。非同期HTTPハンドラの場合、呼び出されることは無い。非同期ハンドラの実装
ここでは、IHttpAsyncHandlerインターフェイスを実装している「App_Code\ChatHandler.cs」のソースを説明します。「ChatHandler.cs」は、IAsyncResultインターフェイスを実装し、ASP.NETとのインターフェイスに利用されるChatClientクラスと、IHttpAsyncHandlerインターフェイスを実装した非同期HTTPハンドラそのもののChatHandlerクラスの2つのクラスを含みます。
なお、以下のリストは、アーカイブのソースコードには含まれているデバッグ出力などを省略しています。
ソースコード説明
ChatHandlerクラスは、IHttpAsyncHanlderインターフェイスを実装した、非同期HTTPハンドラです。
フィールド
ChatHandlerクラスは、クライアントからのメッセージを受けて、保留状態にあるCometクライアントに対してレスポンスを返します。このため、保留状態のCometクライアントを保持するコレクションオブジェクトが必要となります。ここでは、staticフィールドを利用してコレクションオブジェクトを持ちます。また、途中で消失したクライアントをチェックするためのタイマーも同様にstaticフィールドで管理します。これらは、ChatHandlerクラスのオブジェクトとは異なる寿命を持つことと、Webサイト全体で共有されることが理由です。
static volatile List<ChatClient> clients = new List<ChatClient>(); static Timer idleTimer = new Timer(delegate(object state) { DateTime dt = DateTime.Now; List<ChatClient> list = new List<ChatClient>(); lock (clients) { clients.RemoveAll(delegate(ChatClient c) { if (c.IsExpired(dt)) { list.Add(c); return true; } return false; }); } foreach (ChatClient c in list) { c.Release(string.Empty); } }, null, TimerInterval, TimerInterval);
clientsフィールドは、Comet管理用のChatClientクラスのListです。クラスのロードと、オブジェクトの生成が同一のスレッドで行われるか不明なため、volatileフィールドとして定義しています。
idleTimerフィールドは、メッセージの送信が一定期間行われなかった場合に、Cometクライアントを一度解放(および消失したクライアントをチェック)する処理を実行するために利用します。
ここで実行しているListクラスのRemoveAllメソッドは、すべての要素について引数で指定したデリゲートを呼び出しtrueを返したらコレクションから削除し、falseであればコレクションに保持したままとするメソッドです。ここでは無名メソッドを利用してCometクライアントの受信時刻と現在時刻の差を求め、既定値以上であればtrueを返してコレクションから削除します。また、コネクションが有効であれば該当するCometクライアントに再接続を要求するために送信用のコレクション(list変数)へ追加します。
最後に呼び出しているCometClientクラスのReleaseメソッドは引数で指定した文字列(ここでは空文字列)をCometクライアントへ送信するメソッドです。また、Releaseメソッドの中でコネクションが有効かどうかのチェックが行われます。
リクエストの開始処理
クライアントからのリクエストによって、ASP.NETはChatHandlerクラスのBeginProcessRequestメソッドを呼び出します。
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { ChatClient c = new ChatClient(context, cb); lock (clients) { clients.Add(c); } return c; }
ASP.NETからリクエスト処理を要求されると、ChatHandlerは、ASP.NETから与えられたオブジェクトを保持させるためにChatClientクラスを生成し、clientsコレクションに追加します。以降、このリクエストに対するCometクライアントは、アイドルタイマーが経過するか、メッセージが発生するまでclientsコレクション内に保持されたままとなります。一方、BeginProcessRequestメソッドを処理したワーカスレッドはスレッドプールへ戻されて、他のクライアントからのリクエストを受け付け可能な状態となります。
メッセージ受信処理
Webブラウザから、ユーザーがメッセージを送信すると、「Default.aspx.cs」に対してクライアントコールバックが実行されます。
次のリストは、「Default.aspx.cs」から、ChatHandlerのメッセージ受信処理を呼び出すRaiseCallbackEventメソッドです。
public void RaiseCallbackEvent(string eventArgument) { string[] args = eventArgument.Split('&', '='); ChatHandler.AddMessage(ExtractValue("name", args), ExtractValue("message", args)); }
RaiseCallbackEventメソッドの中で、ブラウザから受信したフォームメッセージからname要素とmessage要素それぞれの設定値を抽出し、デコードします。その後、ChantHanlderクラスのAddMessageメソッドを呼び出します。
public static void AddMessage(string name, string msg) { string s = String.Format("<span id=\"dname\">{0}</span>" + "<span id=\"dtime\">{1:HH:mm:ss}</span>" + "<span id=\"dmessage\">{2}</span>", HttpUtility.HtmlEncode(name), DateTime.Now, HttpUtility.HtmlEncode(msg)); List<ChatClient> list; lock (clients) { list = new List<ChatClient>(clients); clients.Clear(); } foreach (ChatClient c in list) { c.Release(s); } }
AddMessageメソッドの中では、clientsコレクション内の全要素を送信用のコレクション(list)にコピーしてから、clientsコレクションを消去します。これは実際のクライアントへの送信がどのような方法で行われるかわからないため、clientsコレクションを同期したまま送信処理を実行しなくても済むようにするためです。
クライアントへの送信とASP.NETへの処理完了通知
CometClientクラスのReleaseメソッドは、指定された文字列をCometクライアントへ送信してから、ASP.NETへ処理の完了を通知します。非同期HTTPハンドラからASP.NETへ処理の完了を通知するには、BeginProcessRequestメソッドの引数で与えられたデリゲートを呼び出します。
internal void Release(string msg) { if (context.Response.IsClientConnected) { context.Response.Cache.SetCacheability(HttpCacheability.NoCache); context.Response.Write(msg); } completed = true; callback(this); }
最初に、該当クライアントとのコネクションがまだ有効かどうかをチェックします。もし有効であれば、クライアントキャッシュを不可に設定してから、メッセージ(またはアイドルタイマー経過時の空文字列)をレスポンス用ストリームへ書き出します。その後で、デリゲートを呼び出して処理の完了を通知します。なお、その直前に設定しているcompletedフィールドは、IAsyncResultインターフェイスがシグナル状態になったことを示すIsCompleteプロパティで返す値を保持するフィールドです。completedフィールドを参照するスレッドがReleaseメソッドを呼び出したスレッドと等しいかは不明なのでvolatileフィールドとして定義します。
なお、このReleaseメソッド内でレスポンスの出力と、ASP.NETへの完了通知の両方を実行するため、ChatHandlerのEndProcessRequestメソッドでは特に処理を行いません(アーカイブではデバッグ出力のみを実装しています)。
Web.config設定
Webアプリケーション固有のHTTPハンドラは、サイトのWeb.configファイルのsystem.web要素内に定義します。
<httpHandlers> <add verb="GET" path="*.chat"
type="ChatHandler"/> </httpHandlers>
ここでは、クライアントからのGETメソッドのリクエストで、URIの拡張子がchatの場合にChatHandlerクラスをHTTPハンドラとして呼び出すように定義しています。なお、App_Codeディレクトリに配置したソースファイルはASP.NETによって自動的にロードパスに含まれます。そのため、type属性には単にクラス名を指定するだけです。Webサイト固有のクラスをApp_Codeディレクトリに配置する場合は、それがWebサイト固有のクラスだということが明らかなのでネームスペースは不要だと筆者は考えます。
なお、拡張子chatというのは、Default.aspx内で、CometクライアントのリクエストURIで指定したファイル名です。実際には「Message.chat」というファイルは必要なく、ASP.NETはWeb.configファイルの設定からChatHandlerクラスの呼び出しだと判断します。
new Ajax.Request( "/CometChat/Message.chat", // Message.chatというファイル名で呼び出し { method: 'get', onSuccess: function(req, json) { if (!exitOn) { if (req.responseText != "") { new Insertion.Top('list', '<li id="message">' + req.responseText + '</li>'); } longPoll(); } }, ...(省略)... });
HTTPハンドラをIISへ配備する場合の設定
CometChatでは、拡張子chatを独自のHTTPハンドラに割り当てています。
Web.configファイルでこの設定を行っていますが、これはあくまでもASP.NET内部でHTTPハンドラを決定するのに利用される設定です。
IISをWebサーバーとして利用する場合には、それ以前の段階で、IISに対して拡張子chatをASP.NETの呼び出しとして設定する必要があります。この設定を行わないとIISは単純に該当ファイルが存在しないと見なしてステータス404をクライアントへ返してしまいます。
IISに対して、特定の拡張子を特定のISAPI DLL(ASP.NETはIISからはISAPI DLLとして扱われます)に割り当てるには、IISマネージャのWebサイトのプロパティで行います(図)。アプリケーションの構成ダイアログのマッピングタブから、aspxの設定内容を参照して割り当ててください。

IISの設定については、その他に2点注意事項があります。
1つは、既定で作成した仮想ディレクトリのプロパティでは既定のドキュメントにDefault.aspxが含まれていないことです。このため、「http://.../CometChat」のようにブラウザからアクセスすると、ディレクトリの読み取りは禁止されていますというエラーとなります。これを避けるには、プロパティのドキュメントタブで、[既定のコンテンツページを有効にする]をチェックし、Default.aspxを「追加」してください。
2点目は、既定ではASP.NETのバージョンが1.1になっていることです。ここではASP.NET 2.0をターゲットとしているため、プロパティの[ASP.NET]タブで、[ASP.NET バージョン]ドロップダウンリストから[2.0]を選択してください。ちなみに筆者の環境では実際には「2.0.50727」がドロップダウンリストに表示されました。
まとめ
ASP.NETの非同期HTTPハンドラを利用すると、それだけで単純なCometの実装ができます。
もちろん、プロダクトモードで利用するには、仮想ホストを利用した複数ロングポールの発行への対応、ここでは制限としたFirefoxなどで発生するprototype.jsとクライアントコールバック用にASP.NETが生成するJavaScriptの混在によるエラーへの対応、といった解決すべき問題があります。
しかし、Comet実装の要点であるサーバーサイドのWebアプリケーション実行モデルという点では、ASP.NETの非同期HTTPハンドラはすぐに利用できる有効な機構です。
参考資料
- MSDN ライブラリ(Microsoft Visual Studio 2005ドキュメント)
