リバース Ajax
第 1 回 Comet の紹介
ストリーミングおよびロング・ポーリングで、応答性に優れたクライアント・サーバー間通信を実現する
コンテンツシリーズ
このコンテンツは全#シリーズのパート#です: リバース Ajax
このコンテンツはシリーズの一部分です:リバース Ajax
このシリーズの続きに乞うご期待。
はじめに
この数年の間に、Web 開発は大きく進化しました。以前の Web は、互いにリンクされた静的 Web ページのリンクをクリックすることでブラウザーに表示されるページを更新し、ページがロードされるのを待つというものでしたが、今や Web はそれだけのものではなく、完全に動的なアプリケーションに Web からアクセスできることが求められています。このようなアプリケーションは、多くの場合、できる限り高速でほぼリアルタイムの動作をするコンポーネントを提供しなければなりません。今回から始まったこの 5 回の連載記事で、リバース Ajax の手法を使用してイベント駆動型の Web アプリケーションを開発する方法を学んでいきましょう。
第 1 回のこの記事で学ぶのは、リバース Ajax、ポーリング、ストリーミング、Comet、そしてロング・ポーリングです。さまざまなリバース Ajax 通信手法を実装する方法を学んで、それぞれの手法の利点と欠点を探ります。記事に記載する例に従うには、ソース・コードをダウンロードしてください。
Ajax、リバース Ajax、および WebSocket
Ajax (Asynchronous JavaScript and XML) は JavaScript で使用できるブラウザーの機能として、ページをリロードすることなく、スクリプトによって裏で Web サイトに HTTP リクエストを送信することができます。Ajax が登場してからは、すでに 10 年以上が経っています。Ajax の名前には XML という言葉が含まれていますが、Ajax リクエストではほぼあらゆるフォーマットのデータを送ることができます。なかでも最もよく使用されているのは、JavaScript の構文に似ていて、使用する帯域幅の少ない JSON フォーマットのデータです。リスト 1 に、郵便番号から、それに対応する場所の名前を取得する Ajax リクエストの例を記載します。
リスト 1. Ajax リクエストの例
var url = 'http://www.geonames.org/postalCodeLookupJSON?postalcode=' + $('#postalCode').val() + '&country=' + $('#country').val() + '&callback=?'; $.getJSON(url, function(data) { $('#placeName').val(data.postalcodes[0].placeName); });
上記のサンプル・コードの動作は、この記事のダウンロード可能なソース・コードに含まれている listing1.html で確認することができます。
基本的に、リバース Ajax はサーバーからクライアントにデータを送信することを可能にする概念です。標準的な HTTP Ajax リクエストでは、データはサーバーに送信されます。一方、サーバーが可能な限り短時間でクライアントにイベントを送信できるようにするためには (低遅延通信)、この記事で説明する特定の方法で、Ajax リクエストを送信するようにリバース Ajax をシミュレートすることができます。
HTML5 から生まれた WebSocket はごく最近の手法ですが、多くのブラウザー (Firefox、Google Chrome、Safari など) ですでにサポートされています。WebSocket は、双方向の全二重通信チャネルを可能にする手法です。ある種の HTTP リクエスト (WebSocket ハンドシェーク) といくつかの特殊なヘッダーを使用して接続をオープンすると、接続がそのまま維持されます。また、TCP ソケットを未加工のまま使用しているかのように、JavaScript でデータを作成および受信することができます。WebSocket については、連載の第 2 回で詳しく説明します。
リバース Ajax の手法
リバース Ajax の目標は、サーバーがクライアントに情報をプッシュできるようにすることです。Ajax リクエストはデフォルトではステートレスであるため、クライアントからサーバーに対して開始することしかできません。この制約は、応答性の高いクライアント・サーバー間通信をシミュレートする手法を使用することによって回避することができます。
HTTP ポーリングと JSONP ポーリング
ポーリングを行うには、クライアンがサーバーにリクエストを送信し、何らかのデータを要求しなければなりません。このリクエストは当然、単なる Ajax HTTP リクエストです。サーバーのイベントをできる限り早く取得するためには、ポーリング間隔 (リクエストを送信してから、次のリクエストを送信するまでの時間) をできるだけ短くしなければなりませんが、そこに欠点があります。ポーリング間隔を短くすると、クライアント・ブラウザーが送信するリクエストの数はそれだけ多くなりますが、ほとんどのリクエストに対して有用なデータが返されません。したがって、無駄に帯域幅と処理リソースを使用することになります。
図 1に示すタイムラインでは、クライアントによってポーリング・リクエストがいくつか送信されても、最初はそれに対するレスポンスにはデータが含まれていません。そして、その後でサーバーが受信した 2 つのイベントを取得するには、クライアントは次のポーリングまで待たなければなりません。
図 1. HTTP ポーリングによるリバース Ajax

