Why is it said that Decoder and Encoder are the core components of Netty? How does Netty use the template method pattern to efficiently complete decoding and encoding? A thorough analysis in 10,000 words

The job of the Netty inbound processor is the packet decoding and business processing of the IO processing operation. During the inbound processing, the bottom layer of Netty first reads the ByteBuf binary data, and finally needs to convert it into a java POJO object. This process requires a Decoder to complete.

The job of Netty’s outbound processor is to encode the target data in the IO processing operation and write the data packet to the channel. During the outbound processing, the Java POJO object needs to be converted into the final ByteBuf binary data, and then it can be sent to the peer through the underlying Java channel. This conversion process needs to be completed through the Encoder.

So Decoder and Encoder are the core components of Netty.

The decoder must ensure that the ByteBuf binary data packet received must be a complete POJO object binary data packet. Before the Decoder performs the decoding (or deserialization) operation, it must first be determined: the binary data packet it receives must be a complete packet, not a half packet or a sticky packet.

Therefore, during the decoding/encoding process, the problems of half-packets and sticky packets need to be solved.

What is the half-package problem

The place where Netty sends and reads data is the ByteBuf buffer. For the sending end, each sending is to write a ByteBuf to the channel. When sending data, fill in the ByteBuf first, and then send it out through the channel. For the receiving end, each read is to read a ByteBuf from the channel through the inbound method of the Handler business processor.

Sticky packet means that the Receiver (receiving end) receives a ByteBuf, which contains multiple ByteBufs of the Sender (sending end). The multiple ByteBufs of the sending end are “glued” together at the receiving end.

Half a packet means that the Receiver “opened” a ByteBuf of the Sender and received multiple broken packets. In other words, the Receiver receives a small portion of a ByteBuf from the Sender.

The situation of “sticky bag” can be regarded as a special “half bag”. “Sticky packets” and “half-packets” can be collectively referred to as the “half-packet problem” of transmission. The details are shown in the figure below:

The underlying network transmits data in the form of binary byte messages, and before the data enters the transmission stage, CPU data copying and DMA data copying also occur. Regardless of the data transmission stage or the data copy stage, there may be secondary separation of binary byte data.

The process of writing data is roughly as follows: the encoder converts a java type data into binary ByteBuf buffer data that can be transmitted by the underlying layer. The application layer Netty program on the sending end sends data in ByteBuf units. The data will first be copied to the underlying operating system kernel buffer through CPU copying; then through DMA copying, the DMA device will copy the data in the kernel buffer. When copied to the network card device, a single data packet in the TCP kernel buffer may be relatively small. A DMA copy may contain more than one small packet in the kernel buffer, and multiple small data packets will be copied together to improve efficiency.

After the data is copied to the network card device, since the effective data size of a TCP protocol message is limited, the specific MSS value will be negotiated during the three-way handshake phase, and the maximum will not exceed 1460 bytes. Therefore, the network card device protocol stack handler will re-encapsulate the data packet according to the TCP/IP protocol specification, encapsulate it into a transport layer TCP protocol message and then send it. During this data packet encapsulation process, binary data will also be sent. secondary separation.

Therefore, whether the binary data is separated in the data transmission stage or the binary data is separated in the data copy stage, it may lead to the final sticky phenomenon or half-packet phenomenon.

Netty’s basic idea to solve the half-packet problem is that at the receiving end, the Netty program needs to reassemble the read process buffer ByteBuf at the application layer according to the custom protocol, and reassemble the data packets at the application layer. This process is called subpackaging/unpacking,

This process on the receiving end is often called packetization or unpacking. There are two main methods for subcontracting in Netty:

(1) You can customize the decoder packetizer: define your own user buffer packetizer based on ByteToMessageDecoder or ReplayingDecoder.

(2) Use Netty’s built-in decoder. For example, you can use Netty’s built-in LengthFieldBasedFrameDecoder custom length packet decoder to correctly packetize the user buffer ByteBuf.

Netty decoder

Netty’s decoder is an InBound inbound processor, responsible for processing “inbound data”. It can also decode or format the input data passed by the previous station’s Inbound inbound processor, and then send it to the next station. An Inbound handler.

