Рубрики
Без рубрики

Введение в Netty

Узнайте, как настроить небольшой сервер Netty и клиента на Java.

Автор оригинала: baeldung.

Введение в Netty

1. Введение

В этой статье мы собираемся взглянуть на Netty – асинхронную систему сетевых приложений, управляемых событиями.

Основной целью Netty является создание высокую производительность протокольных серверов на основе NIO (или, возможно, NIO.2) с разделением и свободным соединением компонентов сетевой и бизнес-логики. Он может реализовать широко известный протокол, такой как HTTP, или ваш собственный конкретный протокол.

2. Основные концепции

Netty — это не блокирующая структура. Это приводит к высокой пропускной способности по сравнению с блокированием IO. Понимание не блокируя IO имеет решающее значение для понимания основных компонентов Netty и их отношений.

2.1. Канал

Канал является основой Java NIO. Он представляет собой открытое соединение, способное выполнять io-операции, такие как чтение и письмо.

2.2. Будущее

Каждая операция IO на канал в Netty не блокируется.

Это означает, что каждая операция возвращается сразу после вызова. Существует Будущие интерфейс в стандартной библиотеке Java, но это не удобно для целей Netty – мы можем спросить только Будущие о завершении операции или о блокировке текущего потока до завершения операции.

Вот почему Netty имеет свои собственные ChannelFuture интерфейс . Мы можем передать обратный звонок ChannelFuture которые будут призваны к завершению операции.

2.3. События и обработчики

Netty использует парадигму приложения, движимую событиями, поэтому конвейер обработки данных — это цепочка событий, проходят через обработчиков. События и обработчики могут быть связаны с потоком входящих и исходящих данных. Входящие события могут быть следующими:

  • Активация и деактивация канала
  • Читать события операции
  • События исключения
  • События пользователей

Исходящие события проще и, как правило, связаны с открытием/закрытием данных соединения и написания/промывки.

Приложения Netty состоят из нескольких событий логики сетей и приложений и их обработчиков. Базовые интерфейсы обработчиков событий канала КаналХэндлер и его предки ChannelOutboundHandler и ChannelInboundHandler .

Netty обеспечивает огромную иерархию реализаций КаналХэндлер. Стоит отметить адаптеры, которые являются пустыми реализациями, например, ChannelInboundHandlerАдаптер и ChannelOutboundHandlerАдаптер . Мы могли бы расширить эти адаптеры, когда нам нужно обработать только подмножество всех событий.

Кроме того, существует множество реализаций конкретных протоколов, таких, как HTTP, например, HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Было бы хорошо, чтобы познакомиться с ними в Javadoc Нетти.

2.4. Кодеры и декодеры

Работая с сетевым протоколом, мы должны выполнять сериализацию данных и дезириализацию. Для этого Netty вводит специальные расширения ChannelInboundHandler для декодеры которые способны декодировать входящие данные. Базовый класс большинства декодеров ByteToMessageDecoder.

Для кодирования исходящих данных Netty имеет расширения ChannelOutboundHandler называется Кодеры. СообщениеToByteEncoder является основой для большинства кодера реализации . Мы можем преобразовать сообщение из последовательности byte в java-объект и наоборот с помощью кодеров и декодеров.

3. Пример серверного приложения

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

3.1. Зависимости

Прежде всего, мы должны обеспечить зависимость Netty в нашей пом.xml :


    io.netty
    netty-all
    4.1.10.Final

Мы можем найти последнюю версию в течение на Мавен Центральной .

3.2. Модель данных

Класс данных запросов будет иметь следующую структуру:

public class RequestData {
    private int intValue;
    private String stringValue;
    
    // standard getters and setters
}

Допустим, сервер получает запрос и возвращает intValue умножается на 2. Ответ будет иметь одно int значение:

public class ResponseData {
    private int intValue;

    // standard getters and setters
}

3.3. Запрос декодера

Теперь нам нужно создать кодеры и декодеры для наших протокольных сообщений.

Следует отметить, что Netty работает с розеткой получать буферные , которая представлена не как очередь, а просто как куча байтов. Это означает, что наш входящий обработчик может быть вызван, когда полное сообщение не получено сервером.

Мы должны убедиться, что мы получили полное сообщение перед и есть много способов сделать это.

Прежде всего, мы можем создать временную ByteBuf и придаток к нему все входящие байты, пока мы не получим необходимое количество байтов:

