トップ » 技術記事 » チャット作成で学ぶWebリモーティング(4) - DWRのReverse Ajaxで簡単Comet!

チャット作成で学ぶWebリモーティング(4) - DWRのReverse Ajaxで簡単Comet!

タグ: Ajax Comet DWR Java javascript

どうも皆さん、こんにちは。白石です。

今回はWebリモーティングに関する連載の第四回目です。前回の記事では、DWRのバージョン2から利用可能になったアノテーションを用いて、リモートプロキシやDTOを設定する方法を紹介しました。今回は、これもバージョン2から利用できるようになった「Reverse Ajax」と呼ばれるテクニックを用いて、チャット画面の更新をよりリアルタイムに行えるようにしたいと思います。

これまでのチャットの問題点

さて今回は、これまでのチャットアプリが抱えていた問題点を、DWR2の機能を用いて改善するのが目的です。その問題点とは、「チャット画面の更新を、1秒おきのポーリングによって実現していた」ことです。
チャット画面を表すHTMLファイル、chat.htmlの中で、ポーリングに関する部分を抜き出してみてみましょう。

  var refreshTimer;

  // ページ初期化時に呼び出される関数
  function init() {
    …(略)…
    showMessages();
  }

  // メッセージを表示
  function showMessages() {
    abortRefresh();
    // リモーティングを用いてメッセージを取得し、表示
    ChatService.getAllMessages({
      callback: _showMessages,
      errorHandler: onError
    });
  }

  function _showMessages(messages) {
    // メッセージを画面に表示
    …(略)…

    // 次のチャット更新をスケジューリング
    refreshTimer = setTimeout(showMessages, 1000);
  }

  // チャットのリフレッシュを中止する
  function abortRefresh() {
    if (refreshTimer) {
      clearTimeout(refreshTimer);
      refreshTimer = null;
    }
  }

window.setTimeout()を用いて1秒おきにチャットの全メッセージを取得し、リフレッシュを行っているのが分かりますね。しかしこの方法には以下のような問題点があります。

  • 人の書き込みを見るのが最大1秒遅れる (ネットワークの遅延によってはさらに遅れる)
  • 更新がない場合でも常にリクエストが発生するので、サーバ負荷が高い

こうした問題をスマートに解決する方法があります。それは「Comet」と呼ばれる手法です。

Cometとは

Cometとは、昨年 (2006年) くらいから大きく取り上げられるようになった手法で、どういうものかというと「HTTPを使用した、データのプッシュ配信の仕組み」です。データのプッシュ配信とはつまり、サーバからクライアントに向けてデータを配信できるということ。通常HTTPでは、クライアントからのリクエスト→レスポンスという流れしか存在しないのですが、そこをうまくカバーするための仕組みがCometです (Cometは彗星と言う意味。データが彗星のように降ってくる、と考えればイメージしやすいですね)。

このチャットアプリでCometを使用したとすると、「誰かが書き込みを行ったら、チャットに参加している全クライアントに対してメッセージをプッシュ配信する」という方法が採れます。これによってリアルタイム性が向上するだけではなく、無駄なポーリングを行う必要がないのでサーバの負荷も軽減されます。

ただ、Cometには標準があるわけでもないので、実装方法によってコードはかなり変わってきます。たとえばCometdという実装であれば、Bayeuxプロトコルと言うCometプロトコルを策定した上で、Dojo Toolkitと言うJavaScriptフレームワークと連携してCometを実現しています。Apache Tomcatも、バージョン6からCometを実現できるようになりましたが、それは特定のインターフェースをimplementsしたサーブレット、と言う形で実装します。

そして、DWR2では「Reverse Ajax」と呼ばれる仕組みで実現しています。今回は、そのReverse Ajaxについてお話したいと思います。

Reverse Ajaxの利用方法

「Reverse Ajax」とは、なかなか実体を想像しにくいへんてこなネーミングですが、どういうものかというと「サーバサイドのJavaコードから、JavaScriptコードを操作することができる」と言うものです。リモーティングの手法により、JavaScriptからJavaをいじることができるようにはなっていたわけですが、DWR2ではJavaからJavaScriptをいじれるようになったのです。この発想の逆転が「Reverse」というわけで、僕も最初見たとき衝撃を受けました。

詳しい話はさておき、早速Reverse Ajaxを使ってチャットアプリを改良してみましょう。以下のような流れで進めます。

設定作業:
・web.xmlの修正
・Webページ内で、ReverseAjaxを使用するよう宣言

コードの修正:
・クライアントのJavaScriptコード修正
・サーバサイドのJavaコード修正

設定作業
web.xmlの修正

まず、Reverse Ajaxを使用するにはサーブレットに対する設定が必要です。初期化パラメータactiveReverseAjaxEnabledにtrueをセットすればOKです。

<!– DWRServlet –>
<servlet>
  <servlet-name>dwr-invoker</servlet-name>
  <display-name>DWR Servlet</display-name>
  <description>Direct Web Remoter Servlet</description>
  <servlet-class>
    org.directwebremoting.servlet.DwrServlet
  </servlet-class>
  <init-param>
    <param-name>debug</param-name>
    <param-value>true</param-value>
  </init-param>
  <init-param>
    <param-name>activeReverseAjaxEnabled</param-name>
    <param-value>true</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
