Java NIOセレクタの紹介

Java NIOセレクターの概要

1. 概要

この記事では、Java NIOのSelectorコンポーネントの紹介部分について説明します。

セレクタは、1つ以上のNIOチャネルを監視し、1つ以上がデータ転送に使用可能になったことを認識するメカニズムを提供します。

このように、a single thread can be used for managing multiple channels、したがって複数のネットワーク接続。

2. なぜセレクターを使用するのですか?

セレクタを使用すると、複数ではなく1つのスレッドを使用して、複数のチャネルを管理できます。 Context-switching between threads is expensive for the operating system、さらにeach thread takes up memory.

したがって、使用するスレッドが少ないほど良いです。 ただし、modern operating systems and CPU’s keep getting better at multitaskingを覚えておくことが重要です。そのため、マルチスレッドのオーバーヘッドは時間の経過とともに減少し続けます。

ここでは、セレクターを使用して1つのスレッドで複数のチャネルを処理する方法について説明します。

セレクターはデータの読み取りに役立つだけではないことにも注意してください。また、着信ネットワーク接続をリッスンし、低速チャネルを介してデータを書き込むこともできます。

3. セットアップ

セレクタを使用するには、特別な設定は必要ありません。 必要なクラスはすべてコアjava.nioパッケージであり、必要なものをインポートするだけです。

その後、複数のチャンネルをセレクターオブジェクトに登録できます。 I / Oアクティビティがいずれかのチャネルで発生すると、セレクターから通知されます。 これは、1つのスレッドから多数のデータソースを読み取る方法です。

セレクターに登録するチャネルは、SelectableChannelのサブクラスである必要があります。 これらは、非ブロッキングモードにできる特別なタイプのチャネルです。

4. セレクターの作成

セレクターは、Selectorクラスの静的openメソッドを呼び出すことで作成できます。このメソッドは、システムのデフォルトのセレクタープロバイダーを使用して新しいセレクターを作成します。

Selector selector = Selector.open();

5. 選択可能なチャネルの登録

セレクタがチャネルを監視するには、これらのチャネルをセレクタに登録する必要があります。 これを行うには、選択可能なチャネルのregisterメソッドを呼び出します。

ただし、セレクタにチャネルを登録する前に、非ブロッキングモードである必要があります。

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

これは、ソケットチャネルの場合のように非ブロッキングモードに切り替えることができないため、セレクターでFileChannelsを使用できないことを意味します。

最初のパラメーターは前に作成したSelectorオブジェクトで、2番目のパラメーターはインタレストセット,を定義します。これは、セレクターを介して、監視対象チャネルでリッスンすることに関心のあるイベントを意味します。

リッスンできるイベントは4つあり、それぞれがSelectionKeyクラスの定数で表されます。

  • Connectクライアントがサーバーに接続しようとしたとき。 SelectionKey.OP_CONNECTで表されます

  • Acceptサーバーがクライアントからの接続を受け入れる場合。 SelectionKey.OP_ACCEPTで表されます

  • Readサーバーがチャネルから読み取る準備ができている場合。 SelectionKey.OP_READで表されます

  • サーバーがチャネルに書き込む準備ができたときのWriteSelectionKey.OP_WRITEで表されます

返されるオブジェクトSelectionKeyは、セレクターへの選択可能なチャネルの登録を表します。 次のセクションで詳しく説明します。

6. SelectionKeyオブジェクト

前のセクションで見たように、セレクターにチャネルを登録すると、SelectionKeyオブジェクトが取得されます。 このオブジェクトは、チャネルの登録を表すデータを保持します。

これには、チャンネルでセレクターを使用できるようにするためによく理解する必要があるいくつかの重要なプロパティが含まれています。 これらのプロパティについては、次のサブセクションで説明します。

6.1. インタレストセット

インタレストセットは、セレクタがこのチャネルで監視するイベントのセットを定義します。 整数値です。この情報は次の方法で取得できます。

まず、SelectionKeyinterestOpsメソッドによって返されるインタレストセットがあります。 次に、前に見たSelectionKeyのイベント定数があります。

これらの2つの値をANDすると、イベントが監視されているかどうかを示すブール値が取得されます。

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

6.2. レディセット

