Netty系列-8 Netty处理粘包和半包问题

news2024/10/4 13:46:53

1.半包和粘包问题

TCP协议是基于字节流的数据通讯协议,数据被看做是一连串的字节流;不具备边界信息,给接收方带来半包和粘包问题。
半包:TCP传输时,将数据切割成一个个数据包进行传输。接收方一次读取操作,如果没有接受完完整的数据包而只能读取部分数据时,出现半包问题。
在这里插入图片描述
粘包:发送方发送的两个或者多个数据包在接受方接受时被合并成了一个数据包。
在这里插入图片描述
半包和粘包问题可以通过业务层解决。

2.常见处理器

Netty中引入了4个常见的处理器,用于解决不同场景下的TCP粘包和半包问题。以下分小节分别介绍使用方式。

2.1 FixedLengthFrameDecoder

数据流过FixedLengthFrameDecoder后,消息会被裁接或者拼接为一个个固定长度的ByteBuf消息对象,通过构造参数确定消息长度。

// 消息长度为2
channel.pipeline().addLast(new FixedLengthFrameDecoder(2));
// 打印消息详情
channel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

客户端向服务器发送123456消息时,服务器消息显示如下:

#消息片段【12】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x5368287f, L:/127.0.0.1:9998 - R:/127.0.0.1:53981] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 32                                           |12              |
+--------+-------------------------------------------------+----------------+

#消息片段【34】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x5368287f, L:/127.0.0.1:9998 - R:/127.0.0.1:53981] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 33 34                                           |34              |
+--------+-------------------------------------------------+----------------+

#消息片段【56】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x5368287f, L:/127.0.0.1:9998 - R:/127.0.0.1:53981] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 35 36                                           |56              |
+--------+-------------------------------------------------+----------------+

服务端程序修改为:

channel.pipeline().addLast(new FixedLengthFrameDecoder(2));
channel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

channel.pipeline().addLast(new FixedLengthFrameDecoder(4));
channel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

客户端向服务器发送123456消息时,服务器消息显示如下:

#消息片段【12】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x7940d10b, L:/127.0.0.1:9998 - R:/127.0.0.1:54420] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 32                                           |12              |
+--------+-------------------------------------------------+----------------+

#消息片段【34】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x7940d10b, L:/127.0.0.1:9998 - R:/127.0.0.1:54420] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 33 34                                           |34              |
+--------+-------------------------------------------------+----------------+

#消息片段【1234】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x7940d10b, L:/127.0.0.1:9998 - R:/127.0.0.1:54420] READ: 4B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 32 33 34                                     |1234            |
+--------+-------------------------------------------------+----------------+

#消息片段【56】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x7940d10b, L:/127.0.0.1:9998 - R:/127.0.0.1:54420] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 35 36                                           |56              |
+--------+-------------------------------------------------+----------------+

#消息片段【78】保存在内存中。

2.2 LineBasedFrameDecoder

数据流过LineBasedFrameDecoder后,按换行符(\r或\r\n)裁接消息对象,可通过构造参数确定最大消息长度。

// 最大消息长度为1024
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));

当截取的消息长度超过设定的1024时,将消息丢弃。
测试使用如下案例:

// 最大长度设置为10
channel.pipeline().addLast(new LineBasedFrameDecoder(10));
channel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

客户端发送如下消息时:

15:53:03 发送数据:1234567890123
456
789

服务器日志如下:

#【对1234567890123超出长度部分消息报错,并丢弃】
TooLongFrameException: frame length (16) exceeds the allowed maximum (10)
# ...省略报错详细信息

#【消息片段456】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x0bade5de, L:/127.0.0.1:9998 - R:/127.0.0.1:61868] READ: 3B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 34 35 36                                        |456             |
+--------+-------------------------------------------------+----------------+

#【消息片段789】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x0bade5de, L:/127.0.0.1:9998 - R:/127.0.0.1:61868] READ: 3B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 37 38 39                                        |789             |
+--------+-------------------------------------------------+----------------+