Webページ内で、ReverseAjaxを使用するよう宣言

次に、Webページ内のJavaScriptに以下のコードを追加します。

dwr.engine.setActiveReverseAjax(true);

このコード呼び出し以降、サーバからのデータプッシュが可能になります。今回のチャットアプリでは、ページの読み込みが終わった瞬間 (body.onload) にこのコードが実行されるよう修正しました。

コードの修正

上のたった2つの手順だけで、Reverse Ajaxを使用する準備は整っています。次は、Reverse Ajaxを前提としたコードの修正を行いましょう。

クライアントのJavaScriptコード修正

先ほども言ったとおり、DWRのReverse Ajaxを用いれば、サーバサイドのJavaからJavaScriptを操作できます。そうなると、クライアントが能動的にチャットデータを取りに行く必要が全くなくなります。というわけで、この記事の最初の方に挙げたポーリングに関するロジックを全て除去します

サーバサイドのJavaコード修正

今回最も重要なのがこのトピックです。Reverse Ajaxを利用するには、サーバサイドでどのようなコードを書けばよいのか?

その具体的なコードをお見せする前に、DWR2の「スクリプトスコープ」について軽く説明します。

スクリプトスコープ
いきなり話が脇道にそれて申し訳ありませんが、Reverse Ajaxを理解するにはこの「スクリプトスコープ」に関する理解が不可欠です。なので、少しお付き合いください。

「スコープ」と言えば、JavaでWeb開発を行ってきた皆さんにとってはおなじみの用語ですね。サーブレットAPIではpage、request、session、applicationと言ったスコープが利用できます。
DWRでは、Ajaxアプリケーションにとっては前述のスコープだけでは不足しているとして、独自のスコープを定義しました。それがスクリプトスコープで、「Webページ上で宣言されたJavaScript変数と同じ生存期間を持つスコープ」のことです。具体的には、Webページ上で以下のようにして宣言した変数と同じ生存期間を持つということです。

<script type="text/javascript">
var v;
</script>

この変数がいつまで有効かはお分かりですね。ユーザがこのページに留まり続ける限り有効で、このページから移動したり、ウィンドウを閉じた場合などに無効になります。同じページを表示していても、ブラウザウィンドウが異なれば、変数の有効範囲は別々になります。

このスクリプトスコープは、DWR2で新たに導入された様々な機能で利用されます。しっかり、その概念を理解しておきましょう。

話を本筋に戻す
で、Reverse Ajaxを使用するサーバサイド側のコードに戻ります。Javaコードをどのように修正する必要があるかと言うと、「誰かがチャットに書き込みを行ったら、クライアントに対してメッセージを配信する」ようにします。チャットに書き込みを行うメソッドは「addMessage()」でしたので、そのメソッド内を以下のように修正します。

public void addMessage(ChatMessageDTO messageDto) {
  messageDto.setTimestamp(timestampFormat.format(new Date()));
  messages.add(messageDto);

  // Reverse Ajaxを用いてメッセージを配信

  // このメソッドの呼び出し元であるページを取得
  WebContext wc = WebContextFactory.get();
  String currentPage = wc.getCurrentPage();

  // そのページに紐付くスクリプトセッションを全て取得
  Collection<ScriptSession> sessions =
    (Collection<ScriptSession>) wc.getScriptSessionsByPage(currentPage);
  for (ScriptSession sess : sessions) {
    // スクリプトセッションに対し、JavaScript呼び出しを追加
    sess.addScript(
      new ScriptBuffer("_showMessages(")
        .appendData(messages)
        .appendScript(");"));
  }
}

WebContextという新しいクラスが出てきました。WebContextは、現在のリモートメソッド呼び出しに伴う様々な情報を取得するための窓口となるクラスで、HttpServletRequestやHttpServletResponseも取得できます。
まずは、この呼び出しを行ったページのURLを「getCurrentPage()」メソッドにより取得しています。

次に、スクリプトセッションの取得です。WebContext.getScriptSessionsByPage()メソッドを用いると、そのページに関連する全てのスクリプトスコープを取得できます (そのスコープを表すのがScriptSessionです)。

そして、Cometを行っていると言えるのがScriptSession.addScript()を呼び出している部分です。
ここでは、クライアント上で定義されたJavaScript関数「_showMessages」に対し、チャットメッセージを引数に与えて呼び出しています。ScriptBufferという、java.lang.StringBufferに似たクラスを使っているのがお分かりでしょう。ScriptBuffer.appendData()を呼び出すと、JavaオブジェクトをJavaScriptオブジェクトに変換してバッファに追加できます。ScriptBuffer.appendScript()は、渡された文字列がそのままJavaScriptコードとして追加されます。
サーバサイドのJavaコードにJavaScriptのコードが混じるのは、なんだか新鮮な感覚ですね。