準備完了セットは、チャネルの準備ができているイベントのセットを定義します。 整数値でもあります。この情報は次の方法で取得できます。

SelectionKeyreadyOpsメソッドによって返されるレディセットがあります。 対象セットの場合と同様に、この値とイベント定数をANDすると、チャネルが特定の値に対して準備ができているかどうかを表すブール値を取得します。

これを行う別の代替のより短い方法は、これと同じ目的でSelectionKey's便利なメソッドを使用することです。

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWriteable();

6.3. チャンネル

SelectionKeyオブジェクトから監視されているチャネルへのアクセスは非常に簡単です。 channelメソッドを呼び出すだけです。

Channel channel = key.channel();

6.4. セレクター

チャネルを取得するのと同じように、SelectionKeyオブジェクトからSelectorオブジェクトを取得するのは非常に簡単です。

Selector selector = key.selector();

6.5. オブジェクトの添付

オブジェクトをSelectionKey.にアタッチできます。チャネルにカスタムIDを指定したり、追跡したい任意の種類のJavaオブジェクトをアタッチしたりする場合があります。

オブジェクトを添付することは、それを行う便利な方法です。 SelectionKeyからオブジェクトをアタッチおよび取得する方法は次のとおりです。

key.attach(Object);

Object object = key.attachment();

または、チャンネル登録時にオブジェクトを添付することもできます。 次のように、チャネルのregisterメソッドに3番目のパラメータとして追加します。

SelectionKey key = channel.register(
  selector, SelectionKey.OP_ACCEPT, object);

7. チャンネルキーの選択

これまで、セレクターを作成し、それにチャネルを登録し、セレクターへのチャネルの登録を表すSelectionKeyオブジェクトのプロパティを検査する方法について見てきました。

これはプロセスの半分にすぎません。今、先ほど見たレディセットを選択する連続プロセスを実行する必要があります。 次のように、セレクターのselectメソッドを使用して選択を行います。

int channels = selector.select();

このメソッドは、少なくとも1つのチャネルが操作の準備ができるまでブロックします。 返される整数は、チャネルが操作の準備ができているキーの数を表します。

次に、通常、選択したキーのセットを取得して処理します。

Set selectedKeys = selector.selectedKeys();

取得したセットはSelectionKeyオブジェクトであり、各キーは操作の準備ができている登録済みチャネルを表します。

この後、通常、このセットを反復処理し、各キーについて、チャネルを取得し、それに設定されている対象セットに表示される操作を実行します。

チャネルの存続期間中、キーはさまざまなイベントの準備完了セットに表示されるため、数回選択される場合があります。 これが、発生したときにチャネルイベントをキャプチャして処理するための連続ループが必要な理由です。

8. 完全な例

前のセクションで得た知識を強化するために、完全なクライアントサーバーの例を作成します。

コードのテストを簡単にするために、エコーサーバーとエコークライアントを構築します。 この種のセットアップでは、クライアントはサーバーに接続し、サーバーへのメッセージの送信を開始します。 サーバーは、各クライアントから送信されたメッセージをエコーバックします。

サーバーは、endなどの特定のメッセージを検出すると、それを通信の終了として解釈し、クライアントとの接続を閉じます。

8.1. サーバー

EchoServer.javaのコードは次のとおりです。

public class EchoServer {

    private static final String POISON_PILL = "POISON_PILL";

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 5454));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(256);

        while (true) {
            selector.select();
            Set selectedKeys = selector.selectedKeys();
            Iterator iter = selectedKeys.iterator();
            while (iter.hasNext()) {

                SelectionKey key = iter.next();

                if (key.isAcceptable()) {
                    register(selector, serverSocket);
                }

                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }
        }
    }

    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key)
      throws IOException {

        SocketChannel client = (SocketChannel) key.channel();
        client.read(buffer);
        if (new String(buffer.array()).trim().equals(POISON_PILL)) {
            client.close();
            System.out.println("Not accepting client messages anymore");
        }

        buffer.flip();
        client.write(buffer);
        buffer.clear();
    }

    private static void register(Selector selector, ServerSocketChannel serverSocket)
      throws IOException {

        SocketChannel client = serverSocket.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }

    public static Process start() throws IOException, InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
        String classpath = System.getProperty("java.class.path");
        String className = EchoServer.class.getCanonicalName();

        ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath, className);

        return builder.start();
    }
}

