Руководство по асинхронному сокетному каналу 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.