JSONP ポーリングは基本的に HTTP ポーリングと同じですが、JSONP では、クロスドメイン・リクエスト (ドメイン外部でのリクエスト) を送信できるという点が異なります。リスト 1 では、郵便番号からそれに対応する場所の名前を取得するために JSONP が使用されています。一般に、JSONP リクエストはそのコールバック・パラメーターと返されるコンテンツ (実行可能 JavaScript コード) によって認識することができます。
JavaScript でポーリングを実装するには、setInterval
を使用して Ajax
リクエストを周期的に送信します (リスト 2 を参照)。
リスト 2. JavaScript ポーリング
setInterval(function() { $.getJSON('events', function(events) { console.log(events); }); }, 2000);
この記事のソース・コードに含まれているポーリングのデモに、ポーリング手法で使用している帯域幅が示されています。ポーリング間隔が短いとは言え、リクエストの一部はイベントを返していないことがわかります。リスト 3 に、サンプル・ポーリングの出力を記載します。
リスト 3. サンプル・ポーリングのデモの出力例
[client] checking for events... [client] no event [client] checking for events... [client] 2 events [event] At Sun Jun 05 15:17:14 EDT 2011 [event] At Sun Jun 05 15:17:14 EDT 2011 [client] checking for events... [client] 1 events [event] At Sun Jun 05 15:17:16 EDT 2011
JavaScript でのポーリングには、以下の利点と欠点があります。
- 利点: 極めて簡単に実装できて、サーバー・サイドに必要な特殊な機能はありません。また、すべてのブラウザーで機能します。
- 欠点: この手法にはスケーラビリティーがまったくないことから、めったに採用されません。例えば、100 のクライアントがそれぞれ 2 秒間隔でポーリング・リクエストを送信し、これらのリクエストの 30% がデータを何も返さないとしたら、帯域幅とリソースの損失がどれだけの量になるかを想像してみてください。
ピギーバック
ピギーバック・ポーリングは、不要なリクエスト (レスポンスとしてデータが返されないリクエスト) をすべて排除する傾向があるため、単なるポーリングよりも遥かに賢い手法です。ピギーバック・ポーリングでは、ポーリングの間隔が指定されず、ポーリング・リクエストは、クライアントがサーバーにリクエストを送信するときに一緒に送信されます。単なるポーリングとの違いは、レスポンスにあります。ピギーバック・ポーリングの場合、レスポンスはリクエストで要求されたデータ用のレスポンスと、サーバー・イベント (発生した場合) 用のレスポンスの 2 つに分かれています。図 2 に一例を示します。
図 2. ピギーバック・ポーリングによるリバース Ajax