The decoder’s responsibility is to decode the data of the input type ByteBuf buffer and output the java Pojo object. Netty’s built-in decoder is ByteToMessageDecoder. The decoders in Netty are all Inbound inbound processor types, and almost all of them directly or indirectly implement the super interface ChannelInboundHandler for inbound processing.

ByteToMessageDecoder is a very important decoder base class. It is an abstract class that implements the basic logic and process of decoding processing. ByteToMessageDecoder inherits from the ChannelInboundHandlerAdapter adapter and is an inbound processor used to complete the decoding function from ByteBuf to Java POJO objects.

The decoding process of ByteToMessageDecoder is as follows:

step1: It decodes the input data from the previous station into Bytebuf and decodes a List object list;

step2: Iterate the List list and pass the Java POJO objects to the next Inbound processor one by one.

Since ByteToMessageDecoder is an abstract class:

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {<!-- -->
...
}

Directly using the ByteToMessageDecoder class cannot complete the decoding of the specific Java type of ByteBuf bytecode, and it also needs to rely on the specific implementation of its subclasses.

The decoding method of the ByteToMessageDecoder abstract class is decode, which is also an abstract method:

 protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

As the parent class of the decoder, ByteToMessageDecoder only provides an overall framework: it will call the decode method of the subclass to complete the specific binary byte decoding, and then obtain the Object result decoded by the subclass and put it into its own internal result list In List, ultimately, the parent class will be responsible for passing the elements in List to the next station one by one.

From a design pattern perspective, ByteToMessageDecoder adopts the template method design pattern. The ByteToMessageDecoder abstract class defines the algorithm skeleton. The subclasses of ByteToMessageDecoder complete the specific implementation of the algorithm, that is, all Object instances that need to be decoded from the inbound Bytebuf are added to the List list of the parent class.

The implementation process of ByteToMessageDecoder subclass is as follows:

step1: Inherit the ByteTOMessageDecoder abstract class.

step2: Implement the decode abstract method of its base class, write the ByteBuf to target POJO decoding logic into this method, and be responsible for decoding the binary data in Bytebuf into Java POJO objects.

step3: After decoding is completed, the decoded Java POJO object needs to be put into the List actual parameter of the decode method. This actual parameter is the decoding result collection container passed in by the parent class.

The remaining work is automatically completed by the parent class ByteToMessageDecoder. During the processing of the pipeline, after the parent class completes the decoding of the subclass, it will pass the results collected by List to the next Inbound processor one by one.

The code example for decoding the bytes in the ByteBuf buffer into the Integer integer type is as follows:

public class Byte2IntegerDecoder extends ByteToMessageDecoder {<!-- -->

    //hook implementation
    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {<!-- -->
        while (in.readableBytes() >= 4) {<!-- -->
            Integer anInt = in.readInt();
            Logger.info("Decode an integer: " + anInt);
            out.add(anInt);
        }
    }
}

Since the custom decoder still has various problems, such as the need to check the length of ByteBuf. Therefore, Netty also provides a number of Decoder decoders that can be used out of the box:

Let’s sort out some of the more common decoders:

(1)Fixed-length packet decoder FixedLengthFrameDecoder

Applicable scenarios: The length of each received data packet is fixed, such as 100 bytes. In this scenario, you only need to add this decoder to the pipeline, it will split the inbound ByteBuf data packet into packets with a length of 100, and then send them to the next channelHandler inbound processor.

(2)Line split packet decoder LineBasedFrameDecoder

Applicable scenarios: For each ByteBuf data packet, use the newline character (or carriage return and line feed character) as the boundary delimiter of the data packet. In this scenario, you only need to add the LineBasedFrameDecoder decoder to the pipeline. Netty will use the newline delimiter to split the ByteBuf data packet into complete application layer ByteBuf data packets and then send them to the next station.

(3)Custom delimiter packet decoder DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder is a generic version of LineBasedFrameDecoder that splits by line. The difference is that this decoder is more flexible and can customize delimiters instead of being limited to newlines. If this decoder is used, the received data packet must have the corresponding delimiter at the end.

(4)Custom length packet decoder LengthFieldBasedFrameDecoder

This is a flexible length based decoder. In the ByteBuf data packet, a length field is added to save the length of the original data packet. When decoding, the original data packet will be extracted according to this length.

Netty Encoder

After Netty’s business processing is completed, the result of the business processing is often a Java POJO object, which needs to be encoded into the final ByteBuf binary type and written to the underlying Java channel through the pipeline. This process requires the use of Encoder.

In Netty, the encoder is an OutBound outbound processor, responsible for processing outbound data. The encoder can also encode or format the input (Input) data passed by the previous Outbound outbound processor, and then pass it to Next-stop ChannelOutboundHandler outbound handler.

Encoders in Netty are responsible for encoding “outbound” a Java POJO object into a binary ByteBuf, or into another Java POJO object.

The encoder is a concrete implementation class of ChannelOutboundHandler. After an encoder encodes the outbound object, the encoded data will be passed to the next ChannelOutboundHandler outbound processor for subsequent outbound processing. MessageToByteEncoder is Netty’s built-in encoder. It is an abstract class whose function is to encode a Java POJO object into a ByteBuf data packet.

MessageToByteEncoder packet. It is an abstract class that only implements the basic process of encoding. During the encoding process, it is completed by calling the encode abstract method.

public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {<!-- -->
 protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
}

The encode() method does not have a specific implementation of encode encoding logic. The work of implementing the encode abstract method needs to be completed by subclasses.

The implementation process of MessageToByteEncoder subclass is as follows:

step1: Inherit the MessageToByteEncoder abstract class.

step2: Implement the encode abstract method of its base class, and encode the Java POJO object into a binary ByteBuf, or convert it into another Java POJO object.

step3: After the encoding is completed, the base class MessageToByteEncoder will send the output ByteBuf data packet to the next station.

Example code for encoding Java integers into binary ByteBuf packets is as follows:

 public class Integer2ByteEncoder extends MessageToByteEncoder<Integer> {<!-- -->

    @Override
    public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out)
            throws Exception {<!-- -->

        // target bytebuf
        out.writeInt(msg);

        Logger.info("encoder Integer = " + msg);
    }
}

Using the MessageToMessageEncoder encoder, you can also encode a certain POJO object into another POJO object. Its subclasses inherit the MessageToMessageEncoder abstract class and implement the encode() abstract method.

Combining encoder and decoder

In actual development, due to the close relationship between the inbound and outbound data, the relationship between the encoder and the decoder is very close. Encoding and decoding are a close and mutually supporting relationship. During pipeline processing, data often flows in and out, decoding when it comes in and encoding when it goes out. Therefore, if some encoding logic is added to the same pipeline, it is often necessary to add a corresponding decoding logic.

It is troublesome to add separate decoders and encoders that match each other in two steps when adding them to the channel pipeline. Netty provides a Codec (codec) type, ByteToMessageCodecI>, an abstract class. Functionally speaking, inheriting it is equivalent to inheriting the two base classes of ByteToMessageDecoder decoder and MessageToByteEncoder encoder.

The codec ByteToMessageCodec also contains two abstract methods: encode and decode. Both methods need to be implemented by ourselves:

(1) Encoding method encode(ChannelHandlerContext, I, ByteBuf)

(2) Decoding method decode(ChannelHandlerContext, ByteBuf, List)

Integer to byte, byte to integer encoding and decoding examples are as follows:

public class Byte2IntegerCodec extends ByteToMessageCodec<Integer> {<!-- -->

    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {<!-- -->
        if (in.readableBytes() >= 4) {<!-- -->
            int i = in.readInt();
            System.out.println("Decoder i= " + i);
            out.add(i);
        }

    }

    @Override
    public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out)
            throws Exception {<!-- -->
        out.writeInt(msg);
        System.out.println("write Integer = " + msg);
    }


}

The combination of encoder and decoder is simply through inheritance. The encode method of the previous encoder and the decode method of the decoder are placed in the same custom class, which is logically closer. Of course, when used, it only needs to be added once to the pipeline.

syntaxbug.com © 2021 All Rights Reserved.