Введение в Нетти

Введение в Нетти

1. Вступление

В этой статье мы рассмотрим Netty - платформу для асинхронных событийных сетевых приложений.

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

2. Основные понятия

Netty - это неблокирующая структура. Это приводит к высокой пропускной способности по сравнению с блокировкой ввода-вывода. Understanding non-blocking IO is crucial to understanding Netty’s core components and their relationships.с

2.1. канал

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

2.2. Будущее

Каждая операция ввода-вывода наChannel в Netty не является блокирующей.

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

Вот почемуNetty has its own ChannelFuture interface. Мы можем передать обратный вызовChannelFuture, который будет вызываться после завершения операции.

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

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

  • Активация и деактивация канала

  • Чтение событий операции

  • Исключительные события

  • Пользовательские события

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

Netty-приложения состоят из пары сетевых и прикладных логических событий и их обработчиков. Базовыми интерфейсами для обработчиков событий канала являютсяChannelHandler и его предкиChannelOutboundHandler иChannelInboundHandler.

Netty предоставляет огромную иерархию реализацийChannelHandler.. Стоит отметить адаптеры, которые являются просто пустыми реализациями, например ChannelInboundHandlerAdapter иChannelOutboundHandlerAdapter. Мы могли бы расширить эти адаптеры, когда нам нужно обработать только подмножество всех событий.

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

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

Поскольку мы работаем с сетевым протоколом, нам необходимо выполнить сериализацию и десериализацию данных. С этой целью Netty вводит специальные расширенияChannelInboundHandler дляdecoders, которые способны декодировать входящие данные. Базовый класс большинства декодеров -ByteToMessageDecoder.

Для кодирования исходящих данных Netty имеет расширенияChannelOutboundHandler, называемыеencoders.MessageToByteEncoder - это основа для большинства реализаций кодировщика. Мы можем преобразовать сообщение из последовательности байтов в объект Java и наоборот, с кодировщиками и декодерами.

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

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

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

Прежде всего, нам нужно предоставить зависимость Netty в нашемpom.xml:


    io.netty
    netty-all
    4.1.10.Final

Мы можем найти последнюю версию вon Maven Central.

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 works with socket receive buffer представлен не как очередь, а просто как набор байтов. Это означает, что наш входящий обработчик может быть вызван, когда полное сообщение не получено сервером.

We must make sure that we have received the full message before processing, и есть много способов сделать это.

Прежде всего, мы можем создать временный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 иReplayingDecoder..

Как мы уже отмечали выше, мы можем создать конвейер обработки каналов с помощью 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, которая вызывает исключение, когда в буфере недостаточно данных для операции чтения.

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

3.4. Кодер ответа

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

Мы можем записать данные вChannel в нашем основном обработчике или мы можем разделить логику и создать обработчик, расширяющийMessageToByteEncoder, который будет перехватывать операцию записиResponseData:

public class ResponseDataEncoder
  extends MessageToByteEncoder {

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

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

Поскольку мы выполняли декодирование и кодирование в отдельных обработчиках, нам нужно изменить нашProcessingHandler:

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. Начальная загрузка сервера

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

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. Клиентское приложение

Клиент должен выполнять обратное кодирование и декодирование, поэтому нам нужныRequestDataEncoder иResponseDataDecoder:

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);
    }
}


Также нам нужно определитьClientHandler, который будет отправлять запрос и получать ответ от сервера:

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();
    }
}

Теперь давайте загрузим клиента:

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();
        }
    }
}

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

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

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

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

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