1.前言
对Netty中的粘包半包问题还不了解的童鞋,可以看看之前的
Netty通信中的粘包半包问题(一到五)系列,以免产生不适如果你对Netty中的粘包半包问题已经熟悉了,可以直接阅读本文,
本文主要介绍了消息头+消息体去解决粘包半包问题。
2.LengthFieldBasedFrameDecoder
LengthFieldBasedFrameDecoder
是Netty提供给我们一种消息头+消息体的一种解决思路,这种方式使用起来比较复杂,因为该类有几个核心参数需要进行设置,否则将会影响数据报文的正常读取.
- maxFrameLength:表示的是包的最大长度
- lengthFieldOffset:指的是长度域的偏移量,表示跳过指定个数字节之后的才是长度域
- lengthFieldLength:记录该帧数据长度的字段,也就是长度域本身的长度
- lengthAdjustment:长度的一个修正值,可正可负,Netty在读取到数据包的长度值N后,认为接下来的N个字节都是需要
- initialBytesToStrip:从数据帧中跳过的字节数,表示得到一个完整的数据包之后,扔掉这个数据包中多少字节数,才是后续业务实际需要的业务数据
- failFast:如果为true,则表示读取到长度域,它的值超过maxFrameLength,就跑出一个TooLongFrameException,如果为false只有当真正读取完长度域的值表示的字节之后,才会抛出TooLongFrameException,默认情况下为true,一般不要修改,否则可能造成内存溢出
接下来我们将通过几个例子来讲解这些参数的使用,在分析这些例子之前,建议先把上面的参数解释复制下来,用两个窗口来分析
3.示例一
数据包大小:14B = 长度域为2B + “HELLO, WORLD”(单词HELLO + 一个逗号 + 一个空格 + 一个单词WORLD)
长度域的值为12B(0x000C),希望解码后保持一样,参数设置应为如下:
- lengthFieldOffset = 0,因为在当前的例子中,长度域前面没有任何其他数据,所以这里不需要设置长度域的偏移量
- lengthFieldLength = 2,十六进制的0x000C,根据一个字节表示两位十六进制,所以0x000C需要两个字节来表示
- lengthAdjustment =0 ,无需调整,在当前例子中需要全部读取,不需要额外增加和减少
- initialBytesToStrip = 0 解码过程中,也不需要丢弃任何数据
4 示例二
数据包大小: 14B = 长度域2B + “HELLO, WORLD”
长度域的值为12B(0x000C),解码后,希望丢弃长度域2B字段,所以下方参数设置应为:
- lengthFieldOffset=0 长度域前面没有其他数据,不需要设置偏移量
- lengthFieldLength = 2 0x000C十六进制占用两个字节
- lengthAdjustment = 0 长度值也不需要调整,因为长度域本身的值和报文长度的值是相等的
- initialBytesToStrip = 2 解码过程中,需要丢弃2个字节的数据
5.示例三
数据包大小: 14B = 长度域2B + “HELLO, WORLD”,长度域的值为14(0x000E),包含了长度域本身的长度,希望解码后保持一样,参数设置如下:
- lengthFieldOffset = 0,长度域偏移量前面没有任何数据,不需要设置
- lengthFieldLength = 2 0x000E,根据一个字节等于两位十六进制的换算,所以占2个字节
- lengthAdjustment = -2, 因为长度域为14(0x000E),而报文内容为12,为了防止读取报文超出报文本体,将和长度字段一起读取进来,需要告诉Netty,实际读取的报文长度比长度域中的要少2(12-14=-2)
- initialBytesToStrip = 0,解码过程中没有丢弃任何数据
6 示例四
在长度域前添加2个字节的Header,长度域的值(0x00000C)=12,总数据包长度:17 = Header(2B) + 长度域(3B) + “HELLO, WORLD”
长度域的值为12B(0x00000C),编码解码后,长度保持一直,所以参数设置应为
- lengthFieldOffset = 2,由于长度域前面有2个字节的Header,所以需要跳过两个字节才是长度域
- lengthFieldLength = 3, 0x00000C,根据1个字节等于两位十六进制数
- lengthAdjustment = 0 无需修正长度,因为长度域本身的值和报文长度的值是相等的
- initialBytesToStrip = 0 在解码过程没有丢弃任何数据
7 示例五
带有两个Header.HDR1 丢弃,长度域丢弃,只剩下第二个Header和有效包体,这种协议中,一般HDR1 可以表示magicNumber,表示应用只接受该magicNumber开头的二进制数据,RPC里面用的比较多,总数据包长度:16 = HDR1(1B) + 长度域(2B) + HDR2(1B) + “HELLO, WORLD”
长度域的值为12B(0x000C),参数应设置为
- lengthFieldOffset = 1 表示长度域前面需要跳过HDR1的长度,才是长度域的数据
- lengthFieldLength = 2 根据十六进制与字节的换算单位,两个十六进制数等于1个字节,所以0x000C占用2个字节
- lengthAdjustment = 1 因为长度域的值为12,报文内容为12,到那时我们需要把HDR2的值一起读取进来,需要告诉Netty,实际读取的报文内容比长度域中的要多1(12 +1 =13)
- initialBytesToStrip = 3 丢弃了HDR1和长度域(1 + 2 = 3)
8. 示例6
带有两个Header,HDR1丢弃,长度域也丢弃,只剩下第二个header和有效包体,总数据包长度:16 = HDR(1B) + 长度域(2B) + HDR2(1B) + “HELLO, WORLD”
参数设置如下:
- lengthFieldOffset = 1 长度域的偏移量为1,需要跳过HDR1才能读取到长度域
- lengthFieldLength = 2 根据十六进制与字节的换算单位,两个十六进制数等于1个字节,所以0x0010 占用2个字节
- lengthAdjustment = -3 因为长度域为16,需要告诉Netty,实际读取的报文内容长度比实际需要读取的报文长度要少3(13 - 16 = -3),因为需要读取到HDR2(1B) + “HELLO, WORLD”(12B)
- initialBytesToStrip = 3 丢弃了HDR1和长度域字段
9.总结
LengthFieldBasedFrameDecoder
中的参数相比之前的特殊字符、固定长度、特殊分隔符的解决方案复杂度上升了不少,但是这却是最优的解决方案,具体业务还要具体分析,有的业务场景可能不需要消息头+消息体这种方案,毕竟这种方案稍有不慎就会出错,参数设置也比较多,需要每个参数都熟悉才能开发业务,相信以后可能会有更好的方案