Java NIOセレクタの紹介

1概要

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

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

このようにして、 単一のスレッドを使用して複数のチャネルを管理することができ、 したがって複数のネットワーク接続を管理できます。

2なぜセレクタを使うのですか?

セレクタを使用すると、複数のチャネルを管理するために複数のスレッドの代わりに1つのスレッドを使用できます。 スレッド間のコンテキスト切り替えは、オペレーティングシステムにとっては高価です 、さらに** 各スレッドはメモリを消費します。

したがって、使用するスレッドが少ないほど、優れています。ただし、** 最新のオペレーティングシステムとCPUはマルチタスク処理で効率が向上し続けているので、マルチスレッドのオーバーヘッドは時間の経過とともに減少し続けていることを覚えておくことが重要です。

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

また、セレクタはデータの読み取りに役立つだけではありません。彼らはまた入ってくるネットワーク接続を待ち受け、遅いチャンネルを越えてデータを書くことができます。

3セットアップ

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

その後、セレクタオブジェクトに複数のチャンネルを登録することができます。

いずれかのチャネルでI/Oアクティビティが発生すると、セレクタから通知されます。これが、シングルスレッドから多数のデータソースから読み取る方法です。

セレクタに登録するチャネルはすべて SelectableChannel のサブクラスでなければなりません。これらは、ノンブロッキングモードにすることができる特別なタイプのチャンネルです。

4セレクタを作成する

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

Selector selector = Selector.open();

5選べるチャンネルを登録する

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

しかし、チャネルがセレクタに登録される前に、それはノンブロッキングモードでなければなりません:

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

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

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

私たちが聞くことができる4つの異なるイベントがあり、それぞれは SelectionKey クラスの定数で表されます。

  • Connect - クライアントがサーバーに接続しようとしたとき。

SelectionKey.OP CONNECT で表されます Accept__ - ** サーバーがクライアントからの接続を受け入れるとき。

SelectionKey.OP ACCEPT によって表されます Read__ - ** サーバーがチャネルから読み取る準備ができたとき。

SelectionKey.OP READ によって表されます Write__ - ** サーバーがチャネルに書き込む準備ができているとき。

SelectionKey.OP WRITE__によって表されます

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

6. SelectionKey オブジェクト

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

それはチャンネルでセレクターを使用することができるためによく理解しなければならないいくつかの重要な特性を含みます。次のサブセクションでこれらのプロパティを調べます。

6.1. インタレストセット

インタレストセットは、セレクターにこのチャンネルで監視させたいイベントのセットを定義します。整数値です。次のようにしてこの情報を得ることができます。

まず、 SelectionKey 's s interestOps メソッドによって返されるインタレストセットがあります。それから先ほど見た 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. レディセット

レディセットは、チャンネルの準備ができているイベントのセットを定義します。

整数値でもあります。次のようにしてこの情報を得ることができます。

SelectionKey sの readyOps メソッドによってレディセットが返されました。

この値と、関心セットの場合のようにイベント定数との論理積をとると、チャネルが特定の値に対応できるかどうかを表すブール値が得られます。

これを行うもう1つの代替的で短い方法は、これと同じ目的で____SelectionKeyの便利なメソッドを使用することです。

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<SelectionKey> selectedKeys = selector.selectedKeys();

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

この後は、通常このセットを繰り返し処理し、各キーについてチャネルを取得して、それに関心のあるセットに表示される操作を実行します。

チャンネルの有効期間中は、そのイベントのキーがさまざまなイベントの準備完了セットに表示されるため、複数回選択することができます。そのため、発生したチャネルイベントをいつでも捕捉して処理するための連続ループが必要です。

8完全な例

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

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

サーバーが 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<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> 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 が選択可能で、ストリーム指向の待機ソケット に適しているためです。

それから私たちはそれを私たちの選んだポートに束縛します。選択可能なチャンネルをセレクターに登録する前に、まずそれを非ブロックモードに設定する必要があることを先に述べました。それで次に我々はこれをしてそしてそれからセレクタにチャンネルを登録する。

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

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

したがって、サーバーが読み書きする新しい ByteBuffer を作成します。 256バイトに初期化します。これは、送受信するデータ量に応じて、任意の値になります。

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

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

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

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

  • 私たちが読んできたバッファに書き込みたいときは、 flip() メソッドを呼ばなければなりません** 。

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

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

8.2. クライアント

これがEcho Client.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 staticメソッド内でインスタンス化するためにシングルトンパターンを使用します。このメソッドからプライベートコンストラクタを呼び出します。

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

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

最後に、送信する 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コンポーネントの基本的な使用法について説明しました。

この記事の完全なソースコードとすべてのコードスニペットは、私のhttps://github.com/eugenp/tutorials/tree/master/core-java-io/src/main/java/com/baeldung/java/nioにあります。/selector[GitHubプロジェクト]。