注意点:
LineBasedFrameDecoder提供了两个构造函数:

public LineBasedFrameDecoder(final int maxLength) {
	this(maxLength, true, false);
}

public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
	// 最大消息长度
	this.maxLength = maxLength;
	// 超出最大消息长度时,是否尽快抛出异常
	this.failFast = failFast;
	// 是否删除换行符,默认为删除
	this.stripDelimiter = stripDelimiter;
}

上述测试案例中构造的LineBasedFrameDecoder,处理消息后会取出换行符。
如业务需要保留换行符,可使用如下方式:

// 裁切后的消息对象包括换行符
new LineBasedFrameDecoder(65535, true, true)

2.3 DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder可通过指定分隔符裁切消息。
包含的属性有:

// 最大数据帧长度
private final int maxFrameLength;
// 分隔符列表
private final ByteBuf[] delimiters;
// 是否保留分隔符
private final boolean stripDelimiter;
// 消息超出最大长度,是否快速抛出异常
private final boolean failFast;

对应提供了以下多个构造函数:

public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {//...}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf delimiter) {//...}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf delimiter) {//...}
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters) {//...}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf... delimiters) {//...}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) {//...}

分隔符可以设置一个,也可设置多个。多个分隔符的实现逻辑是遍历分隔符,找到能将当前消息切割长度最小的分隔符分隔。
服务端测试使用如下案例

// 分隔符号为@ 
channel.pipeline().addLast(new DelimiterBasedFrameDecoder(10, Unpooled.copiedBuffer("@".getBytes())));
channel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

客户端发送如下消息时:

1@2@3

服务器日志如下:

#【消息片段 1】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xc5f14089, L:/127.0.0.1:9998 - R:/127.0.0.1:55915] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 33 31                                           |31              |
+--------+-------------------------------------------------+----------------+
#【消息片段 2】
io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xc5f14089, L:/127.0.0.1:9998 - R:/127.0.0.1:55915] READ: 1B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 32                                              |2               |
+--------+-------------------------------------------------+----------------+
#【消息片段 3保存在内存中等待累加】

2.4 LengthFieldBasedFrameDecoder

一般,消息长度不是固定的;也并非所有的消息都通过分隔符分隔,且使用分隔符分隔消息的策略本身也存在风险,消息内容本身中可能包含分隔符。
可以通过TLV或者LV方式更安全地解码消息,LengthFieldBasedFrameDecoder选择基于LV方式。

LV:L指代长度域,V指代数据域; 通过长度域得到消息内容的长度,从而确定每个消息帧的长度。

使用LengthFieldBasedFrameDecoder的核心在于配置以下4个属性:

// 长度域偏移量(起始位置)
private final int lengthFieldOffset;
// 长度域数据长度
private final int lengthFieldLength;
// 消息长度的修正值
private final int lengthAdjustment;
// 消息内容需要跳过的字节数
private final int initialBytesToStrip;

通过不同的组合方式,可以应对多种场景(消息格式)。
对应提供了如下构造函数:

// maxFrameLength 和 failFast属性的意义与LineBasedFrameDecoder和DelimiterBasedFrameDecoder相同;
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength);
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip);
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast);
public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,int lengthAdjustment, int initialBytesToStrip, boolean failFast);

以下结合LengthFieldBasedFrameDecoder类注释内容,整理出如下几个测试案例.
case1:

BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+
lengthFieldOffset   = 0
lengthFieldLength   = 2
lengthAdjustment    = 0
initialBytesToStrip = 0

分析:
长度域为0x000C,从第0位(第1个字节)开始,长度为2:
lengthFieldOffset=0, lengthFieldLength = 2

0x000C值为12,表示数据域长度为12,而"HELLO, WORLD"字符串长度为12,此时,长度值修正值为0
lengthAdjustment = 0

编码后要求得到的数据内容包括长度域和数据域,因此内容跳过0字节
initialBytesToStrip = 0

case2:

BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
+--------+----------------+      +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
+--------+----------------+      +----------------+
lengthFieldOffset   = 0
lengthFieldLength   = 2
lengthAdjustment    = 0
initialBytesToStrip = 2

分析:
长度域为0x000C,从第0位开始,长度为2
lengthFieldOffset=0, lengthFieldLength = 2

0x000C值为12,等于内容域长度,因此长度修正值为0
lengthAdjustment = 0

编码后要求得到的数据内容仅包括数据域,因此内容跳过2字节(跳过0x000C)
initialBytesToStrip = 2

case3:

BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+
lengthFieldOffset   =  0
lengthFieldLength   =  2
lengthAdjustment    = -2
initialBytesToStrip =  0

分析:
长度域为0x000E,从第0位开始,长度为2
lengthFieldOffset=0, lengthFieldLength = 2

0x000E值为14,比内容域长度大2,因此长度修正值为-2
lengthAdjustment = -2

编码后要求得到的数据内容包括长度域和数据域,因此内容跳过0字节
initialBytesToStrip = 0

case4:

BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
+----------+----------+----------------+      +----------+----------+----------------+
| Header 1 |  Length  | Actual Content |----->| Header 1 |  Length  | Actual Content |
|  0xCAFE  | 0x00000C | "HELLO, WORLD" |      |  0xCAFE  | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+      +----------+----------+----------------+
lengthFieldOffset   = 2
lengthFieldLength   = 3
lengthAdjustment    = 0
initialBytesToStrip = 0

分析:
长度域为0x00000C,从第2位开始,长度为3
lengthFieldOffset=2, lengthFieldLength = 3

0x00000C值为12,等于内容域长度,因此长度修正值为0
lengthAdjustment = 0

编码后要求得到的数据内容包括Header1、长度域和数据域,因此内容跳过0字节
initialBytesToStrip = 0

case5:

BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
+----------+----------+----------------+      +----------+----------+----------------+
|  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
| 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
+----------+----------+----------------+      +----------+----------+----------------+
lengthFieldOffset   = 0
lengthFieldLength   = 3
lengthAdjustment    = 2
initialBytesToStrip = 0

分析:
长度域为0x00000C,从第0位开始,长度为3
lengthFieldOffset=0, lengthFieldLength = 3

0x00000C值为12,等于内容域长度,但不包含Header1,因此长度修正值为2(可以将长度域和内容之间的数据理解成内容域的一部分)
lengthAdjustment = 2

编码后要求得到的数据内容包括Header1、长度域和数据域,因此内容跳过0字节
initialBytesToStrip = 0

case6:

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+
lengthFieldOffset   = 1
lengthFieldLength   = 2
lengthAdjustment    = 1
initialBytesToStrip = 3

分析:
长度域为0x000C,从第1位开始,长度为2
lengthFieldOffset=1, lengthFieldLength = 2

0x0000C值为12,等于内容域长度,但不包含HDR2,因此长度修正值为1(HDR2长度)
lengthAdjustment = 1

编码后要求得到的数据内容包括Header2、数据域,因此内容跳过3字节(HDR1和长度域)
initialBytesToStrip = 3

case7:

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+
lengthFieldOffset   =  1
lengthFieldLength   =  2
lengthAdjustment    = -3
initialBytesToStrip =  3

分析:
长度域为0x0010,从第1位开始,长度为2
lengthFieldOffset=1, lengthFieldLength = 2

0x0010值为16, 大于内容域长度,包含了HDR1+长度域+HDR2+内容域,实际应为HDR2+内容域,因此长度修正值为-3(HDR1+长度域)
lengthAdjustment = -3

编码后要求得到的数据内容包括Header2、数据域,因此内容跳过3字节(HDR1和长度域)
initialBytesToStrip = 3

3.实现原理

2章节介绍的解码器有一个共同的父类ByteToMessageDecoder,核心逻辑在ByteToMessageDecoder中,四个解码器针对不同使用场景进行了定制化扩展(实现decode方法)。
在这里插入图片描述
本章将结合ByteToMessageDecoder源码对实现原理进行介绍,之后基于ByteToMessageDecoder对FixedLengthFrameDecoder(最简单、清晰易懂)进行介绍。

