Fundamentals of Netty

Keywords: Java Netty Framework

1, BIO (Blocking IO)

1. Characteristics

In the synchronous blocking model, a thread can only process one request

2. Shortcomings

  • The read operation in IO code is a blocking operation. If the connection does not read and write data, it will cause thread blocking and waste resources

  • If there are many threads, there will be too many server threads and too much pressure

3. Application scenario

BIO is suitable for architectures with small and fixed requests

2, NIO (Non Blocking IO)

1. Characteristics

Synchronous non blocking. The server implementation mode is that one thread can process multiple requests. The connection requests sent by the client will be registered on the multiplexer epoll, and the multiplexer will execute the processing when it polls the requests; I/O multiplexing is generally implemented with Linux API s (select, poll, epoll). difference:

2. Three components

  • Channel
  • Buffer (buffer)
  • Selector

3,demo

It is generally used in architectures with many and short connections, such as chat system, barrage system and communication between servers

getSelector()

  • Create a default pipe connection, initialize the port, and register a connection listening event
  • The loop reads the events above the selector
  • Call processing
public class NIOServer {
    private static final Selector SELECTOR = getSelector();
    public static void main(String[] args) throws IOException {
        assert SELECTOR != null;
        while (true){
            //Listen for events registered on the selector
            SELECTOR.select();
            Iterator<SelectionKey> iterator = SELECTOR.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                //Process the current key to prevent the next selector from processing
                handler(selectionKey);
                iterator.remove();
            }
        }
    }
    public static Selector getSelector() {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(9001));
            serverSocketChannel.configureBlocking(false);
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            return selector;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

handler()

  • If it is a connection event, configure the pipeline and register a read event
  • If it is a read event, read the data
private static void handler(SelectionKey selectionKey) throws IOException {
        if(selectionKey.isAcceptable()){
            System.out.println("Connection event detected...");
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(SELECTOR,SelectionKey.OP_READ);
            socketChannel.write(ByteBuffer.wrap("hello client".getBytes()));
        }else if (selectionKey.isReadable()){
            System.out.println("Read event detected...");
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            ByteBuffer allocate = ByteBuffer.allocate(1024);
            int read = socketChannel.read(allocate);
            if(read != -1){
                System.out.println("Read client data:"+new String(allocate.array(),0,read));
            }
            socketChannel.write(ByteBuffer.wrap("The server receives the message".getBytes()));
        }else if(selectionKey.isWritable()){
            System.out.println("Write event detected");
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            ByteBuffer allocate = ByteBuffer.allocate(1024);
            int read = socketChannel.read(allocate);
            if(read != -1){
                System.out.println("Read client write data:"+new String(allocate.array(),0,read));
            }
            socketChannel.write(ByteBuffer.wrap("The server receives the information and returns the data after waiting for the client to process it..".getBytes()));
        }
    }

3, AIO

Asynchronous non blocking, encapsulating NIO

4, Netty

  • mainReactor: only the connection of the client is processed. After the connection is processed, the pipeline is registered with the subReactor
  • subReactor: after processing the connection of the client, the pipeline will be registered to the current reactor, and then events such as read and write will be distributed to the thread pool for processing

1. Netty model

  • Boss Group: equivalent to the above mainReactor, which only processes client requests and maintains a selector in each thread
    • The first step is to listen to the client's request and generate NioSokectChannel
    • Step 2: register NioSokectChannel to a thread in a Work Group
    • Step 3: run all tasks during task execution
  • Work Group: it is equivalent to the subReactor above. A pipeline is established to handle events such as reading and writing, in which a selector is maintained in each thread
    • Poll and handle read and writer events in NioSocketChannel
    • runAllTasks processes the tasks of the task queue TaskQueue

2,ByteBuf

3. Solution of TCP sticky packet / half packet problem

3.1 causes of problems

  • The byte size written by the application write is larger than the socket send buffer size
  • TCP segmentation for MSS size
  • The payload of Ethernet frame is larger than MTU for IP fragmentation

3.2 mainstream solutions

  • The message length is fixed. For example, the size of each message is fixed at 200 bytes. If it is not enough, fill in the space
  • Add a carriage return line feed character at the end of the package for segmentation, such as FTP protocol
  • The message is divided into a message header and a message body. The message header contains a field representing the total length of the message (or the length of the message body). The general design idea is that the first field of the message header uses int32 to represent the total length of the message
  • More complex application layer protocols

3.3 netty decoder

LineBasedFrameDecoder: successively traverse the readable bytes in ByteBuf to determine whether there are \ n or \ r\n, and if so, take this as the end position. If the maximum row is read and there is still no newline, an exception will be thrown

StringDecoder: converts the received object into a string, and then continues to call the subsequent handler

Time server:

