The Apache Tomcat Servlet/JSP Container

Apache Tomcat 6.0

Apache Logo

Apache Tomcat 6.0

Advanced IO と Tomcat

はじめに

コネクタの基盤として APR [Apache Portable Runtime] または NIO [New I/O] の API を使うことで,Tomcat は, Servlet API 用サポートで用意されるような通常のブロッキング IO を超える数多くの拡張機能を用意できる。

重要な注意: これらの機能を使うには APR または NIO の HTTP コネクタが必要である。古典的な java.io HTTP コネクタと AJP コネクタはそれらをサポートしない。

Comet サポート

Comet サポートはサーブレットが IO を非同期的に処理することを可能にする。 (常にブロッキング read を使うかわりに) read 用データがコネクション上に準備された時にイベントを受け取ること, および (おそらくどこか他のソースから発生したイベントに応答して) データをコネクションに非同期的に write back することができる。

CometEvent

org.apache.catalina.CometProcessor インタフェース [void event(CometEvent event) throws IOException, ServletException を唯一のメソッドとしてもつ] を実装するサーブレットは,生じたイベントによって,普通の service メソッドのかわりに event メソッドが呼び出される。 event オブジェクトは,普通の方法で使える普通の request と response のオブジェクトへのアクセスを与える。 主な違いは,それらのオブジェクトが,BEGIN イベントの処理から END または ERROR イベントの処理までのすべての時点で有効であり,完全に機能することである。 次のイベント型が存在する:

  • EventType.BEGIN: コネクションの処理の最初に呼び出される。 その request と response のオブジェクトを使って,[サーブレットの] 関係するフィールドを初期化するために使える。 このイベントの処理の終了時から,end または error イベントの処理の開始時まで, response オブジェクトを使って,データをその開いているコネクション上で書き込むことができる。 response オブジェクトと depedent [ママ,おそらく dependent,従属する] OutputStream および Writer はまだ synchronized されていないから, 複数のスレッドからアクセスするときは同期化が必須なことに注意せよ。 [この] 初期イベントを処理した後,リクエストがコミットされたと見なされる。
  • EventType.READ: これは入力データが準備され,ブロックすることなく 1 回 read できることを示す。 InputStream または Reader の available と ready のメソッドがブロッキングの危険の有無の判定に使える。 つまり,サーブレットはデータが available であると報告されているあいだは read すべきであり, データが available であると報告されているあいだは,ブロックすることなくもう1回 read できる。 read error に遭遇した時は,servlet はその例外を正しく伝播させることによって,それを報告しなければならない。 例外の送出は error イベントを発生させ,そしてコネクションが閉じられる。 もう一つの方法として, 例外を catch し,サーブレットが使用している限りのデータ構造の後始末をし,それから event の close メソッドを呼び出してもよい [原文は英語として破格。この訳文は一つの解釈です]。 この [イベントで起動された event] メソッドの実行以外 [の文脈] で,request オブジェクトからデータを読もうとすることは許されない。
    Windows など,プラットフォームによっては,クライアントとの接続断が READ イベントで示される。 [接続が切れたとき] stream からの読込みが -1 の戻り値になるかもしれないし, IOException または EOFException [これは IOException からの派生クラス] になるかもしれない。 忘れずに,これら三つの場合すべてを正しく処理すること。 もしも君が IOException を catch しなければ,Tomcat が君にかわってエラーを catch し, ただちに君のイベントチェーンを ERROR で呼び出すだろう。 君はその時点でエラーを知らされることになる。 [これは,ここの前半で述べられている例外の伝播ですから,catch しないことも正しい処理のひとつでしょう]
  • EventType.END: End はリクエストの処理を終了するために呼び出される。 begin [に対して呼び出された event] メソッド で初期化したフィールドは,[このイベントの処理で] リセットすべきである。 このイベントの処理後,request と response のオブジェクトは,その従属するオブジェクトも含めてリサイクルされ, 他のリクエストを処理するために使われる。 End はデータが準備され,request の入力で EOF に達したときにも呼び出される (これは普通,クライアントがリクエストをパイプライン化したことを意味する)。
  • EventType.ERROR: IO 例外または類似の回復不可能なエラーがコネクションで生じた場合に, コンテナにより呼び出される。 begin メソッド [上記参照] で初期化したフィールドはリセットすべきである。 このイベントの処理後,request と response のオブジェクトは,その従属するオブジェクトも含めてリサイクルされ, 他のリクエストを処理するために使われる。