こうして、addMessage()の呼び出しが行われると、チャットに参加している全クライアントの_showMessages()関数が呼び出され、結果として全員のチャット画面がリフレッシュされます。メッセージの書き込みを行ってから、クライアントの画面が更新されるまでのタイムラグもほとんどなく、リアルタイム性がかなり向上しています。以下のリンクからソースコードを入手して、自分のPC上でたくさんクライアントを立ち上げ (たくさんのブラウザウィンドウからチャットにアクセスして)、ぜひ試してみてください。

今回のソースコード

Reverse Ajaxの実際

ここからは、少し高度な話題として、Reverse Ajaxがどのように実現されているかについて少し触れ、それに伴う設定などを説明したいと思います。

Cometを用いるとサーバからのデータプッシュを容易に実現できます。しかし、実際HTTPというプロトコルはサーバからクライアントへの通信を行うようには設計されていません。では、どのようにして実装されているのでしょうか?

もっとも簡単な実装方法は、HTTPのレスポンスにサーバがデータを流し込み続けると言うものです。これは、ループ処理内でHttpServletResponseへの書き込みを続けることで実現できます。こうすれば、最初に一度クライアントからリクエストを受けさえすれば、データプッシュが可能と言うわけです。

しかし、この方法には大きな問題が二つあります。
一つはサーバ側の問題。サーブレットコンテナは、一つのHTTPリクエストを一つのスレッドで処理するよう実装されていることがほとんどですが、前述のような実装を行ってしまうと、そのスレッドの処理がいつまでたっても終了しません。つまり、接続しているクライアントの数だけスレッドがロックされてしまうのです。これでは、サーバサイドのリソースがいくらあっても足りません。

もう一つはクライアント側の問題。通常のWebブラウザは、一つのWebサイトに対するHTTP同時接続数が「2」に制限されています (これはサーバ側の負荷を減らすのが目的で、RFCでも推奨されています)。しかし、先ほどのような方法ではComet用に必ず接続数を一つ消費してしまいます。同時接続数の上限を超えてHTTPリクエストを行おうとすると、接続に空きができるまでブラウザによってウェイトさせられますので、下手をするとAjaxクライアントがフリーズしてしまいます。

こうした様々な問題に対処するため、DWR2に限らず様々なComet実装は、ポーリングをうまく組み合わせています。基本的には前述の方法なのですが、短い間隔でいったんレスポンスをコミットし (これによりサーバスレッドが解放される)、再度クライアントから接続してもらいます。こうした動作は全てライブラリによって隠蔽されるので、プログラムはそれを意識する必要はありません。

ここまでの説明がお分かりでしょうか?では最後に、Cometのパフォーマンスとリソース消費量を左右する、サーブレットの初期化パラメータを紹介したいと思います。DWRでは、Cometのモードを以下の三つに分類しています。

フルストリーミングモード

可能な限りレスポンスに書き込みを続けるモードです。スレッドは長時間ロックされますが、クライアントからの再接続回数は最少となるため、リアルタイム性は良好です。同時接続ユーザの少ないサイトに有効です。
このモードがデフォルトなので、必要な設定はReverse Ajaxの有効化だけです。

<servlet>
  <servlet-name>dwr-invoker</servlet-name>
  <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
  <init-param>
    <param-name>activeReverseAjaxEnabled</param-name>
    <param-value>true</param-value>
  </init-param>
</servlet>
Early Closing Mode

レスポンスを早めにコミットし、スレッドのロック時間を短くできます。クライアントからの再接続回数が増えるのでリアルタイム性が多少落ちますが、サーバのリソース消費量を調整できます。レスポンスに対する最初の書き込みが行われてから何ミリ秒待機するかを、以下の初期化パラメータで指定することができます (activeReverseAjaxEnabled=trueの設定も必要)。
デフォルトは-1で、60秒間待機します。これが上のフルストリーミングモードです。

<init-param>
  <param-name>maxWaitAfterWrite</param-name>
  <param-value>500</param-value>
</init-param>
ポーリングモード

単純なポーリングで、前回までのチャットアプリと同様の動作をします。
以下の初期化パラメータを指定すれば有効になります (activeReverseAjaxEnabled=trueの設定も必要)。

<init-param>
  <param-name>org.directwebremoting.extend.ServerLoadMonitor</param-name>
  <param-value>org.directwebremoting.impl.PollingServerLoadMonitor</param-value>
</init-param>
<init-param>
  <param-name>disconnectedTime</param-name>
  <param-value>60000</param-value>
</init-param>

最後に

以上で、Reverse Ajaxに関する説明は終わりです。最後は結構難しい話でしたが、いかがでしたでしょうか。次回は、Spring FrameworkとDWRを連携させる方法についてお話したいと思います。

ご意見、ご感想お待ちしています。

Series Navigation«DWRのアノテーションを使用するDWRとSpringの連携»

執筆者紹介

shiraishi

shiraishi

最近書いてばっかりいます。 眠いとおんなじことばかり書きます。 そして、大概眠いです。

TrackBack URL :