public class NettyTimeServer {
    public static void main(String[] args) {
        int port = 8080;
        new NettyTimeServer().bind(port);
    }
    public void bind(int port) {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap
                .group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChildChannelHandler());
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            //Wait for the server listening port to close
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
    private static class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            //When handling sticky packets / half packets, it will traverse whether there is an escape character in the data. If not, it will throw exception information
            socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
            //Decode the received data into a string, and then you can directly obtain string type data
            socketChannel.pipeline().addLast(new StringDecoder());
            socketChannel.pipeline().addLast(new TimeServerHandler());
        }
    }
    private static class TimeServerHandler extends ChannelHandlerAdapter {

        private int counter;

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("The time server receive order : " + body + ", the counter is :" + ++counter);
            String currentTime = "query time order".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "bad order";
            currentTime = currentTime + System.getProperty("line.separator");
            ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes(StandardCharsets.UTF_8));
            ctx.write(resp);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.flush();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

Time client:

public class NettyTimeClient {

    public static void main(String[] args) {
        try {
            new NettyTimeClient().connect("127.0.0.1", 8080);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void connect(String host, int port) throws InterruptedException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap client = new Bootstrap();
        client.group(eventLoopGroup)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                    socketChannel.pipeline().addLast(new StringDecoder());
                    socketChannel.pipeline().addLast(new TimeClientHandler());
                }
            });
        ChannelFuture future = client.connect(new InetSocketAddress(host, port)).sync();
        future.channel().closeFuture().sync();
    }


    public static class TimeClientHandler extends ChannelHandlerAdapter {

        private int counter;

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            byte[] req = ("query time order" + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8);
            for (int i = 0; i < 100; i++) {
                ByteBuf buffer = Unpooled.buffer(req.length);
                buffer.writeBytes(req);
                ctx.writeAndFlush(buffer);
            }
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            /*ByteBuf byteBuf = (ByteBuf) msg;
            byte[] bytes = new byte[byteBuf.readableBytes()];
            byteBuf.readBytes(bytes);
            String body = new String(bytes, StandardCharsets.UTF_8);*/
            String body = (String) msg;
            System.out.println("Now is: " + body + "; the counter is :" + ++counter);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.close();
        }
    }
}

DelimiterBasedFrameDecoder: Specifies delimiter segmentation

FixedLengthFrameDecoder: fixed length for segmentation

If the separator decoder does not read the separator when the size exceeds the limit, it will throw an exception. The main purpose is to prevent memory overflow

ByteBuf delimiter = Unpooled.copiedBuffer("@".getBytes(StandardCharsets.UTF_8));
                        //1024 represents the maximum length of a single message. When the separator is not found after reaching the length, a TooLongFrameException exception is thrown, and the specified separator is used to split the packet
                        DelimiterBasedFrameDecoder delimiterBasedFrameDecoder = new DelimiterBasedFrameDecoder(1024, delimiter);
                        socketChannel.pipeline().addLast(delimiterBasedFrameDecoder);

//The fixed length decoder decodes according to the specified length. If the length is insufficient, it will wait for the next packet, and then spell the packet
                        socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(20));

4. Codec technology

ObjectInputStream and ObjectOutputStream provided by java can directly write Java objects as a storable byte array to files and network transmission.

There are two main purposes of java serialization

  • Network transmission: the transmitted java object needs to be encoded into byte array or Bytebuffer object, and the remote service reads the Bytebuffer or byte array and converts it into java object
  • Object persistence

4.1 disadvantages of Java serialization

  • Unable to cross language: Java serialization is not competent if heterogeneous language processes are used for interaction
  • The serialized code stream is too large
  • Serialization performance is too low

4.2 mainstream codec framework in the industry

1. Protobuf

Google open source framework, data structure is described in. proto file

characteristic:

  • Structured data storage structure (XML, JSON, etc.)
  • Efficient codec performance
  • Language independent, platform independent, good scalability
  • Java, C + + and Python are officially supported

2. Thrift

Facebook is open source

Three typical encoding and decoding modes are supported

  • General binary codec
  • Compressed binary codec
  • Optimized optional field compression codec

3. JBoss Marshalliing

The serialization API package for java objects corrects many problems with the serialization package provided by JDK

advantage:

  • Pluggable class parser and more convenient class loading customization strategy can be realized through one interface
  • Pluggable object replacement technology does not need to be inherited
  • The pluggable predefined class cache table can reduce the length of serialized byte array and improve the serialization performance of common types of objects
  • Java serialization can be implemented without implementing the java.io.Serializable interface
  • Improve the serialization performance of objects through caching technology

4.3 netty object encoding and decoding

The ObjectEncoder and ObjectDecoder processors provided by Netty are used for encoding and decoding

//Add a codec in the socket pipeline to automatically serialize and deserialize data
//Create a WeakReferenceMap to cache the class loader and support multi-threaded concurrent access. When the virtual machine is short of memory, it will release the memory in the cache to prevent memory leakage
            socketChannel.pipeline().addLast(
                new ObjectDecoder(1024 * 1024,                        ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
            socketChannel.pipeline().addLast(new ObjectEncoder());
            socketChannel.pipeline().addLast(new SubReqServerHandler());

Use the serialization provided by Protobuf for processing

			socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder()); //Processing half package
			socketChannel.pipeline().addLast(new ProtobufDecoder(Decoding target class.class)); //There is no semi package processing logic
			socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
            socketChannel.pipeline().addLast(new ProtobufEncoder());
            socketChannel.pipeline().addLast(new SubReqServerHandler());

Posted by tartou2 on Tue, 23 Nov 2021 04:01:41 -0800