ピギーバックの手法を実装すると、通常は、サーバーを対象とするすべての Ajax リクエストが混合レスポンスを返すことになります。実装例は、記事のダウンロードと以下のリスト 4 に示されています。
リスト 4. ピギーバックのサンプル・コード
$('#submit').click(function() { $.post('ajax', function(data) { var valid = data.formValid; // process validation results // then process the other part of the response (events) processEvents(data.events); }); });
リスト 5 に、ピギーバックによる出力を記載します。
リスト 5. ピギーバックの出力例
[client] checking for events... [server] form valid ? true [client] 4 events [event] At Sun Jun 05 16:08:32 EDT 2011 [event] At Sun Jun 05 16:08:34 EDT 2011 [event] At Sun Jun 05 16:08:34 EDT 2011 [event] At Sun Jun 05 16:08:37 EDT 2011
この出力には、フォーム検証の結果と併せて、レスポンスに追加されたイベントが表示されています。この手法にしても、利点と欠点があります。
- 利点: クライアントがリクエストを送信するタイミングを制御することから、データを含まないレスポンスが返されるようなクエストは送信されず、リソースの使用が抑えられます。また、すべてのブラウザーで機能し、サーバー・サイドに特殊な機能は必要ありません。
- 欠点: サーバー・サイドに蓄積されたイベントがクライアントにいつ配信されるかを知るには、クライアント・アクションによってイベントをリクエストしなければならないため、イベントがいつ配信されるかを知る手掛かりがありません。
Comet
ポーリングまたはピギーバック手法によるリバース Ajax には、かなりの制約があります。その制約とは、スケーリングしないこと、そして (イベントがサーバーに到着したと同時にブラウザーに到着する) 低遅延通信を行わないことです。Comet とは、リクエストをサーバーに送信した後、タイムアウトまたはサーバー・イベントが発生するまで長時間リクエストを存続させておく Web アプリケーション・モデルです。リクエストが完了すると、別の長時間存続する Ajax リクエストが送信されて、他のサーバー・イベントを待機します。Comet では、クライアントが明示的にリクエストしなくても、Web サーバーがクライアントにデータを送信することができます。
Comet の大きな利点は、各クライアントが常にサーバーに対する通信リンクをオープンにしていることです。したがって、サーバーはイベントが到着すると直ちにレスポンスをコミット (完了) することによって、クライアントにイベントをプッシュすることができます。あるいは、イベントを蓄積して一度に送信することも可能です。リクエストは長時間にわたってオープン状態に維持されるので、サーバー・サイドにはこれらの長時間存続するすべてのリクエストを処理する特殊な機能が必要となります。図 3 に一例を示します (サーバーの制約については、連載の第 2 回で詳しく説明します)。
図 3. Comet によるリバース Ajax

Comet には 2 つの実装タイプがあります。1 つはストリーミング手法を使用した実装、もう 1 つは ロング・ポーリング手法を使用した実装です。
HTTP ストリーミングを使用した Comet
ストリーミング手法では、1 つの永続接続だけがオープンになります。サーバー・サイドに到着するイベントはすべて同じ接続を通じて送信されるため、長時間存続するリクエストは 1
つしかありません (図 3 の
#1)。したがって、クライアント・サイドでは、同じ接続を通じて送られてくる複数のレスポンスのそれぞれを切り離す手段が必要になります。技術的な点から言うと、ストリーミングに一般的に使用されている
2 つの手法には、Forever Iframe (隠し iframe) と、JavaScript で Ajax リクエストを作成するために使用される XMLHttpRequest
のマルチパート機能があります。
Forever Iframe
Forever Iframe 手法では隠し iframe タグをページに組み込み、このタグの src
属性で、サーバー・イベントを返すサーブレット・パスを指定します。イベントが受信されるたびに、サーブレットは新しいスクリプト・タグを作成し、そこに JavaScript コードを書き込みます。このスクリプト・タグに、実行する iframe コンテンツが追加されます。
- 利点: 簡単に実装できて、iframe をサポートするすべてのブラウザーで機能します。
- 欠点: すべての接続およびデータは、ブラウザーが HTML タグを使って処理するため、信頼性のあるエラー処理を実装する手段も、接続の状態を追跡する手段もありません。したがって、どちらかの側で接続が切断されたとしても、わかりません。
マルチパート XMLHttpRequest
もう一方の手法は、上記の手法よりも信頼性があります。それは、一部のブラウザー (Firefox など) でサポートされているマルチパート・フラグを XMLHttpRequest
オブジェクトに設定するというものです。Ajax
リクエストが送信されると、そのリクエストはサーバー・サイドでオープンされたままになります。イベントが到着するたびに、マルチパート・レスポンスが同じ接続を通じて書き込まれます。リスト 6 に一例を記載します。
リスト 6. マルチパート・ストリーミング・リクエストをセットアップするサンプル JavaScript コード
var xhr = $.ajaxSettings.xhr(); xhr.multipart = true; xhr.open('GET', 'ajax', true); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { processEvents($.parseJSON(xhr.responseText)); } }; xhr.send(null);
サーバー・サイドでは、事態はもう少し複雑になります。まず、マルチパート・リクエストをセットアップし、それから接続を保留しなければなりません。リスト 7 に、HTTP ストリーミング・リクエストを保留する方法を示します (API については、連載の第 3 回で詳しく説明します)。
リスト 7. Servlet 3 API を使用して、サーブレットで HTTP ストリーミング・リクエストを保留する
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // start the suspension of the request AsyncContext asyncContext = req.startAsync(); asyncContext.setTimeout(0); // send the multipart separator back to the client resp.setContentType("multipart/x-mixed-replace;boundary=\"" + boundary + "\""); resp.setHeader("Connection", "keep-alive"); resp.getOutputStream().print("--" + boundary); resp.flushBuffer(); // put the async context in a list for future usage asyncContexts.offer(asyncContext); }
これで、イベントが発生するたびに、保留中のすべての接続に対して繰り返し処理を行ってデータを書き込めるようになります (リスト 8 を参照)。
リスト 8. Servlet 3 APIを使用して、保留中のマルチパート・リクエストに対してイベントを送信する
for (AsyncContext asyncContext : asyncContexts) { HttpServletResponse peer = (HttpServletResponse) asyncContext.getResponse(); peer.getOutputStream().println("Content-Type: application/json"); peer.getOutputStream().println(); peer.getOutputStream().println(new JSONArray() .put("At " + new Date()).toString()); peer.getOutputStream().println("--" + boundary); peer.flushBuffer(); }
この記事に付属のダウンロード・ファイルの Comet-streaming フォルダーに、HTTP ストリーミングを実際に行っているファイルが含まれています。サンプル・コードを実行してホーム・ページを開くと、サーバーにイベントが到着すると同時に、イベントが非同期で表示される様子がわかります。また、Firebug コンソールを開いてみると、オープン状態の Ajax リクエストは 1 つしかないことがわかります。さらに注意深く見てみると、複数の JSON レスポンスが「Response (レスポンス)」タブに追加されていることもわかります。
図 4. FireBug で表示した HTTP ストリーミング・リクエスト

例の如く、この手法にも利点と欠点があります。
- 利点: 1 つの永続接続だけがオープンになります。これは、帯域幅の使用を最も抑える Comet の手法です。
- 欠点: マルチパート・フラグはすべてのブラウザーでサポートされているわけではありません。広く使用されているライブラリーのいくつか (Java の CometD など) では、バッファリングの問題が報告されています。例えば、データのチャンク (マルチパート) がバッファーに入れられると、その接続が完了した時点またはバッファーがフルになった時点でしか送信することができません。そのため、レイテンシーが予想以上に大きくなることがあります。
HTTP ロング・ポーリングを使用した Comet
ロング・ポーリング手法には、接続をオープンする手法が伴います。接続はサーバーによってオープン状態のまま維持され、イベントが発生すると同時にレスポンスがコミットされると、接続がクローズされます。その直後に、クライアントは新しいロング・ポーリング接続をオープンして、新規イベントが到着するのを待機します。
HTTP ロング・ポーリングを実装するには、スクリプト・タグを使用することも、単なる XMLHttpRequest
オブジェクトを使用することもできます。
スクリプト・タグ
iframe の場合と同じく、スクリプト・タグを使用する目的は、スクリプト・タグをページに追加してスクリプトを実行させることです。サーバーは接続を保留し、イベントが発生した時点でスクリプトの内容をブラウザーに送信し、それから別のスクリプト・タグを開いて次のイベントを取得します。
- 利点: HTML をベースとしているため、この手法は極めて簡単に実装できて、ドメイン間でも機能します (デフォルトでは、
XMLHttpRequest
に他のドメインやサブ・ドメインでのリクエストは許可されません)。 - 欠点: iframe 手法と同じように、エラー処理がないこと、そして接続の状態を持てないこと、または接続を中断できないことが欠点として挙げられます。
XMLHttpRequest
ロング・ポーリング
Comet を実装するもう 1 つの、そして推奨される手法は、サーバーに対して Ajax リクエストを開始し、レスポンスを待機することです。サーバーには、サーバー・サイドでリクエストを保留するための特定の機能が必要となります。イベントの発生と同時に、サーバーは保留されているリクエストに対してレスポンスを送信し、サーブレット・レスポンスの出力ストリームをクローズするのとまったく同じようにリクエストをクローズします。クライアントがレスポンスを取り込むと、改めて長期間存続する Ajax リクエストをサーバーに対して開始します (リスト 9 を参照)。
リスト 9. ロング・ポーリング・リクエストをセットアップするサンプル JavaScript コード
function long_polling() { $.getJSON('ajax', function(events) { processEvents(events); long_polling(); }); } long_polling();
上記のコードはバックエンドで、同じく Servlet 3 API を使ってリクエストを保留します。この点は HTTP ストリーミングの場合と同じですが、マルチパート処理コードはまったく必要ありません。リスト 10 に一例を記載します。
リスト 10. ロング・ポーリング Ajax リクエストを保留する
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { AsyncContext asyncContext = req.startAsync(); asyncContext.setTimeout(0); asyncContexts.offer(asyncContext); }
イベントを受信したら、保留中のすべてのリクエストを完了させればよいのです (リスト 11 を参照)。
リスト 11. イベントが発生した時点でロング・ポーリング Ajax リクエストを完了させる
while (!asyncContexts.isEmpty()) { AsyncContext asyncContext = asyncContexts.poll(); HttpServletResponse peer = (HttpServletResponse) asyncContext.getResponse(); peer.getWriter().write( new JSONArray().put("At " + new Date()).toString()); peer.setStatus(HttpServletResponse.SC_OK); peer.setContentType("application/json"); asyncContext.complete(); }
記事に付属のダウンロード可能なソース・ファイルの comet-long-polling
フォルダーに、ロング・ポーリングのサンプル Web アプリケーションが含まれています。このアプリケーションは、mvn jetty:run
コマンドを使用して実行することができます。
- 利点: エラー処理システムおよびタイムアウト管理と併せて、クライアント・サイドに簡単に実装することができます。この信頼性のある手法では、接続は永続的ではないため
(アプリケーションで多数のクライアントに対応する上で好都合です)、サーバー・サイドで接続をラウンドトリップすることも可能です。さらに、この手法は単純な Ajax
リクエストを送信して
XMLHttpRequest
オブジェクトを利用しているだけなので、すべてのブラウザーで機能します。 - 欠点: 他の手法と比べると、大きな欠点はありません。ただし、これまで説明したすべての手法と同じように、この手法もステートレスな HTTP 接続に依存していることには変わりないため、サーバー・サイドで一時的に接続を保留するための特殊な機能が必要です。
推奨事項
最近のブラウザーはいずれも CORS (Cross-Origin Resource Sharing) 仕様をサポートしています。この仕様によって XHR はドメイン間でリクエストを実行できるようになるため、スクリプト・ベースの手法や iframe ベースの手法は推奨されなくなっています。
リバース Ajax のための Comet を実装および使用するのに最善の方法は、実際の接続ハンドルとエラー処理を提供する XMLHttpRequest
オブジェクトを使うことです。マルチパート・フラグはすべてのブラウザーでサポートされているわけではないこと、さらにマルチパート・ストリーミングはバッファリングの問題を引き起こす可能性があることを考えると、XMLHttpRequest
オブジェクト (サーバー・サイドで保留される単純な Ajax リクエスト) で HTTP ロング・ポーリングを行うという方法で Comet を使用することが推奨されます。Ajax をサポートするブラウザーであれば、例外なくこの手法をサポートします。
まとめ
今回の記事では、リバース Ajax 手法を紹介しました。リバース Ajax 通信を実装するさまざまな手法を詳しく探り、それぞれの実装に伴う利点と欠点を説明しました。どの実装手法が最適であるかは具体的な状況とアプリケーションの要件によって左右されますが、一般的に言って、Ajax ロング・ポーリング・リクエストを使用した Comet が、低遅延通信、タイムアウトおよびエラー検出、単純さ、そしてあらゆるブラウザーとプラットフォームによる十分なサポートという点で最善の選択肢になります。
連載の第 2 回では、リバース Ajax 手法の 3 つめとして WebSocket について説明するのでお見逃しなく。まだすべてのブラウザーでサポートされているわけではないものの、WebSocket は間違いなく、リバース Ajax の非常に優れた通信媒体となるはずです。WebSocket は、HTTP 接続のステートレス性に関するすべての制約を取り除くためです。第 2 回では、Comet 手法と WebSocket 手法によってもたらされるサーバー・サイドの制約についても取り上げます。
ダウンロード可能なリソース
- このコンテンツのPDF
- Article source code (reverse_ajaxpt1_source.zip | 17KB)
関連トピック
- ウィキペディアで以下のエントリーを読んでください。
- 「Exploring Reverse AJAX」(Google Maps .Net Control ブログ、2006年8月): リバース Ajax のいくつかの手法を紹介しています。
- 「JSONP によるクロスドメインの通信: 第 1 回 JSONP と jQuery を組み合わせ、強力なマッシュアップを迅速に作成する」(developerWorks、2009年2月): あまり知られていないクロスドメインの呼び出し手法 (JSONP) と柔軟な JavaScript ライブラリー (jQuery) とを組み合わせ、強力なマッシュアップを驚くほど迅速に作成する方法を学んでください。
- 「Cross-Origin Resource Sharing (CORS)"」仕様 (W3C、2010年7月): XHR がクロスドメイン・リクエストを実行できるようにするこのメカニズムについて詳しく学んでください。
- 「Ext JS で作る AJAX アプリケーション」(developerWorks、2008年7月): この記事では、Ext JS の基盤となっている JavaScript によるオブジェクト指向的な設計概念の概要、そしてリッチ・インターネット・アプリケーションの UI 要素に Ext JS フレームワークを適用する方法を説明しています。
- 「JavaScript フレームワークの比較」(developerWorks、2010年2月): JavaScript 開発を大幅に強化するフレームワークの概要を説明しています。
- 「Ajax をマスターする: 第 2
回 JavaScript と Ajax を使用して行う非同期要求」(developerWorks、2006年1月): Ajax と
XMLHttpRequest
オブジェクトを使用して、ユーザーにサーバーからのレスポンスを待たせることのないリクエスト/レスポンス・モデルを作成する方法を学んでください。 - 「モバイル Web 用の Ajax アプリケーションを作成する」(developerWorks、2010年3月): Ajaxを使用して特定のブラウザーに依存しないスマートフォン用 Web アプリケーションを構築する方法を学んでください。
- 「Where and when to use Ajax in your applications」(developerWorks、2008年2月): Ajax を利用して、Web サイトを改善すると同時に不快なユーザー・エクスペリエンスを防ぐ方法について説明しています。
- 「Web 2.0 アプリケーションのパフォーマンスを改善する」(developerWorks、2009年12月): この記事では、さまざまなブラウザーのキャッシュ・メカニズムを探っています。
- 「Introducing JSON」(JSON.org): JSON 構文を学んでください。
- developerWorks Web architecture ゾーン: さまざまな Web ベースのソリューションを話題にした記事を調べてください。
- developerWorks podcasts: ソフトウェア開発者向けの興味深いインタビューとディスカッションを聞いてください。
- リッチ・インターネット・アプリケーションを構築するためのクロスブラウザー JavaScript ライブラリー、ExtJS を入手してください。
- XAMPP は、Apache、PHP、MySQL などを簡単にインストールする手段となります。
- IBM のソフトウェアを無料で試してみてください。試用版をダウンロードすることも、オンライン評価版にログインすることも、Sandbox 環境で製品を操作することも、クラウドを介して IBM 製品にアクセスすることもできます。100 を超える IBM 製品の評価版のなかから選ぶことができます。