[Netty] decoder related: DelimiterBasedFrameDecoder

Keywords: Netty codec Java Junit

Article directory

Preface

This section will take a look at the decoder that Netty provides to users out of the box: DelimiterBasedFrameDecoder.

Before reading, try to make sure there is a certain ByteBuf foundation. For example, you must know the readerIndex and writerIndex. Please refer to my previous blog records [about ByteBuf].

If there is any doubt about the general process of decoder, it is recommended to go back and have a look [ByteToMessageDecoder] In this paper, we will not talk about the whole process, and focus on the decode method of decoder.

Netty Version: 4.1.6


Experimental code

Also take the unit test provided by Netty:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import org.junit.Test;

import java.nio.charset.Charset;

import static io.netty.util.ReferenceCountUtil.releaseLater;
import static org.junit.Assert.*;

public class DelimiterBasedFrameDecoderTest {

    @Test
    public void testMultipleLinesStrippedDelimiters() {
        EmbeddedChannel ch = new EmbeddedChannel(new DelimiterBasedFrameDecoder(8192, true,
                delimiter()));
        ch.writeInbound(Unpooled.copiedBuffer("firstgasecondga", Charset.defaultCharset()));

        System.out.println(releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
        System.out.println(releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
        System.out.println(releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
        ch.finish();
    }

    // Custom separator
    private static ByteBuf[] delimiter() {
        return new ByteBuf[] {
                Unpooled.wrappedBuffer(new byte[] { 'a' }),
                Unpooled.wrappedBuffer(new byte[] { 'g' }),
        };
    }
}

Output results:

first

second
  • first and second are indeed separated. That is to say, the second readInbound does not have a string. It's not that I wrote markdown wrong.
  • Why read in bound for the second time? After reading the analysis of this article, you see.
  • If the "firstgasecondga" of the experiment code is replaced by "firstgeeasecondga", the blank in the middle of the output result will become "ee"

Follow up source code

DelimiterBasedFrameDecoder inheritance relationship

Let's first look at the inheritance diagram of DelimiterBasedFrameDecoder:


From the above class diagram, combined with the previous learning experience of pipeline, it is not difficult to find that we can actually regard LineBasedFrameDecoder as a channel handler, that is to say, decoding is actually one of the links in event propagation and processing. The code is similar to the following:

    // slightly
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        public void initChannel(SocketChannel ch) {
            ch.pipeline().addLast(new Base64Decoder());
            ch.pipeline().addLast(new DelimiterBasedFrameDecoder());
            ch.pipeline().addLast(new FixedLengthFrameDecoder(3));
            ch.pipeline().addLast(new LineBasedFrameDecoder(10, false, false));

       }
  • (the above passage has been said in the previous section. It's just for me to emphasize.)

Properties of DelimiterBasedFrameDecoder

I still annotate the source code:

public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {

    /** Separator, multiple */
    private final ByteBuf[] delimiters;
    /** How long is the maximum packet capacity */
    private final int maxFrameLength;
    /** Do packets need to keep delimiters */
    private final boolean stripDelimiter;
    /** Whether to throw an exception after maxFrameLength is exceeded */
    private final boolean failFast;
    /** Whether it belongs to discard mode */
    private boolean discardingTooLongFrame;
    /** Bytes discarded */
    private int tooLongFrameLength;
    /** Set only when decoding with "\n" and "\r\n" as the delimiter.  */
    /** As mentioned before, line decoder means that if the separator is only \ N and \ r\n, it will be directly thrown to line decoder (code reuse)*/
    private final LineBasedFrameDecoder lineBasedDecoder;
    ...(Non attribute omitted)

Analysis of construction method

You may wonder: why does the LineBasedFrameDecoder mentioned in the previous section appear in the separator decoder? When is this LineBasedFrameDecoder!=null? The answers to these questions are actually in the construction method. Let's take a look at the following:
io.netty.handler.codec.DelimiterBasedFrameDecoder#DelimiterBasedFrameDecoder(int, boolean, boolean, io.netty.buffer.ByteBuf...)

    public DelimiterBasedFrameDecoder(
            int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) {
        validateMaxFrameLength(maxFrameLength);
        if (delimiters == null) {
            throw new NullPointerException("delimiters");
        }
        if (delimiters.length == 0) {
            throw new IllegalArgumentException("empty delimiters");
        }

        // If split by line break only
        if (isLineBased(delimiters) && !isSubclass()) {
            // Create a newline decoder object
            lineBasedDecoder = new LineBasedFrameDecoder(maxFrameLength, stripDelimiter, failFast);
            this.delimiters = null;
        } else {
            this.delimiters = new ByteBuf[delimiters.length];
            for (int i = 0; i < delimiters.length; i ++) {
                ByteBuf d = delimiters[i];
                validateDelimiter(d);
                this.delimiters[i] = d.slice(d.readerIndex(), d.readableBytes());
            }
            // If the separator is not just a line feed or does not include a line feed, it is left blank.
            lineBasedDecoder = null;
        }
        this.maxFrameLength = maxFrameLength;
        this.stripDelimiter = stripDelimiter;
        this.failFast = failFast;
    }

Follow up the isLineBased method and verify if you want to:
io.netty.handler.codec.DelimiterBasedFrameDecoder#isLineBased

    // Determine if only line breaks are used as separators
    private static boolean isLineBased(final ByteBuf[] delimiters) {
        // If the number of delimiters is less than 2, it must not be a line break (line breaks include \ r\n and \ n) and return false
        if (delimiters.length != 2) {
            return false;
        }
        // Get the first two separators
        ByteBuf a = delimiters[0];
        ByteBuf b = delimiters[1];
        if (a.capacity() < b.capacity()) {
            a = delimiters[1];
            b = delimiters[0];
        }

        // Determine if it is split by \ n & & \ R \ n
        return a.capacity() == 2 && b.capacity() == 1
                && a.getByte(0) == '\r' && a.getByte(1) == '\n'
                && b.getByte(0) == '\n';
    }

To put it bluntly: if the separator is just two line breaks, it can be directly thrown to the LineBasedFrameDecoder instance for processing. Reuse Principle~


decode implementation

Next is the core part, which decodes the data from the ByteToMessageDecoder accumulator.

Find the decode method and look at the code directly:
io.netty.handler.codec.DelimiterBasedFrameDecoder#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf, java.util.List<java.lang.Object>)

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            // The decoded object is added to out and handed to ByteToMessageDecoder for propagation
            out.add(decoded);
        }
    }

Continue to follow up the decode method, and also write what the code does into the comments. The following will also draw a picture for analysis:
io.netty.handler.codec.DelimiterBasedFrameDecoder#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf)

    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        // If the separator is a line break (determined when constructing this decoder)
        if (lineBasedDecoder != null) {
            // Direct throw to line processor
            return lineBasedDecoder.decode(ctx, buffer);
        }
        // Try all delimiters and choose the delimiter which yields the shortest frame.
        // index for recording the minimum separator
        int minFrameLength = Integer.MAX_VALUE;
        // Minimum length separator
        ByteBuf minDelim = null;
        for (ByteBuf delim: delimiters) {
            int frameLength = indexOf(buffer, delim);
            // Calculate which separator reads the minimum data length
            if (frameLength >= 0 && frameLength < minFrameLength) {
                // Record minimum length
                minFrameLength = frameLength;
                // Minimum record length separator
                minDelim = delim;
            }
        }

        // Start decoding logic below

        if (minDelim != null) {
            // Returns the length occupied by the separator
            int minDelimLength = minDelim.capacity();
            ByteBuf frame;

            // If it belongs to discard mode
            if (discardingTooLongFrame) {
                // We've just finished discarding a very large frame.
                // Go back to the initial state.
                discardingTooLongFrame = false;
                buffer.skipBytes(minFrameLength + minDelimLength);

                int tooLongFrameLength = this.tooLongFrameLength;
                this.tooLongFrameLength = 0;
                if (!failFast) {
                    fail(tooLongFrameLength);
                }
                return null;
            }

            // Len gt h of packets read > maximum packets currently allowed
            if (minFrameLength > maxFrameLength) {
                // Discard read frame.
                // Read pointer moves after separator
                buffer.skipBytes(minFrameLength + minDelimLength);
                // Throw exception (not exception propagation)
                fail(minFrameLength);
                return null;
            }

            // Need to keep separator
            if (stripDelimiter) {
                // Do not keep separator
                frame = buffer.readRetainedSlice(minFrameLength);
                buffer.skipBytes(minDelimLength);
            } else {
                // Keep separator
                frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
            }
            // Return packets read
            return frame;
        }
        // If you don't read the separator
        else {
            // Non discard mode
            if (!discardingTooLongFrame) {
                // Packet length greater than maximum capacity
                if (buffer.readableBytes() > maxFrameLength) {
                    // Discard the content of the buffer until a delimiter is found.
                    tooLongFrameLength = buffer.readableBytes();
                    buffer.skipBytes(buffer.readableBytes());
                    // Enable discard mode
                    discardingTooLongFrame = true;
                    // Whether to throw an exception (not exception propagation)
                    if (failFast) {
                        fail(tooLongFrameLength);
                    }
                }
            }
            else {
                // Discard mode
                // Still discarding the buffer since a delimiter is not found.
                tooLongFrameLength += buffer.readableBytes();
                buffer.skipBytes(buffer.readableBytes());
            }
            return null;
        }
    }

Before looking at the overall process, you may be confused by the above code segment:

        // Minimum length separator
        ByteBuf minDelim = null;
        for (ByteBuf delim: delimiters) {
            int frameLength = indexOf(buffer, delim);
            // Calculate which separator reads the minimum data length
            if (frameLength >= 0 && frameLength < minFrameLength) {
                // Record minimum length
                minFrameLength = frameLength;
                // Minimum record length separator
                minDelim = delim;
            }
        }

Here is a picture to explain:

  • To put it bluntly, it is to obtain the separator with the shortest distance from the record readerIndex and the distance from the readerIndex to the separator.

Combined with the experimental code, the above decode process is roughly as follows:

Well, the general process of decode experimental code test data with decode method is as shown in the figure above. I believe that the above doubts can be solved after reading. For exceptions such as discard mode, you can continue to follow up with unit tests provided by Netty if you are interested. I won't go into details here.

Published 45 original articles, won praise 3, visited 1126
Private letter follow

Posted by ATS16805 on Sat, 15 Feb 2020 06:11:08 -0800