SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Javaによる簡易HTTPサーバーの作成

NIOの非ブロック接続を利用した多重IOの実装

Javaによる簡易HTTPサーバーの作成 第4回


解説(前半)

SelectableChannel、Selector、SelectionKey

 NIOが提供するIO多重化では、次の3つのクラスを利用します。

  • java.nio.channels.Selector
  • SelectableChannelのマルチプレクサ(多数の入力信号を1つの信号として出力する装置)
  • java.nio.channels.SelectableChannel
  • Selectorによって多重化可能なチャネル(従来のストリームに対してNIOで利用するオブジェクト)
  • java.nio.channle.SelectionKey
  • Selectorに登録されたSelectableChannelとその状態を保持するオブジェクト

 次の図はこれら3つのクラスの関係を示したものです。

 それでは「HttpServer.java」の中から、実際に、これらのクラスを利用するコードを見ていきましょう。

HttpServerのコンストラクタ
public HttpServer(int port) throws IOException {
    selector = Selector.open();
    serverChannel = ServerSocketChannel.open();
    ServerSocket ss = serverChannel.socket();
    ss.setReuseAddress(true);
    ss.bind(new InetSocketAddress(port));
    if (serverChannel.isBlocking()) {
        serverChannel =
            (ServerSocketChannel)serverChannel
            .configureBlocking(false);
    }
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}

 コンストラクタで、SelectorのインスタンスをSelector.openによって生成します。次に、ServerSocketChannelのインスタンスを同様にクラス自身のstaticメソッド(open)で生成します。NIOで利用するクラスのインスタンス化は、ほとんどすべて、このようにクラスのstaticメソッドを利用します。

 ここで作成したServerSocketChannelクラスは、SelectableChannelの派生クラスです。このクラスは、ServerSocket#bindなどのメソッドを実装していないため(逆に言うとServerSocketのオペレーションはacceptしか実装していません)、socketメソッドを利用してServerSocketのインスタンスを取り出して、それに対してクライアントからのリクエストを受け付けるための準備をします。

 次に、configureBlockingメソッドを偽を引数として呼び出して、非ブロックモードに状態を変更します。なおAPI仕様では、SelectableChannelクラスは作成した直後はブロックモードと規定されています。従って、ここでのisBlockingメソッドの呼び出しは、定義から必ず真となるため冗長です。防御的コードと考えてください。

 最後にregisterメソッドを呼び出してSelectorに登録します。registerメソッドの最初の引数は登録対象のSelectorのインスタンスです。第2引数にはSelectorによって監視対象となるオペレーションのコードを指定します。registerメソッドの呼び出しによりSelectionKeyのインスタンスが生成され、SelectorにはSelectionKeyのインスタンスが登録されます。なお、registerメソッドの戻り値はここで生成されたSelectionKeyのインスタンスですが、ここでは利用していません。なお、registerメソッドには、3引数のオーバーロードされたメソッドも定義されています。そちらについては、後でクライアント用のSocketChannelSelectorに登録するときに利用するので、そこで説明します。

 Selectorが受け付けるオペレーションには、次の4種類があります。

  • SelectionKey.OP_ACCEPT
  • サーバーソケットのクライアントからの接続要求を受け付けオペレーション
    受け付けるとSelectionKey#isAcceptableメソッドが真となる
  • SelectionKey.OP_CONNECT
  • クライアントソケットのサーバーへの接続要求オペレーション
    要求が受け付けられるとSelectionKey#isConnectableメソッドが真となる
  • SelectionKey.OP_READ
  • 読み込みオペレーション(読み込み可能なデータまたはEOFの検出)
    読み込み可能になるとSelectionKey#isReadableメソッドが真となる
  • SelectionKey.OP_WRITE
  • 書き込みオペレーション(書き込み可能状態の検出)
    書き込み可能になるとSelectionKey#isWritableメソッドが真となる