3.1 ByteToMessageDecoder

ByteToMessageDecoder的核心逻辑是未读数据累加机制,以及通过预留decode给子类实现定制功能,是一个模板抽象类。ByteToMessageDecoder本身是一个Inbound事件的ChannelHandler, 可以处理可读事件。
在这里插入图片描述
当消息可读时,消息进入ByteToMessageDecoder的channelRead方法,对数据进行累加处理,然后调用子类的decode方法(decode接口声明如下所示):

// in累加的数据流, out解码结果列表
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

子类可以从数据流中取出一部分解码出消息,也可以不取;此时未被取出的数据流将保存在内存中。下次数据流到达时,累加器会从内存中取出的未读部分并进行数据累加,然后继续调用子类的decode方法。以下将多这一部分逻辑结合源码进行详细介绍。

3.1.1 累加器和累加数据

// 累加器
private Cumulator cumulator = MERGE_CUMULATOR;

// 累加属性,未被读取的数据
ByteBuf cumulation;

// 提供了一个设值方法,自定义累加器(一般直接使用默认的MERGE_CUMULATOR即可)
public void setCumulator(Cumulator cumulator) {
    this.cumulator = ObjectUtil.checkNotNull(cumulator, "cumulator");
}

Cumulator和MERGE_CUMULATOR定义如下:

// Cumulator仅定义了cumulate一个方法
public interface Cumulator {
    ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);
}

// cumulate实现将in的数据读取到cumulation中并返回,必要时对cumulation进行扩容
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        // cumulation没有可读数据时,将in返回,并回收cumulation
        if (!cumulation.isReadable() && in.isContiguous()) {
            cumulation.release();
            return in;
        }
        try {
            final int required = in.readableBytes();
            // cumulation装不下in的数据时,对cumulation进行扩容
            if (required > cumulation.maxWritableBytes() || (required > cumulation.maxFastWritableBytes() && cumulation.refCnt() > 1) || cumulation.isReadOnly()) {
                return expandCumulation(alloc, cumulation, in);
            }
            // 从in的可取位置读取in的数据至cumulation
            cumulation.writeBytes(in, in.readerIndex(), required);
            // 将in设置为已读完
            in.readerIndex(in.writerIndex());
            return cumulation;
        } finally {
            in.release();
        }
    }
};

逻辑较为清晰: cumulate实现将in的数据读取到cumulation中并返回,必要时对cumulation进行扩容。
扩容时,新创建一个ByteBuf对象,将in数据和oldCumulation读取到新的cumulation并返回。

3.1.2 解码过程

消息可读时,进入解码器的channelRead方法:

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
	if (msg instanceof ByteBuf) {
		// 1.构造out列表临时存放解码后的对象
		CodecOutputList out = CodecOutputList.newInstance();
		try {
			// 2.使用cumulator将cumulation数据与msg数据整合成新的cumulation
			first = cumulation == null;
			cumulation = cumulator.cumulate(ctx.alloc(), first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
			// 3.调用callDecode从cumulation数据中解码出对象,保存到out列表中
			callDecode(ctx, cumulation, out);
		} catch (DecoderException e) {
			throw e;
		} catch (Exception e) {
			throw new DecoderException(e);
		} finally {
			//4.解码后处理工作
			try {
				// 如果cumulation数据已被读完,重置cumulation
				if (cumulation != null && !cumulation.isReadable()) {
					numReads = 0;
					cumulation.release();
					cumulation = null;
				} else if (++numReads >= discardAfterReads) {
					// 重复多次(默认16)为解析完数据,丢弃这部分数据,防止OOM
					numReads = 0;
					discardSomeReadBytes();
				}

				int size = out.size();
				firedChannelRead |= out.insertSinceRecycled();
				// 遍历out列表,对所有元素向pipeline触发channelRead事件
				fireChannelRead(ctx, out, size);
			} finally {
				// 回收再利用out列表
				out.recycle();
			}
		}
	} else {
		ctx.fireChannelRead(msg);
	}
}

