NIO2非同期ソケットチャネルの手引き

NIO2非同期ソケットチャネルのガイド

1. 概要

この記事では、Java 7 NIO.2チャネルAPIを使用して単純なサーバーとそのクライアントを構築する方法を示します。

サーバーとクライアントの実装にそれぞれ使用される主要なクラスであるAsynchronousServerSocketChannelクラスとAsynchronousSocketChannelクラスを見ていきます。

NIO.2チャネルAPIを初めて使用する場合は、このサイトに入門記事があります。 このlinkに従って読むことができます。

NIO.2チャネルAPIを使用するために必要なすべてのクラスは、java.nio.channelsパッケージにバンドルされています。

import java.nio.channels.*;

2. Futureのサーバー

AsynchronousServerSocketChannelのインスタンスは、そのクラスで静的オープンAPIを呼び出すことによって作成されます。

AsynchronousServerSocketChannel server
  = AsynchronousServerSocketChannel.open();

新しく作成された非同期サーバーソケットチャネルは開いていますが、まだバインドされていないため、ローカルアドレスにバインドし、オプションでポートを選択する必要があります。

server.bind(new InetSocketAddress("127.0.0.1", 4555));

ローカルアドレスを使用して任意のポートにバインドするように、nullを渡すこともできます。

server.bind(null);

バインドされると、accept APIを使用して、チャネルのソケットへの接続の受け入れを開始します。

Future acceptFuture = server.accept();

非同期チャネル操作の場合と同様に、上記の呼び出しはすぐに戻り、実行が継続されます。

次に、get APIを使用して、Futureオブジェクトからの応答をクエリできます。

AsynchronousSocketChannel worker = future.get();

この呼び出しは、必要に応じてブロックし、クライアントからの接続要求を待機します。 オプションで、永久に待機したくない場合は、タイムアウトを指定できます。

AsynchronousSocketChannel worker = acceptFuture.get(10, TimeUnit.SECONDS);

上記の呼び出しが戻り、操作が成功した後、着信メッセージをリッスンしてクライアントにエコーバックするループを作成できます。

runServerというメソッドを作成してみましょう。このメソッド内で待機を行い、着信メッセージを処理します。

public void runServer() {
    clientChannel = acceptResult.get();
    if ((clientChannel != null) && (clientChannel.isOpen())) {
        while (true) {
            ByteBuffer buffer = ByteBuffer.allocate(32);
            Future readResult  = clientChannel.read(buffer);

            // perform other computations

            readResult.get();

            buffer.flip();
            Future writeResult = clientChannel.write(buffer);

            // perform other computations

            writeResult.get();
            buffer.clear();
        }
        clientChannel.close();
        serverChannel.close();
    }
}

ループ内では、操作に応じて読み書きするバッファーを作成するだけです。

次に、読み取りまたは書き込みを行うたびに、他のコードの実行を続行でき、結果を処理する準備ができたら、Futureオブジェクトでget()APIを呼び出します。

サーバーを起動するには、コンストラクターを呼び出してから、main内のrunServerメソッドを呼び出します。

public static void main(String[] args) {
    AsyncEchoServer server = new AsyncEchoServer();
    server.runServer();
}

3. CompletionHandlerのサーバー

このセクションでは、FutureアプローチではなくCompletionHandlerアプローチを使用して同じサーバーを実装する方法を説明します。

コンストラクター内で、AsynchronousServerSocketChannelを作成し、以前と同じ方法でローカルアドレスにバインドします。

serverChannel = AsynchronousServerSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999);
serverChannel.bind(hostAddress);

次に、コンストラクターの内部で、クライアントからの着信接続を受け入れるwhileループを作成します。 このwhileループは、厳密にprevent the server from exiting before establishing a connection with a clientに使用されます。

prevent the loop from running endlesslyに対して、最後にSystem.in.read()を呼び出して、着信接続が標準入力ストリームから読み取られるまで実行をブロックします。

while (true) {
    serverChannel.accept(
      null, new CompletionHandler() {

        @Override
        public void completed(
          AsynchronousSocketChannel result, Object attachment) {
            if (serverChannel.isOpen()){
                serverChannel.accept(null, this);
            }

            clientChannel = result;
            if ((clientChannel != null) && (clientChannel.isOpen())) {
                ReadWriteHandler handler = new ReadWriteHandler();
                ByteBuffer buffer = ByteBuffer.allocate(32);

                Map readInfo = new HashMap<>();
                readInfo.put("action", "read");
                readInfo.put("buffer", buffer);

                clientChannel.read(buffer, readInfo, handler);
             }
         }
         @Override
         public void failed(Throwable exc, Object attachment) {
             // process error
         }
    });
    System.in.read();
}