HttpServerのセレクト処理の呼び出しループ
public void service(String authtype) throws IOException {
    assert serverChannel != null && selector != null;

    authority = Authority.newAuthority(authtype);

    for (accept(); select();) {
    }
}

 ここでは、実際にSelectorに対してセレクトを実行するselectメソッド(後述)をforループを利用して呼び出します。for文の初期化部で、ServerSocketChannelacceptメソッドを呼び出すacceptというメソッドを呼び出していますが、これは、セレクト実行前に既にaccept可能な状態になっているかも知れないので、それをチェックするために呼び出しています。

HttpServerのセレクト処理
boolean select() throws IOException {
    try {
        selector.select();
        for (Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); keys.hasNext();) {
            SelectionKey key = keys.next();
            keys.remove();
            selected(key);
        }
    } catch (ClosedSelectorException e) {
        return false;
    }
    return true;
}

 HttpServer#selectは実際にセレクトを行うメソッドです。ここでは、Selector#selectを呼び出してセレクトを実行します。なお、Selector#selectメソッドはセレクト状態のキーを検出するまで呼び出し側に戻りません。

 Selectorに対して、登録されているキーのセレクト状態を検出させるメソッドには、ここで利用しているSelector#selectメソッド(セレクト状態検出まで無限の待ち状態となる)、タイムアウトを指定できるselect(long)メソッド、セレクト状態のチャネルの有無を検出するために利用するselectNowメソッド(セレクト状態のチャネルが無ければ戻り値0ですぐに復帰)の3種類があります。いずれのメソッドとも、戻り値は現在セレクト状態にあるチャネルを保持するSelectionKeyの数です。

 次に、セレクト状態のキーのセットをSelector#selectedKeysメソッドを呼び出して取得します。ここではセレクトされたキーを順番に処理していますが、ここからスレッドプールを呼び出すという方法も取れるでしょう。なお、一度セレクト状態のキーのセットに含まれたSelectionKeyのインスタンスはプログラムで明示的に削除(リストではIterator#removeを呼び出しています)しない限りセレクト状態のセット内に保持されたままとなります(ただしキャンセルしたキーは削除されるため、リソースリークとはなりません)。一度削除したキーもセレクト状態になれば、再度セレクト状態のセットに追加されるので、実際に処理を行う時にはセットから削除したほうが良いでしょう。

 このメソッド内でキャッチしているClosedSelectorExceptionは、Selector#closeによってクローズされたSelectorのインスタンスに対して何らかのメソッドを呼び出した場合にスローされる例外です。このプログラムでは、/quitをクライアントから要求された場合に、そのリクエストハンドラがSelector#closeを呼び出します。その結果、リクエストハンドラから戻ってきた後に呼び出すSelector#selectClosedSelectorException例外となり、キャッチ節内に制御がわたって呼び出し元に偽を返します。それによってセレクションのループを抜けることができ、プログラムの終了処理が実行されます。

