2013年11月24日日曜日

Undertow Handler Authors Guide 和訳

このエントリは WildFly(旧名 JBoss AS)の Web サブシステムとしても利用される Undertow の以下設計ドキュメントの和訳です。実際に存在するクラス名も出てきますので、ソースコードもお手元にあるとよいかと思います。

Undertow Handler Authors Guide

原本は https://github.com/undertow-io/undertow-io-site の eda33a 時点のものを利用しています。更新が確認され次第、随時反映します。

なお、自分が理解が足りていないところが多いため、変な日本語になっています。。ちょくちょく直していく予定です。

イントロダクション


このガイドでは、Undertow のネイティブハンドラの作成方法の概要を説明します。HttpServerExchange オブジェクトの全ての API に言及している訳ではありません。API の多くは自明であったり、javadoc で理解可能なものです。そのかわり、このガイドでは Undertow のハンドラを記述する際に必要な概念の説明を中心に行います。

シンプルな例から始めましょう。
public class HelloWorldServer {

    public static void main(final String[] args) {
        Undertow server = Undertow.builder()                                                                                //Undertow ビルダ
                .addListener(8080, "localhost")                                                                                //リスナバインディング
                .setHandler(new HttpHandler() {                                                                                //デフォルトハンドラ
                    @Override
                    public void handleRequest(final HttpServerExchange exchange) throws Exception {
                        exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");  //レスポンスヘッダ
                        exchange.getResponseSender().send("Hello World");                                        //レスポンスセンダ
                    }
                }).build();
        server.start();
    }

}
大部分は自明です。

Undertow ビルダ


この API により、Undertow の設定と起動をすぐに行うことができます。この API は組込みや試験環境での用途を想定しています。現時点で、この API は変更される可能性があります。

リスナバインディング


次の行では Undertow が localhost の 8080 ポートにバインドするようにしています。

デフォルトハンドラ


このハンドラは Undertow に登録されたパスにどれもマッチしない URL に対して割り当てられます。このケースでは他にハンドラを登録していないので、常にこのハンドラが実行されます。

レスポンスヘッダ


自明ですが、これは Content-Type ヘッダに値を設定しています。1つ注意すべきなのは、Undertow ではヘッダのマップのキー値として String 型ではなく、ケースインセンティブな文字列である io.undertow.util.HttpString を利用します。io.undertow.utils.Headers クラスは一般的なヘッダーをあらかじめ定数で定義しています。

レスポンスセンダ


Undertow のセンダ API の役割はレスポンスを送ることのみです。センダの詳細は後ほど述べますが、このケースでは完了に対応するコールバックが指定されていないので、センダは、与えられた文字列が完全なレスポンスであると認識します。そして Content-Length ヘッダの設定を行い、レスポンス処理を完了します。

今後のコード例ではハンドラ自体に焦点を当て、サーバのセットアップ部分は割愛します。

リクエストのライフサイクル


(このことはリクエストライフサイクルのドキュメントでも 取り上げています。)

クライアントがサーバへ接続する際、Undertow は io.undertow.server.HttpServerConncection を生成します。クライアントがリクエストを送ると、リクエストは Undertow のパーサによってパースされ、その結果の io.undertow.server.HttpServerExchange がルートハンドラへ渡されます。ルートハンドラの処理が完了すると、以下の4つのいずれかの状態となります。

Exchange がすでに完了している


リクエスト及びレスポンスチャンネルの完全に読込み/書込み終わっている場合、Exchange は完了したとみなされます。(GET や HEAD などで) No Content が返されるようなリクエストに対しては、リクエスト側は自動的に完全に読込み終わったと見なします。ハンドラが完全なレスポンスを書込み、クローズし、レスポンスチャネルをフラッシュすると読込み側は完了したとみなされます。Exchange がすでに完全な状態となっているならば、Exchange は完了したとして何もしません。

ルートハンドラが Exchange の完了なしに正常に返る


このケースでは Exchange は HttpServerExchange.endExchange() が呼ばれることで完了します。endExchange() のセマンティクスについては後述します。

ルートハンドラが例外を伴って返る


このケースではレスポンスコードとして 500 が設定され、Exchange はHttpServerExchange.endExchange() を使って終了します。

ルートハンドラが HttpServerExchange.dispatch() が呼ばれた後、または非同期 IO が開始された後に返る


このケースではディスパッチされるタスクがディスパッチエグゼキュータへと渡されるか、非同期 IO がリクエストまたはレスポンスチャネル上で開始していれば、それから処理が始まります。このケースでは Exchange は完了せず、いつ処理が終わって Exchange が完了するかは、非同期タスク次第です。

もっとも一般的な HttpServerExchange.dispatch() の利用用途は、ブロッキング処理の行えない IO スレッドから、ブロッキング処理の行えるワーカースレッドへ処理を委譲することです。この例は典型的には以下のようになります。

ワーカースレッドへの処理の委譲
public void handleRequest(final HttpServerExchange exchange) throws Exception {
    if (exchange.isInIoThread()) {
      exchange.dispatch(this);
      return;
    }
    // ハンドラのコード
}
Exchange は実際にはコールスタックが返るまでディスパッチされないため、ある Exchange 中において同時に 2 つ委譲のスレッドが動作することはないと考えてよいでしょう。この Exchange はスレッドセーフでないですが、複数のスレッドが同時に Exchange を変更しようとしない限り、複数スレッド間で共有される可能性があります。また、1つ目と2つ目のスレッドアクセスの間に、スレッドプールディスパッチのようなことが起こりえます。

