解説(前半)
SelectableChannel、Selector、SelectionKey
NIOが提供するIO多重化では、次の3つのクラスを利用します。
java.nio.channels.Selectorjava.nio.channels.SelectableChanneljava.nio.channle.SelectionKey
次の図はこれら3つのクラスの関係を示したものです。

それでは「HttpServer.java」の中から、実際に、これらのクラスを利用するコードを見ていきましょう。
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引数のオーバーロードされたメソッドも定義されています。そちらについては、後でクライアント用のSocketChannelをSelectorに登録するときに利用するので、そこで説明します。
Selectorが受け付けるオペレーションには、次の4種類があります。
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_CONNECT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
SelectionKey#isAcceptableメソッドが真となるSelectionKey#isConnectableメソッドが真となるSelectionKey#isReadableメソッドが真となるSelectionKey#isWritableメソッドが真となるpublic void service(String authtype) throws IOException { assert serverChannel != null && selector != null; authority = Authority.newAuthority(authtype); for (accept(); select();) { } }
ここでは、実際にSelectorに対してセレクトを実行するselectメソッド(後述)をforループを利用して呼び出します。for文の初期化部で、ServerSocketChannelのacceptメソッドを呼び出すacceptというメソッドを呼び出していますが、これは、セレクト実行前に既にaccept可能な状態になっているかも知れないので、それをチェックするために呼び出しています。
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#selectがClosedSelectorException例外となり、キャッチ節内に制御がわたって呼び出し元に偽を返します。それによってセレクションのループを抜けることができ、プログラムの終了処理が実行されます。
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#isReadableとSelectionKey#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引数として与えます。
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メソッドを呼び出して設定します。