最外层有个判断逻辑,只有消息类型是ByteBuf才会被处理,否则将消息向后传递。
整体上可以分为以下几个步骤:
[1] 构造out列表临时存放解码后的对象;
[2] 使用cumulator将cumulation数据与msg数据整合成新的cumulation;
[3] 调用callDecode从cumulation数据中解码出对象,保存到out列表中;
[4] 解码后的处理工作, 包括:将解码的消息触发为channelRead事件、cumulation处理、out的回收再利用;

其中第三步是核心逻辑:

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
	try {
		// 循环从in中读取数据,直到读完或者被break中断
		while (in.isReadable()) {
			// 如果out中已积累了解码后的元素,先触发并清空out列表
			int outSize = out.size();
			if (outSize > 0) {
				fireChannelRead(ctx, out, outSize);
				out.clear();
				if (ctx.isRemoved()) {
					break;
				}
				outSize = 0;
			}
			int oldInputLength = in.readableBytes();
			decodeRemovalReentryProtection(ctx, in, out);
			// 通道已被移除,不再读取数据
			if (ctx.isRemoved()) {
				break;
			}

			// 此次循环没有解码出新的数据
			if (outSize == out.size()) {
				// 数据源没变
				if (oldInputLength == in.readableBytes()) {
					// 中断提取,防止陷入死循环
					break;
				} else {
					continue;
				}
			}
			// 数据源没被消耗,却读取了新的数据,抛出异常
			if (oldInputLength == in.readableBytes()) {
				throw new DecoderException(StringUtil.simpleClassName(getClass()) + ".decode() did not read anything but decoded a message.");
			}

			//每次消息达到,仅解码一次, 默认为false,一直提取
			if (isSingleDecode()) {
				break;
			}
		}
	} catch (DecoderException e) {
		throw e;
	} catch (Exception cause) {
		throw new DecoderException(cause);
	}
}

对异常场景跳出循环或者抛出异常,重点在decodeRemovalReentryProtection方法:

final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
	decodeState = STATE_CALLING_CHILD_DECODE;
	try {
		// 调用子类的decode方法
		decode(ctx, in, out);
	} finally {
		// 如果通道正在被移除,这里触发解码出的消息
		boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
		decodeState = STATE_INIT;
		if (removePending) {
			fireChannelRead(ctx, out, out.size());
			out.clear();
			handlerRemoved(ctx);
		}
	}
}

所有继承ByteToMessageDecoder的解码器必须要实现decode方法。

3.2 FixedLengthFrameDecoder

FixedLengthFrameDecoder源码如下:

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    private final int frameLength;

    public FixedLengthFrameDecoder(int frameLength) {
        // 正数
        checkPositive(frameLength, "frameLength");
        this.frameLength = frameLength;
    }

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 调用decode解码,如果结果不为空,添加到out列表
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        // 可读取的数据长度小于frameLength,不读取
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            // 可读取的数据长度大于或等于frameLength,从ByteBuf中读取frameLength长度的数据
            return in.readRetainedSlice(frameLength);
        }
    }
}

FixedLengthFrameDecoder中有一个frameLength属性,用于确定每次读取的数据长度。
decode方法逻辑较为简单,当到达的可读数据长度大于等于frameLength时,从中读取frameLength长度的数据,并转为ByteBuf对象返回,未读取的字段保存在内存中累加;可读数据长度小于frameLength时,不进行读取。
由此,数据经过FixedLengthFrameDecoder后,将会被拼接或裁剪为一个个长度为frameLength的ByteBuf对象,并沿着Pipeline向后传递。

4.扩展

可基于上述内容,设计一个消息解码器,实现解码TLV格式的消息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188126.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

吉他弹唱打谱软件哪个好用 吉他弹唱制谱教程