public class SimpleProcessingHandler 
  extends ChannelInboundHandlerAdapter {
    private ByteBuf tmp;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        System.out.println("Handler added");
        tmp = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        System.out.println("Handler removed");
        tmp.release();
        tmp = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        tmp.writeBytes(m);
        m.release();
        if (tmp.readableBytes() >= 4) {
            // request processing
            RequestData requestData = new RequestData();
            requestData.setIntValue(tmp.readInt());
            ResponseData responseData = new ResponseData();
            responseData.setIntValue(requestData.getIntValue() * 2);
            ChannelFuture future = ctx.writeAndFlush(responseData);
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

Пример, показанный выше, выглядит немного странно, но помогает нам понять, как работает Netty. Каждый метод нашего обработчика вызывается, когда происходит соответствующее событие. Таким образом, мы инициализируем буфер при добавлении обработчика, заполняем его данными о получении новых байтов и начинаем обрабатывать его, когда получаем достаточно данных.

Мы сознательно не использовали stringValue – расшифровка таким образом была бы неоправданно сложной. Именно поэтому Netty предоставляет полезные классы декодеров, которые являются реализациями ChannelInboundHandler : ByteToMessageDecoder и Воспроизведениедекодера.

Как мы уже отмечали выше, мы можем создать канал обработки трубопровода с Netty. Таким образом, мы можем поставить наш декодер в качестве первого обработчика и обработчик логики обработки может прийти после него.

Декодер для RequestData отображается следующим:

public class RequestDecoder extends ReplayingDecoder {

    private final Charset charset = Charset.forName("UTF-8");

    @Override
    protected void decode(ChannelHandlerContext ctx, 
      ByteBuf in, List out) throws Exception {
 
        RequestData data = new RequestData();
        data.setIntValue(in.readInt());
        int strLen = in.readInt();
        data.setStringValue(
          in.readCharSequence(strLen, charset).toString());
        out.add(data);
    }
}

Идея этого декодера довольно проста. Он использует реализацию ByteBuf который бросает исключение, когда в буфере недостаточно данных для операции чтения.

Когда исключение поймано, буфер перематывается в начало, и декодер ждет новую порцию данных. Декодирование останавливается, когда из список не пуст после декодировать исполнение.

3.4. Ответ Encoder

Кроме того, расшифровка ЗапросДанные нам нужно закодировать сообщение. Эта операция проще, потому что у нас есть полные данные сообщения, когда происходит операция записи.

Мы можем писать данные Канал в нашем главном обработчике или мы можем отделить логику и создать обработчик, расширяющий СообщениеToByteEncoder который будет ловить писать ОтветДанные операция:

public class ResponseDataEncoder 
  extends MessageToByteEncoder {

    @Override
    protected void encode(ChannelHandlerContext ctx, 
      ResponseData msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getIntValue());
    }
}

3.5. Обработка запросов

Так как мы провели расшифровку и кодирование в отдельных обработчиках, мы должны изменить наши ОбработкаХэндлер :

public class ProcessingHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) 
      throws Exception {
 
        RequestData requestData = (RequestData) msg;
        ResponseData responseData = new ResponseData();
        responseData.setIntValue(requestData.getIntValue() * 2);
        ChannelFuture future = ctx.writeAndFlush(responseData);
        future.addListener(ChannelFutureListener.CLOSE);
        System.out.println(requestData);
    }
}

3.6. Сервер Bootstrap

Теперь давайте ставим все это вместе и запустить наш сервер:

public class NettyServer {

    private int port;

    // constructor

    public static void main(String[] args) throws Exception {
 
        int port = args.length > 0
          ? Integer.parseInt(args[0]);
          : 8080;
 
        new NettyServer(port).run();
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
              .channel(NioServerSocketChannel.class)
              .childHandler(new ChannelInitializer() {
                @Override
                public void initChannel(SocketChannel ch) 
                  throws Exception {
                    ch.pipeline().addLast(new RequestDecoder(), 
                      new ResponseDataEncoder(), 
                      new ProcessingHandler());
                }
            }).option(ChannelOption.SO_BACKLOG, 128)
              .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

Подробную информацию о классах, используемых в приведенном выше примере загрузки сервера, можно найти в их Javadoc. Самая интересная часть этой строки:

ch.pipeline().addLast(
  new RequestDecoder(), 
  new ResponseDataEncoder(), 
  new ProcessingHandler());

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

4. Клиентская заявка

Клиент должен выполнять обратное кодирование и расшифровку, поэтому мы должны иметь ЗапросDataEncoder и ОтветДанныйдекодер :

public class RequestDataEncoder 
  extends MessageToByteEncoder {

    private final Charset charset = Charset.forName("UTF-8");

    @Override
    protected void encode(ChannelHandlerContext ctx, 
      RequestData msg, ByteBuf out) throws Exception {
 
        out.writeInt(msg.getIntValue());
        out.writeInt(msg.getStringValue().length());
        out.writeCharSequence(msg.getStringValue(), charset);
    }
}
public class ResponseDataDecoder 
  extends ReplayingDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, 
      ByteBuf in, List out) throws Exception {
 
        ResponseData data = new ResponseData();
        data.setIntValue(in.readInt());
        out.add(data);
    }
}

Кроме того, мы должны определить КлиентХэндлер который отправит запрос и получит ответ с сервера:

public class ClientHandler extends ChannelInboundHandlerAdapter {
 
    @Override
    public void channelActive(ChannelHandlerContext ctx) 
      throws Exception {
 
        RequestData msg = new RequestData();
        msg.setIntValue(123);
        msg.setStringValue(
          "all work and no play makes jack a dull boy");
        ChannelFuture future = ctx.writeAndFlush(msg);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) 
      throws Exception {
        System.out.println((ResponseData)msg);
        ctx.close();
    }
}

Теперь давайте bootstrap клиента:

public class NettyClient {
    public static void main(String[] args) throws Exception {
 
        String host = "localhost";
        int port = 8080;
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer() {
 
                @Override
                public void initChannel(SocketChannel ch) 
                  throws Exception {
                    ch.pipeline().addLast(new RequestDataEncoder(), 
                      new ResponseDataDecoder(), new ClientHandler());
                }
            });

            ChannelFuture f = b.connect(host, port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

Как мы видим, Есть много деталей, общих с сервера загрузки.

Теперь мы можем запустить основной метод клиента и взглянуть на выход консоли. Как и ожидалось, мы ОтветДанные с intValue равно 246.

5. Заключение

В этой статье, у нас было быстрое введение в Netty. Мы показали его основные компоненты, такие как Канал и КаналХэндлер . Кроме того, мы сделали простой не блокирующий протокол сервера и клиента для него.

Как всегда, все образцы кода доступны более на GitHub .