Glue/Unpack Processing in Netty

Posted by linux1880 on Mon, 09 Dec 2019 18:23:35 +0100

TCP is a protocol based on streaming. Request data is not bounded in its transmission, so when we read a request, we may not get a complete packet.If a packet is large, it may be sliced into multiple packets for multiple transmissions.At the same time, if there are multiple small packages, they may be integrated into one large package for transmission.This is the TCP protocol's glue/unpack concept.

This article is based on Netty5 analysis

Paste/unpack description

Assuming there are currently 123 and abc packets, their transmission is illustrated as follows:

  • I is normal, two separate complete packages are transferred twice.
  • II is the sticky case, 123 and abc are packaged into one package.
  • III is a case of unpacking. The description in the diagram splits 123 into 1 and 23, and 1 and abc are transmitted together.123 and abc may also be abc for unpacking.It is even possible to split 123 and abc several times.

Netty Pack/Unpack Problem

To highlight Netty's sticking/unpacking issues, here is an example of how to reproduce the problem. Here is the main code highlighting the problem:

Server:

/**
 * Read and Write Action Class for Server-side Network Events
 * 
 * Created by YangTao.
 */
public class ServerHandler extends ChannelHandlerAdapter {
    // Receive Message Counter
    private int i = 0;

    // client-side messages
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        i++;
        
        System.out.print(msg);
        
        // Count each read message
        System.out.println("================== ["+ i +"]");
        // Send Answer Message to Client
        ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i).getBytes());
        ctx.write(rmsg);
    }
    
    // Other operations...
}  

Client:

/**
 * Client sends data
 * 
 * Created by YangTao.
 */
public class NettyClient {

    public void send() {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();

        try {
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));
                            pipeline.addLast(new ClientHandler());
                        }
                    });
            Channel channel = bootstrap.connect(HOST, PORT).channel();
            int i = 1;
            while (i <= 300){
                channel.writeAndFlush(String.format("[time %s: \t%s]", new Date(), i));
                // Print number of send requests
                System.out.println(i);
                i++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
             if (group != null)
                 group.shutdownGracefully();
         }
    }
}

In the above code, our first response is to understand that if all the data is sent successfully by the client and received by the server under non-exceptional circumstances.Then you can see from the printed information that the number of times the client sends i and the number of messages received by the server i should be the same.Then run the program to see the printed results.

As shown in the figure above, the last number in [] is the number that is received by the package in its entirety (case I in the glue/unpack diagram).However, there are glue cases in 37 and 38 (case II in the glue/unpack diagram), and the two data are glued together.

As you can see in the figure above, the 167 data is split into two parts (drawing the green line data in the picture), in which case it is unpacked (case III in the stick/unpack diagram).

The above programs do not take TCP sticking/unpacking into account, so if it is a practical application of the program, it can not guarantee the normal condition of the data, which will lead to program exceptions.

Netty Solves the Stick/Unpack Problem

LineBasedFrameDecoder Line Break Handling

Netty has the advantages of power, convenience and ease of use. It also provides a variety of coding and decoding solutions for sticking/unpacking issues, and is easy to understand and master.
LineBasedFrameDecoder and StringDecoder (converting received objects into strings) are used here to solve the glue/unpack problem.
Simply add the LineBasedFrameDecoder and the StringDecoder on the server and the client, respectively, because it's a two-way session, so add both ends. Since I added the StringDecoder initially, adding the LineBasedFrameDecoder is enough.
Server:

Client:

Server-side network event operations:

/**
 * Read and Write Action Class for Server-side Network Events
 * 
 * Created by YangTao.
 */
public class ServerHandler extends ChannelHandlerAdapter {
    // Receive Message Counter
    private int i = 0;

    // client-side messages
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        i++;
        
        System.out.print(msg);
        
        // Count each read message
        System.out.println("================== ["+ i +"]");
        // Send Answer Message to Client
        ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i + System.getProperty("line.separator")).getBytes());
        ctx.write(rmsg);
    }
    
    // Other operations...
}

Client sends data:

/**
 * Client sends data
 * 
 * Created by YangTao.
 */
public class NettyClient {

    public void send() {
        // Connection operation...

        try {
            // Get channel
            Channel channel = channel();
            int i = 1;
            ByteBuf buf = null;
            while (i <= 300){
                String str = String.format("[time %s: \t%s]", new Date(), i) + System.getProperty("line.separator");
                byte[] bytes = str.getBytes();
                // Write Buffer
                buf = Unpooled.buffer(bytes.length);
                buf.writeBytes(bytes);
                channel.writeAndFlush(buf);
                // Print number of send requests
                System.out.println(i);
                i++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        
        // Exit the operation...
    }
}

Watch the code carefully and you should see that the code now has a line break at the end of the message each time it sends it.Note that when using LineBasedFrameDecoder, line breaks must be added, otherwise the receiving side will not receive the message. If handwriting line breaks, remember to distinguish between different systems for adaption.

After several tests on 3W requests, no more sticking/unpacking happened, just see if the last data number is the same.

DelimiterBasedFrameDecoder Custom Delimiter

Custom and line break delimiters are similar, just replace the data you send with the line break you set.

Both the server and client add DelimiterBasedFrameDecoder to the pipeline:

// Specified separator
public static final String DELIMITER = "$@$";

// If there is no delimiter in the current 2048 bytes of data, an exception will be thrown to avoid memory overflow.You can also customize pre-checking the data currently being read to customize rules that are exceeded here
pipeline.addLast(new DelimiterBasedFrameDecoder(
        2048, 
        Unpooled.wrappedBuffer(DELIMITER.getBytes())) // Splitter Buffer Object
);

FixedLengthFrameDecoder is based on a fixed length

Set a fixed length for data transmission, if not a fixed length, use space completion.

Both the server and client add FixedLengthFrameDecoder to the pipeline:

// 100 is the specified fixed length
ch.pipeline().addLast(new FixedLengthFrameDecoder(100));

Each time the data is read, it will be decoded according to the fixed length set in FixedLengthFrameDecoder. If there is a sticky package, it will be decoded several times. If there is a case of unpacking, FixedLengthFrameDecoder will first cache the information of the current part of the package. When receiving the next package, it will stitch with the cached part of the package to know that it meets the specified length.

Dynamically specify length

Dynamically specifying the length means that the length of each message is specified with the header, where the encoder used is LengthFieldBasedFrameDecoder.

pipeline().addLast(
        new LengthFieldBasedFrameDecoder(
        2048, // Maximum length of frame, i.e. maximum per packet
        0, // Length Field Offset
        4, // Number of bytes occupied by length field
        0, // The length of the message header, which can be negative
        4) // Number of bytes to ignore, starting with the header, where the entire package
);

Create your own message object encoder when sending messages

// Create byteBuf
ByteBuf buf = getBuf();

// .....

// Set the message content length
buf.writeInt(msg.length());
// Set Message Content
buf.writeBytes(msg.getBytes("UTF-8"));

When the server reads, it reads directly, without any other special operations.

In addition to the existing solutions provided by Netty above, custom protocols can also be implemented by rewriting MessageToByteEncoder encoding.

summary

Netty provides users with a wide range of solutions to stick/unpack, and can be very happy to decode a variety of messages automatically. It is also very easy to master and understand in the process of use, which greatly improves the efficiency and stability of development.
Personal blog: https://ytao.top
My Public Number ytao

Topics: Java Netty network Session encoding