吉他这门乐器一直受到大众的欢迎&#xff0c;究其原因&#xff0c;还是因为其成本低廉、易上手的特性。但是吉他是一个入门容易精通难的乐器&#xff0c;想要成为一个资深的吉他玩家&#xff0c;那么就少不了用到一些吉他弹唱打谱软件。今天我们就来说一说吉他弹唱打谱软件哪个…

学习 CSS 新的属性 conic-gradient 实现环形进度条

我们在工作中用到环形进度条的时候&#xff0c;一般都是使用组件库提供的&#xff0c;那么你有没有想过这是怎么实现的呢&#xff1f; <divclass"progress"style"--progress: 80%; --last: 20%"data-progress"80%"></div><style …

【宽搜】2. leetcode 102 二叉树的层序遍历

题目描述 题目链接&#xff1a;二叉树的层序遍历 根据上一篇文章的模板可以直接写代码&#xff0c;需要改变的就是将N叉树的child改为二叉树的left和right。 代码 class Solution { public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector&…

k8s的学习和使用

为什么用k8s&#xff0c;不用docker&#xff1f; k8s更适合复杂的微服务架构和大规模的容器应用。 Pods(Pod) Pod是k8s最小可部署单元&#xff0c;他包含一个或多个相关容器。这些容器共享网络命名空间和存储卷&#xff0c;他们通常协同工作来构成一个应用程序。 Serv…

开启AI新篇章:探索GPT-4与大模型!订阅方案!简单支付!

开启AI新篇章&#xff1a;探索GPT-4的无限可能 随着人工智能技术的飞速发展&#xff0c;我们正处于一个前所未有的变革时代。作为人工智能领域的领导者&#xff0c;OpenAI 推出的GPT-4&#xff0c;以其卓越的自然语言处理能力和强大的计算潜力&#xff0c;引发了行业内外的广泛…

深入浅出MySQL

深入浅出MySQL 以下内容参考自 《MySQL是怎样运行的&#xff1a;从根儿上理解MySQL》一书&#xff0c;强烈推荐 存储引擎 对于不同的表可以设置不同的存储引擎 CREATE TABLE tableName (xxxx ) ENGINE 引擎名称; # 修改 ALTER TABLE tableName ENGINE xxx; 编码格式 my…

(C语言贪吃蛇)10.贪吃蛇向右自行行走

目录 前言 本节内容 实现效果 修改后的代码 其他封装函数&#xff1a; 运行效果 总结 前言 我们上节讲解了关于贪吃蛇撞墙然后死翘翘重新初始化蛇身的操作&#xff0c;主要是关于程序初始化释放内存的操作&#xff0c;不理解的再去看看&#x1f618;(贪吃蛇撞墙找死详解)。…

SpringBoot技术栈:构建高效古典舞交流平台

第二章 相关技术介绍 2.1Java技术 Java是一种非常常用的编程语言&#xff0c;在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中&#xff0c;Java的身影无处不在&#xff0c;并且拥有旺盛的生命力。Java的跨平台能力十分强大&#xff0c;只需一次编译&#xf…

openpnp - 吸嘴校正失败的opencv参数分析

文章目录 openpnp - 吸嘴校正失败的opencv参数分析概述笔记阶段验证 - N2吸嘴校验完NT1NT2 阶段验证 - 底部相机高级校验完NT1NT2 参数比对保存 “阶段验证 - N2吸嘴校验完” 的NT1/NT2图像重建参数检测环境NT1ok的3个参数值NT1err的3个参数值NT2ok的3个参数值NT2err的3个参数值…

如何入门运动规划算法? 50篇教程教你手把手推导公式! 实现代码!

经常听到有想入门规划算法的同学说: 各路教程不成体系, 不知从何学起? 网上的规划算法教程资料确实很多. 但是东一篇frenet, 西一篇QP优化, 大部分都是各路大佬写给自己看的学习笔记, 杂乱无章不成体系. 有没有给小白看的, 完整成体系的运动规划算法教程呢? 穷学生囊中羞…