HttpServerのセレクトされたキー処理
void selected(SelectionKey key) {
    assert key.isValid() : "key is not valid";
    try {
        if (key.isAcceptable()) {
            accept();
        } else {
            if (key.isReadable()) {
                ((Request)key.attachment()).readyToRead(key);
            }
            // 読み込み処理の過程でkeyをキャンセルする可能性があるので
            // isValidのチェックが必要。
            if (key.isValid() && key.isWritable()) {
                ((Request)key.attachment()).readyToWrite(key);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
        ((Request)key.attachment()).abort(key);
    }
}

 HttpServer#selectedメソッドでは、セレクト状態のSelectionKeyの状態を判定して、もしaccept可能であれば(OP_ACCEPTを指定されているServerSocketChannelのみ)acceptメソッドを、読み込み可能であればリクエストハンドラのreadToReadメソッドを、書き込み可能であればリクエストハンドラのreadyToWriteメソッドをそれぞれ呼び出します。

 SelectionKey#isReadableSelectionKey#isWritableは同時に真となる可能性があるので、ここではifを並列しています(elseで結合してはならない)。また、読み込み処理で異常を検出したり、処理が完結した場合には、チャネルのクローズとSelectionKeyのキャンセルを行います。その場合、該当するSelectionKeyのインスタンスは無効状態となりSelectionKey#isWriableを呼び出すと、CancelledKeyException例外がスローされます。そのため、まずSelectionKey#isValidを呼び出して有効かどうかをチェックしてからSelection.Key#isWritableを呼び出しています。

 ここで重要な点は、リクエストハンドラ(これは、クライアント単位に作成されるオブジェクトです)のインスタンスをSelectionKey#attachmentメソッドから取り出している点です。このように、SelectionKeyは現在のチャネルのレディ状態のほかに、そのチャネルに関連したユーザー供与のオブジェクト(アタッチメント)も保持します。IOを多重化させる場合には、この例のようにSelectionKeyに個々のIO固有の情報を結合してそれを利用します。なお、SelectionKeyにアタッチメントを付けるには、SelectionKey#attachメソッドを呼び出すか、またはSelectableChannel#registerメソッドによる登録時点で第3引数として与えます。

HttpServerのクライアントリクエスト受付処理
void accept() throws IOException {
    SocketChannel sock = serverChannel.accept();
    if (sock == null) {
        return;
    }
    if (sock.isBlocking()) {
        sock = (SocketChannel)sock.configureBlocking(false);
    }
    Request req = new Request(sock, this);
    SelectionKey key =
        sock.register(selector,
                      SelectionKey.OP_READ, req);
    req.readyToRead(key);
}

 HttpServer#acceptメソッドで、クライアントからの接続要求を受け付け、取得したSocketChannelのインスタンスをSelectorに作成したリクエストハンドラオブジェクト(Requestクラスのインスタンス)と共に登録します。

 このメソッドで重要な点は、reigsterメソッド呼び出し直後に、まだセレクトされていないにもかかわらず、Request#readyToReadメソッドを呼び出している点です。このメソッドは、HttpServer#selectedで示したように、SelectionKey#isReadableが真の場合に呼び出されるクライアントからのリクエスト読み込みメソッドです。

 これは、既にセレクト可能な状態になっているチャネルをそのままSelectorに与えた場合には、その後セレクト状態が変化しないため、セレクト状態が検知されなくなる、という状態を防ぐためです。このあたりの動作はOSの非同期IOの実装とJVMの実装の両方に関係してくるため、実際に複数のJVM(というよりもおそらくOSの実装の差と考えられます)で試すと、この時点でreadyToReadメソッドを呼ばなくてもすぐに最初のSelector#select呼び出しから復帰できる環境と、そうでない環境があることが分かると思います。非ブロックIOはデータがレディでなければすぐにIO呼び出しから復帰できるので、どのような環境であってもここで一度読み込み可能かどうか試すべきでしょう。

 なお、ここでのregisterメソッド呼び出しはSelectionKey.OP_READのみを登録しています。実際にはここで登録しているチャネルはクライアントへ対してのレスポンスを書き出すため、SelectionKey.OP_WRITEの設定が最終的には必要です。しかし、この時点ではまだ何も書き込みを行っていないため、チャネルは書き込み可能(書き込みに対してセレクト)状態となっています。そのため、もしこの時点でSelectionKey.OP_WRITEを設定すると何も行うことがなくてもSelector#selectがセレクト状態として戻ってきてしまいます。それを避けるため、SelectionKey.OP_WRITEの設定は、リクエストハンドラーが最初に書き込みを実行する時に、直接SelectionKeyのインスタンスのSelectionKey#interestOpsメソッドを呼び出して設定します。

次のページ
解説(後半)

この記事は参考になりましたか?

Javaによる簡易HTTPサーバーの作成連載記事一覧

もっと読む

この記事の著者

arton(アートン)

専門は業界特化型のミドルウェアやフレームワークとそれを利用するアプリケーションの開発。需要に応じてメインフレームクラスから携帯端末までダウンサイジングしたりアップサイジングしたりしながらオブジェクトを連携させていくという変化に富んだ開発者人生を歩んでいる。著書に『Ruby③ オブジェクト指向とはじめての設計...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/358 2006/06/14 18:57

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー