1. 概要

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

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

NIO.2チャネルAPIを初めて使用する場合は、このサイトに紹介記事があります。 このリンクをたどると読むことができます。

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<AsynchronousSocketChannel> 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<Integer> readResult  = clientChannel.read(buffer);
            
            // perform other computations
            
            readResult.get();
            
            buffer.flip();
            Future<Integer> 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. CompleteHandlerを備えたサーバー

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

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

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

次に、コンストラクター内で、クライアントからの着信接続を受け入れるwhileループを作成します。 このwhileループは、クライアントとの接続を確立する前にサーバーが終了するのを防ぐために厳密に使用されます

ループが際限なく実行されるのを防ぐために、最後に System.in.read()を呼び出して、着信接続が標準の入力ストリームから読み取られるまで実行をブロックします。

while (true) {
    serverChannel.accept(
      null, new CompletionHandler<AsynchronousSocketChannel,Object>() {

        @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<String, Object> 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操作のCompleteHandlercompletedコールバックメソッドが呼び出されます。

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

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

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

サーバーは、明示的に閉じない限り、接続が確立された後に終了しないことに注意してください。

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

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

class ReadWriteHandler implements 
  CompletionHandler<Integer, Map<String, Object>> {
    
    @Override
    public void completed(
      Integer result, Map<String, Object> attachment) {
        Map<String, Object> 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<String, Object> attachment) {
        // 
    }
}

ReadWriteHandlerクラスの添付ファイルの一般的なタイプはマップです。 特に、操作(アクション)のタイプとバッファーという2つの重要なパラメーターを渡す必要があります。

次に、これらのパラメーターがどのように使用されるかを確認します。

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

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

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

4. クライアント

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

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 4999)
Future<Void> 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<Integer> writeResult = client.write(buffer);

    // do some computation

    writeResult.get();
    buffer.flip();
    Future<Integer> 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. 結論

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

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