接続が確立されると、accept操作のCompletionHandlerにあるcompletedコールバックメソッドが呼び出されます。

その戻り値の型はAsynchronousSocketChannelのインスタンスです。 サーバーソケットチャネルがまだ開いている場合は、accept APIを再度呼び出して、同じハンドラーを再利用しながら、別の着信接続の準備をします。

次に、返されたソケットチャネルをグローバルインスタンスに割り当てます。 次に、nullではなく、操作を実行する前に開いていることを確認します。

読み取りおよび書き込み操作を開始できるポイントは、accept操作のハンドラーのcompletedコールバックAPI内です。 この手順は、チャネルをポーリングした以前のアプローチをgetAPIに置き換えます。

明示的に閉じない限り、the server will no longer exit after a connection has been establishedに注意してください。

読み取り操作と書き込み操作を処理するための別個の内部クラスを作成したことにも注意してください。 ReadWriteHandler。 この時点で添付オブジェクトがどのように役立つかを見ていきます。

まず、ReadWriteHandlerクラスを見てみましょう。

class ReadWriteHandler implements
  CompletionHandler> {

    @Override
    public void completed(
      Integer result, Map attachment) {
        Map actionInfo = attachment;
        String action = (String) actionInfo.get("action");

        if ("read".equals(action)) {
            ByteBuffer buffer = (ByteBuffer) actionInfo.get("buffer");
            buffer.flip();
            actionInfo.put("action", "write");

            clientChannel.write(buffer, actionInfo, this);
            buffer.clear();

        } else if ("write".equals(action)) {
            ByteBuffer buffer = ByteBuffer.allocate(32);

            actionInfo.put("action", "read");
            actionInfo.put("buffer", buffer);

            clientChannel.read(buffer, actionInfo, this);
        }
    }

    @Override
    public void failed(Throwable exc, Map attachment) {
        //
    }
}

ReadWriteHandlerクラスの添付ファイルのジェネリック型はマップです。 具体的には、operation(action)のタイプとバッファーという2つの重要なパラメーターを渡す必要があります。

次に、これらのパラメーターの使用方法を確認します。

これはクライアントメッセージにのみ反応するエコーサーバーであるため、最初に実行する操作はreadです。 ReadWriteHandlercompletedコールバックメソッド内で、添付データを取得し、それに応じて何をするかを決定します。

完了したのがread操作の場合、バッファを取得し、添付ファイルのアクションパラメータを変更し、すぐにwrite操作を実行して、メッセージをクライアントにエコーします。

完了したばかりのwrite操作の場合は、read APIを再度呼び出して、サーバーが別の着信メッセージを受信できるように準備します。

4. クライアント

サーバーをセットアップした後、AsyncronousSocketChannelクラスでopenAPIを呼び出すことでクライアントをセットアップできます。 この呼び出しにより、クライアントソケットチャネルの新しいインスタンスが作成され、サーバーへの接続に使用されます。

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999)
Future future = client.connect(hostAddress);

connect操作は、成功しても何も返しません。 ただし、Futureオブジェクトを使用して、非同期操作の状態を監視することはできます。

get APIを呼び出して、接続を待ちましょう。

future.get()

このステップの後、サーバーへのメッセージの送信とそのエコーの受信を開始できます。 sendMessageメソッドは次のようになります。

public String sendMessage(String message) {
    byte[] byteMsg = new String(message).getBytes();
    ByteBuffer buffer = ByteBuffer.wrap(byteMsg);
    Future writeResult = client.write(buffer);

    // do some computation

    writeResult.get();
    buffer.flip();
    Future readResult = client.read(buffer);

    // do some computation

    readResult.get();
    String echo = new String(buffer.array()).trim();
    buffer.clear();
    return echo;
}

5. テスト

サーバーアプリケーションとクライアントアプリケーションが期待どおりに動作していることを確認するには、テストを使用できます。

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

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
}

6. 結論

この記事では、Java NIO.2非同期ソケットチャネルAPIについて説明しました。 これらの新しいAPIを使用してサーバーとクライアントを構築するプロセスを段階的に進めることができました。

この記事の完全なソースコードには、Github projectからアクセスできます。