Redis入门第四步:Redis发布与订阅

欢迎继续跟随《Redis新手指南&#xff1a;从入门到精通》专栏的步伐&#xff01;在本文中&#xff0c;我们将深入探讨Redis的发布与订阅&#xff08;Pub/Sub&#xff09;模式。这是一种强大的消息传递机制&#xff0c;适用于各种实时通信场景&#xff0c;如聊天应用、实时通知和…

反调试—1

IsDebuggerPresent() CheckRemoteDebuggerPresent() 其内部实际调用NtQueryInformationProcess() bool _stdcall ThreadCall() {while (true){BOOL pbDebuggerPresent FALSE;CheckRemoteDebuggerPresent(GetCurrentProcess(), &pbDebuggerPresent);if (pbDebuggerPres…

Redis数据库与GO(一):安装,string,hash

安装包地址&#xff1a;https://github.com/tporadowski/redis/releases 建议下载zip版本&#xff0c;解压即可使用。解压后&#xff0c;依次打开目录下的redis-server.exe和redis-cli.exe&#xff0c;redis-cli.exe用于输入指令。 一、基本结构 如图&#xff0c;redis对外有个…

Netgear-WN604 downloadFile.php 信息泄露复现(CVE-2024-6646)

0x01 产品描述&#xff1a; NETGEAR WN604是一款功能强大的双频AC1200无线路由器,非常适合中大型家庭和企业使用。它支持最新的802.11ac无线标准,能提供高达1200Mbps的无线传输速度。路由器具备千兆有线网口和3个100Mbps有线网口,可满足有线和无线设备的接入需求。此外,它还内置…

详解JavaScript中函数式编程

函数式编程 JS并非函数式编程语言&#xff0c;但可以应用函数式编程技术&#xff0c;这种风格很多语言都用&#xff0c;例如Java. 使用函数处理数组 假设有一个数组&#xff0c;数组元素都是数字&#xff0c;我们想要计算这些元素的平均值和标准差。使用非函数式编程风格的话…

vue基于springboot的促销商城购物管理系统带抽奖dm8o6

目录 功能介绍系统实现截图开发工具合计数介绍技术介绍核心代码部分展示可行性分析springboot文件解析详细视频演示源码获取 功能介绍 用户功能 注册登录&#xff1a;允许用户创建账户和登录系统。 个人中心&#xff1a;用户可以查看和编辑个人信息&#xff0c;密码、联系方式…

RabbitMQ篇(死信交换机)

目录 一、简介 二、TTL过期时间 三、应用场景 一、简介 当一个队列中的消息满足下列情况之一时&#xff0c;可以成为死信&#xff08;dead letter&#xff09; 消费者使用basic.reject或者basic.nack声明消费失败&#xff0c;并且消息的requeue参数设置为false消息是一个过…

银河麒麟V10 SP1如何进入救援模式?

银河麒麟V10 SP1如何进入救援模式&#xff1f; 1、准备工作2、进入BIOS/UEFI进入救援模式注意事项 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在使用银河麒麟高级服务器操作系统V10 SP1时&#xff0c;如果遇到系统无法正常启动或需要进…

Blazor流程编排的艺术:深入Z.Blazor.Diagrams库的使用与实践

为现代网页应用开发提供动力的其中一个重要方面就是前端框架的强大功能与灵活性。而在.NET生态中&#xff0c;Blazor以其独特的工作方式和优势逐渐获得了开发者们的青睐。今天&#xff0c;在这篇文章中&#xff0c;我将带你深入探索一个基于Blazor的优秀库——Z.Blazor.Diagram…

深入理解Linux内核网络(一):内核接收数据包的过程

在应用层执行read调用后就能很方便地接收到来自网络的另一端发送过来的数据&#xff0c;其实在这一行代码下隐藏着非常多的内核组件细节工作。在本节中&#xff0c;将详细讲解数据包如何从内核到应用层&#xff0c;以intel igb网卡为例。 部分内容来源于 《深入理解Linux网络》…