Exchange の完了


上で説明した通り、Exchange はリクエストおよびレスポンスチャネルのクローズとフラッシュすると完了したとみなされます。

Exchange の完了には2種類あり、リクエストチャネルの読込みが完了し、レスポンスチャネルで shutdownWrites() が呼ばれ、レスポンス内容がフラッシュされたときか、HttpServerExchange.endExchange() が呼ばれた時です。endExchange() が呼ばれた時、Undertow はコンテンツが生成されたかどうかチェックします。生成されていればリクエストチャネルを開放し、レスポンスチャネルのクローズとフラッシュを行います。コンテンツが生成されていない場合、Exchange にデフォルトのレスポンスリスナが登録されていれば、Undertow はレスポンスリスナにデフォルトのレスポンスを設定します。こうしてデフォルトのエラーページを返す機構を提供します。

Undertow のバッファプール


Undertow は NIO をベースとしているので、バッファリングが必要な場合は java.nio.ByteBuffer が使われます。これらのバッファは要求ごとに割り当てると性能に重大な影響を与えるため、プールされます。このバッファプールは HttpServerConnection.getBufferPool() を実行することで取得できます。

プールされたバッファは利用後はガベージコレクトの対象にならないため、開放されなくてはなりません。プール中のバッファのサイズはサーバが作成された際に設定されます。経験則として、最大限のパフォーマンスを出すサイズは 16kb です(これは Linux のデフォルトのソケットバッファサイズと一致します)。

ノンブロッキング IO


デフォルトでは Undertow はノンブロッキングの XNIO チャネルを利用し、リクエストは最初 XNIO の IO スレッド上で実行されます。これらのチャネルは受信データを送るために直接利用されます。これらのチャネルはかなり低レベルですが、Undertow はよい扱いやすくするために抽象化したものを提供します。

ノンブロッキング IO を利用したレスポンスを送信するもっとも簡単な方法は、上述したセンダ API を利用することです。センダ API は byte や String 両方に対応した send() メソッドを様々な用途にあった形で提供しています。メソッドの中には、送信が完了したらコールバック処理を実行するものや、コールバックをしない代わりに送信が完了した際に Exchange を終了させるものもあります。

センダ API はキューイングをサポートしないことに注意してください。send() をコールバックが通知されるまで呼ぶことはおそらくできません。

コールバックを取らない send() メソッドは、Content-Length ヘッダが自動的に設定されます。そうでなければ chunked エンコーディングを避けるために、自分で Content-Length ヘッダを設定する必要があります。

センダ API はまた、ブロッキング IO をサポートします。Exchange が HttpServerExchange.startBlocking() の実行によりブロッキングモードになった場合、センダは Exchange のアウトプットストリームを利用してデータを送信します。

ブロッキング IO


Undertow はブロッキング IO もサポートしています。XNIO のワーカスレッド中ではブロキング IO を使えないため、リード/ライトする前にリクエストをワーカスレッドプールへディスパッチする必要があります。

ワーカスレッドをディスパッチするコードは、前述のとおりです。

ブロッキング IO を利用するために HttpServerExchange.startBlocking() を実行します。このメソッドは 2 つあり、1 つは引数を取らず Undertow のデフォルトのストリーム実装を利用します。もう 1 つの HttpServerExchange.startBlocking(BlockingHttpServerExchange blockingExchange) では、利用中のストリームをカスタマイズして使うことができます。例えば サーブレット実装では Undertow のデフォルトストリームの代わりに Servlet(Input/Output)Stream を使うためにこの 2つ目のメソッドを利用しています。

ブロッキングモード Exchange が完了すると HttpServerExchnage.getInputStream() と HttpServerExchange.getOutputStream() を実行することができ、通常通りデータを書き込むことができます。上で述べた sender API も利用できますが、この場合 sender の実装はブロッキング IO が利用されます。

デフォルトで Undertow はバッファリングストリームを利用し、バッファはバッファプールから取り出されます。レスポンスがバッファに適合するほど十分小さければ、Content-Length ヘッダが自動的に設定されます。

ヘッダ


リクエストとレスポンスヘッダは HttpServerExchange.getRequestHeaders() と HttpServerExchange.getResponseHeaders() を介してアクセスできます。これらのメソッドは最適化sれたマップ実装である HeaderMap を返します。

ヘッダは最初のデータがチャネルへ書き込まれたときに HTTP レスポンスヘッダに書きだされます(バッファリングされていた場合、最初にデータが書き込まれた時刻とは一致しない可能性があります)。

ヘッダへの書き込みを矯正したい場合は、レスポンスチャネルまたはストリームで flush() メソッドを利用できます。

HTTP Upgrade


HTTP upgrade を実行するために HttpServerExchange.upgradeChannel(ExchangeComplietionListener upgradeComplitionListern) が利用できます。レスポンスコードは 101 に設定され、Exchange が完了するとリスナに通知されます。ハンドラは upgrade クライアントが期待するような適切なヘッダを設定することが可能です。


0 件のコメント:

コメントを投稿