これが起こっていることです。静的なopenメソッドを呼び出して、Selectorオブジェクトを作成します。 次に、静的なopenメソッド、具体的にはServerSocketChannelインスタンスを呼び出すことによってもチャネルを作成します。

これは、ServerSocketChannel is selectable and good for a stream-oriented listening socketが原因です。

次に、選択したポートにバインドします。 選択可能なチャネルをセレクターに登録する前に、最初に非ブロッキングモードに設定する必要があることを前に言ったことを思い出してください。 次に、これを実行してから、チャネルをセレクタに登録します。

この段階では、このチャネルのSelectionKeyインスタンスは必要ないため、覚えていません。

Java NIOは、ストリーム指向モデル以外のバッファー指向モデルを使用します。 そのため、通常、ソケット通信はバッファーへの書き込みとバッファーからの読み取りによって行われます。

したがって、サーバーが書き込みおよび読み取りを行う新しいByteBufferを作成します。 256バイトに初期化します。これは、転送する予定のデータ量に応じて、任意の値になります。

最後に、選択プロセスを実行します。 準備ができたチャネルを選択し、選択キーを取得し、キーを反復処理して、各チャネルが準備されている操作を実行します。

通常、サーバーはアクティビティがあるかどうかに関係なく実行し続ける必要があるため、無限ループでこれを行います。

ServerSocketChannelが処理できる唯一の操作は、ACCEPT操作です。 クライアントからの接続を受け入れると、読み取りと書き込みを実行できるSocketChannelオブジェクトを取得します。 非ブロッキングモードに設定し、セレクターに対するREAD操作用に登録します。

後続の選択のいずれかで、この新しいチャネルは読み取り可能になります。 それを取得し、その内容をバッファに読み込みます。 エコーサーバーとしてのとおり、このコンテンツをクライアントに書き戻す必要があります。

When we desire to write to a buffer from which we have been reading, we must call the flip() method

最後に、flipメソッドを呼び出してバッファを書き込みモードに設定し、単純に書き込みます。

start()メソッドは、ユニットテスト中にエコーサーバーを個別のプロセスとして開始できるように定義されています。

8.2. クライアント

EchoClient.javaのコードは次のとおりです。

public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;

    public static EchoClient start() {
        if (instance == null)
            instance = new EchoClient();

        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 5454));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;

    }
}

クライアントはサーバーよりも単純です。

シングルトンパターンを使用して、start静的メソッド内でインスタンス化します。 このメソッドからプライベートコンストラクターを呼び出します。

プライベートコンストラクターでは、サーバーチャネルがバインドされているのと同じポートで、まだ同じホストで接続を開きます。

次に、書き込みおよび読み取り可能なバッファを作成します。

最後に、sendMessageメソッドがあります。このメソッドは、渡した文字列をバイトバッファーに読み取りラップし、チャネルを介してサーバーに送信します。

次に、クライアントチャネルから読み取り、サーバーから送信されたメッセージを取得します。 これをメッセージのエコーとして返します。

8.3. テスト

EchoTest.javaというクラス内で、サーバーを起動し、サーバーにメッセージを送信し、同じメッセージがサーバーから受信された場合にのみ合格するテストケースを作成します。 最後のステップとして、テストケースは完了前にサーバーを停止します。

これでテストを実行できます。

public class EchoTest {

    Process server;
    EchoClient client;

    @Before
    public void setup() throws IOException, InterruptedException {
        server = EchoServer.start();
        client = EchoClient.start();
    }

    @Test
    public void givenServerClient_whenServerEchosMessage_thenCorrect() {
        String resp1 = client.sendMessage("hello");
        String resp2 = client.sendMessage("world");
        assertEquals("hello", resp1);
        assertEquals("world", resp2);
    }

    @After
    public void teardown() throws IOException {
        server.destroy();
        EchoClient.stop();
    }
}

9. 結論

この記事では、Java NIO Selectorコンポーネントの基本的な使用法について説明しました。

この記事の完全なソースコードとすべてのコードスニペットは、私のGitHub projectで入手できます。