よりきめ細かいイベント処理を可能にするイベント派生型がいくつかある (注意: これらのイベントのいくつかは org.apache.catalina.valves.CometConnectionManagerValve valve の使用を必要とする):

  • EventSubType.TIMEOUT: コネクションが時間切れになった (ERROR の派生型)。 この ERROR 型は致命的ではなく,サーブレットが event の colse メソッドを使わない限り, コネクションは閉じられないことに注意せよ。
  • EventSubType.CLIENT_DISCONNECT: クライアントのコネクションが閉じられた (ERROR の派生型)。 method of the event. [←おそらく原文の編集ミス]
  • EventSubType.IOEXCEPTION: 不正なコンテント,例えば,不正な chunk ブロックなどの IO 例外が生じた (ERROR の派生型)。
  • EventSubType.WEBAPP_RELOAD: web アプリケーションが再ロードされた (END の派生型)。
  • EventSubType.SESSION_END: サーブレットがセッションを終了した (END の派生型)。

上述のように Comet リクエストの典型的なライフサイクルは BEGIN -> READ -> READ -> READ -> ERROR/TIMEOUT のようなイベントの列の中にある。 どの時点でもサーブレットは event オブジェクトの close メソッドを使ってリクエストの処理を終了できる。

CometFilter

通常のフィルタと同様,comet イベントが処理されるとき,フィルタ・チェーンが呼び出される。 これらのフィルタは CometFilter インタフェース (これは通常の Filter インタフェースと 同じようにはたらく) を実装すべきである。 そして通常のフィルタと同じように,デプロイメント記述子で宣言され,マップされるべきである。 イベントを処理するときのフィルタ・チェーンは,すべて普通のマッピング規則にマッチし, かつ CometFilter インタフェースを実装するフィルタだけを含むだろう。

コード例

次の疑似コード・サーブレットは,非同期的な chat 機能を上記の API を使って実装している:

