Руководство по асинхронному сокетному каналу NIO2

Руководство по асинхронному сокетному каналу NIO2

1. обзор

В этой статье мы продемонстрируем, как создать простой сервер и его клиент с помощью API-интерфейсов канала Java 7 NIO.2.

Мы рассмотрим классыAsynchronousServerSocketChannel иAsynchronousSocketChannel, которые являются ключевыми классами, используемыми при реализации сервера и клиента соответственно.

Если вы не знакомы с API-интерфейсами канала NIO.2, у нас есть вступительная статья на этом сайте. Вы можете прочитать это, следуя этомуlink.

Все классы, которые необходимы для использования API каналов NIO.2, объединены в пакет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);

После привязки APIaccept используется для инициирования приема подключений к сокету канала:

Future acceptFuture = server.accept();

Как и в случае асинхронных операций на канале, вышеуказанный вызов сразу возвращается, и выполнение продолжается.

Затем мы можем использовать APIget для запроса ответа от объекта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();
    }
}

Внутри цикла все, что мы делаем, это создаем буфер для чтения и записи в зависимости от операции.

Затем, каждый раз, когда мы выполняем чтение или запись, мы можем продолжить выполнение любого другого кода, и когда мы будем готовы обработать результат, мы вызываем APIget() для объектаFuture.

Чтобы запустить сервер, мы вызываем его конструктор, а затем методrunServer внутриmain:

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

3. Сервер сCompletionHandler

В этом разделе мы увидим, как реализовать тот же сервер, используя подходCompletionHandler, а не подходFuture.

Внутри конструктора мы создаем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();
}

Когда соединение установлено, вызывается метод обратного вызоваcompleted вCompletionHandler операции accept.

Его возвращаемый тип - это экземплярAsynchronousSocketChannel. Если канал сокета сервера все еще открыт, мы снова вызываем APIaccept, чтобы подготовиться к другому входящему соединению при повторном использовании того же обработчика.

Затем мы назначаем возвращенный канал сокета глобальному экземпляру. Затем мы проверяем, что он не равен нулю и что он открыт перед выполнением операций над ним.

Точка, с которой мы можем начать операции чтения и записи, находится внутри API обратного вызоваcompleted обработчика операцииaccept. Этот шаг заменяет предыдущий подход, когда мы опрашивали канал с помощью APIget.

Обратите внимание, что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 - это карта. Нам необходимо передать через него два важных параметра - тип операции (действия) и буфер.

Далее мы увидим, как используются эти параметры.

Первая выполняемая нами операция - этоread, поскольку это эхо-сервер, который реагирует только на сообщения клиента. Внутри метода обратного вызоваReadWriteHandlercompleted мы извлекаем прикрепленные данные и решаем, что делать соответственно.

Если это операцияread, которая завершилась, мы получаем буфер, изменяем параметр действия вложения и сразу же выполняем операциюwrite, чтобы передать сообщение клиенту.

Если это только что завершенная операцияwrite, мы снова вызываем APIread, чтобы подготовить сервер к приему другого входящего сообщения.

4. Клиент

После настройки сервера мы можем теперь настроить клиента, вызвав APIopen в классеAsyncronousSocketChannel. Этот вызов создает новый экземпляр канала сокета клиента, который мы затем используем для подключения к серверу:

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

Операцияconnect ничего не возвращает в случае успеха. Однако мы по-прежнему можем использовать объектFuture для отслеживания состояния асинхронной операции.

Давайте вызовем APIget для ожидания соединения:

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. Заключение

В этой статье мы исследовали API-интерфейсы асинхронных каналов сокетов Java NIO.2. Мы смогли пройти через процесс построения сервера и клиента с помощью этих новых API.

Вы можете получить доступ к полному исходному коду этой статьи в папкеGithub project.