public class ChatServlet
    extends HttpServlet implements CometProcessor {

    protected ArrayList<HttpServletResponse> connections = 
        new ArrayList<HttpServletResponse>();
    protected MessageSender messageSender = null;
    
    public void init() throws ServletException {
        messageSender = new MessageSender();
        Thread messageSenderThread = 
            new Thread(messageSender, "MessageSender[" + getServletContext().getContextPath() + "]");
        messageSenderThread.setDaemon(true);
        messageSenderThread.start();
    }

    public void destroy() {
        connections.clear();
        messageSender.stop();
        messageSender = null;
    }

    /**
     * Process the given Comet event.
     * 
     * @param event The Comet event that will be processed
     * @throws IOException
     * @throws ServletException
     */
    public void event(CometEvent event)
        throws IOException, ServletException {
        HttpServletRequest request = event.getHttpServletRequest();
        HttpServletResponse response = event.getHttpServletResponse();
        if (event.getEventType() == CometEvent.EventType.BEGIN) {
            log("Begin for session: " + request.getSession(true).getId());
            PrintWriter writer = response.getWriter();
            writer.println("<!doctype html public \"-//w3c//dtd html 4.0 transitional//en\">");
            writer.println("<head><title>JSP Chat</title></head><body bgcolor=\"#FFFFFF\">");
            writer.flush();
            synchronized(connections) {
                connections.add(response);
            }
        } else if (event.getEventType() == CometEvent.EventType.ERROR) {
            log("Error for session: " + request.getSession(true).getId());
            synchronized(connections) {
                connections.remove(response);
            }
            event.close();
        } else if (event.getEventType() == CometEvent.EventType.END) {
            log("End for session: " + request.getSession(true).getId());
            synchronized(connections) {
                connections.remove(response);
            }
            PrintWriter writer = response.getWriter();
            writer.println("</body></html>");
            event.close();
        } else if (event.getEventType() == CometEvent.EventType.READ) {
            InputStream is = request.getInputStream();
            byte[] buf = new byte[512];
            do {
                int n = is.read(buf); //can throw an IOException
                if (n > 0) {
                    log("Read " + n + " bytes: " + new String(buf, 0, n) 
                            + " for session: " + request.getSession(true).getId());
                } else if (n < 0) {
                    error(event, request, response);
                    return;
                }
            } while (is.available() > 0);
        }
    }

    public class MessageSender implements Runnable {

        protected boolean running = true;
        protected ArrayList<String> messages = new ArrayList<String>();
        
        public MessageSender() {
        }
        
        public void stop() {
            running = false;
        }

        /**
         * Add message for sending.
         */
        public void send(String user, String message) {
            synchronized (messages) {
                messages.add("[" + user + "]: " + message);
                messages.notify();
            }
        }

        public void run() {

            while (running) {

                if (messages.size() == 0) {
                    try {
                        synchronized (messages) {
                            messages.wait();
                        }
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                synchronized (connections) {
                    String[] pendingMessages = null;
                    synchronized (messages) {
                        pendingMessages = messages.toArray(new String[0]);
                        messages.clear();
                    }
                    // Send any pending message on all the open connections
                    for (int i = 0; i < connections.size(); i++) {
                        try {
                            PrintWriter writer = connections.get(i).getWriter();
                            for (int j = 0; j < pendingMessages.length; j++) {
                                writer.println(pendingMessages[j] + "<br>");
                            }
                            writer.flush();
                        } catch (IOException e) {
                            log("IOExeption sending message", e);
                        }
                    }
                }

            }

        }

    }

}
  
Comet timeouts

NIO コネクタを使っている場合は,coment のコネクションごとに別々のタイムアウトをセットできる。 タイムアウトをセットするには,単 [に] 次のコードが示すように request アトリビュートをセット [すればよい]:

CometEvent event.... event.setTimeout(30*1000);
または
event.getHttpServletRequest().setAttribute("org.apache.tomcat.comet.timeout", new Integer(30 * 1000));
これはタイムアウトを 30 秒にセットする。 重要な注意 [として],このタイムアウトをセットするには,BEGIN イベントでしなければならない。 デフォルト値は soTimeout である。

APR コネクタを使っている場合は,すべての Comet コネクションが同じタイムアウト値をもつ。 その値は soTimeout*50 である。

非同期書込み

APR または NIO がイネーブルのとき,Tomcat は,大きな静的ファイルを転送するための sendfile の利用をサポートする。 これらの [ファイルのコネクションへの] 書込みは, システムの負荷が増大するやいなや,非同期的に最も効率的な方法で実行される。 ブロックされるかもしれない書込みを使って1個の大きなレスポンスを送るかわりに, 書込み内容を静的ファイルに書き,sendfile コードを使ってそれを [コネクションに] 書き込む,ということが可能である。 caching valve [この語は docs 以下,ここでしか出現しない] は,これを利用して, レスポンス・データをメモリに格納するかわりに1本のファイルにキャッシュできただろう [仮定法過去。将来はそうしたいという開発者のチラシの裏か?]。 sendfile サポートを利用できるのは,request アトリビュート org.apache.tomcat.sendfile.supportBoolean.TRUE にセットされているときである。

どのサーブレットも,[下記に示す] 適切な response アトリビュートをセットすることによって, Tomcat に sendfile 呼出しを実行するように指示できる。 sendfile を使うときは,リクエストもレスポンスもラップされていないことを確かめるのがベストだ。 なぜなら,コネクタ自らが後からレスポンス本体を送るため,フィルタを経由させられないからである。 3個の必要な response アトリビュートをセットする以外, サーブレットはいかなるレスポンス・データも送ってはならないが, レスポンス・ヘッダを改変する結果になるメソッド (クッキーをセットするなど) は使ってもよい。

  • org.apache.tomcat.sendfile.filename: 転送されるファイルのキャノニカルなファイル名 (String 型)
  • org.apache.tomcat.sendfile.start: 開始オフセット (Long 型)
  • org.apache.tomcat.sendfile.start: 終了オフセット (Long 型)

Copyright © 1999-2006, Apache Software Foundation