这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战
在上一篇博客Netty编程(七)—— 粘包半包(一) - 掘金 (juejin.cn)中介绍了一下什么是粘包和半包,这篇博客将继续介绍Netty如何处理粘包半包问题。
短链接
短链接的思路是客户端每次向服务器发送数据以后,就与服务器断开连接,此时的消息边界为连接建立到连接断开。这时便无需使用滑动窗口等技术来缓冲数据,则不会发生粘包现象。但如果一次性数据发送过多,接收方无法一次性容纳所有数据,还是会发生半包现象,所以短链接无法解决半包现象,短链接这种方式人为地设置了消息边界,不过很明显这种方法效率低,而它能解决粘包问题但不能解决半包问题。
客户端代码需要修改channelActive方法,每次发完数据后就关闭连接:
1 | java复制代码public void channelActive(ChannelHandlerContext ctx) throws Exception { |
客户端使用短链接的方式向服务端连续发送10次消息,运行结果如下,不会出现粘包问题:
定长解码器
定长解码器的思路是客户端和服务器约定一个最大长度,保证客户端每次发送的数据长度都不会大于该长度。若发送数据长度不足则需要补齐至该长度
服务器接收数据时,将接收到的数据按照约定的最大长度进行拆分,即使发送过程中产生了粘包,也可以通过定长解码器将数据正确地进行拆分。服务端需要用到FixedLengthFrameDecoder
对数据进行定长解码,具体使用方法如下
ch.pipeline().addLast(new FixedLengthFrameDecoder(16))
客户端代码需要保证每次发送的数据长度为约定大小,客户端发送数据的代码如下
1 | java复制代码// 约定最大长度为16 |
服务器中需要使用FixedLengthFrameDecoder
对粘包数据进行拆分,该handler需要添加在LoggingHandler
之前,保证数据被打印时已被拆分
1 | java复制代码// 通过定长解码器对粘包数据进行拆分 |
运行结果如下:
行解码器
行解码器的是通过分隔符对数据进行拆分来解决粘包半包问题的,可以通过LineBasedFrameDecoder(int maxLength)
来拆分以换行符(\n)为分隔符的数据,也可以通过DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters)
来指定通过什么分隔符来拆分数据(可以传入多个分隔符), 两种解码器都需要传入数据的最大长度,若超出最大长度,会抛出TooLongFrameException
异常
下面的示例代码以换行符 \n 为分隔符,客户端代码需要在发送数据的结尾加上换行符\n
作为为分隔符
1 | java复制代码// 约定最大长度为 64 |
服务器代码需要使用DelimiterBasedFrameDecoder
对数据进行拆分,该handler需要添加在LoggingHandler
之前,保证数据被打印时已被拆分
1 | java复制代码// 通过行解码器对粘包数据进行拆分,以 \n 为分隔符 |
下面示例代码以自定义分隔符 \c
为分隔符:
客户端代码
1 | java复制代码... |
服务器代码在使用DelimiterBasedFrameDecoder时需要指定分隔符
1 | java复制代码// 将分隔符放入ByteBuf中 |
运行结果:
长度字段解码器
在传送数据时可以在数据中添加一个用于表示有用数据长度的字段,在解码时读取出这个用于表明长度的字段,同时读取其他相关参数,即可知道最终需要的数据是什么样子的
LengthFieldBasedFrameDecoder
解码器可以提供更为丰富的拆分方法,其构造方法有五个参数
1 | java复制代码public LengthFieldBasedFrameDecoder( |
参数解析
- maxFrameLength 数据最大长度
- 表示数据的最大长度(包括附加信息、长度标识等内容)
- lengthFieldOffset 数据长度标识的起始偏移量
- 用于指明数据第几个字节开始是用于标识有用字节长度的,因为前面可能还有其他附加信息
- lengthFieldLength 数据长度标识所占字节数(用于指明有用数据长度的字节数)
- 数据中用于表示有用数据长度的标识所占的字节数,==注意不是内容长度,而是长度字段的长度==
- lengthAdjustment 长度表示与有用数据的偏移量
- 用于指明数据长度标识和有用数据之间的距离,因为两者之间还可能有附加信息
- initialBytesToStrip 数据读取起点
- 读取起点,不读取 0 ~ initialBytesToStrip 之间的数据
参数图解
例子
1 | ini复制代码lengthFieldOffset = 0 |
从0开始即为长度标识,长度标识长度为2个字节
0x000C 即为后面 HELLO, WORLD
的长度
1 | ini复制代码lengthFieldOffset = 0 |
从0开始即为长度标识,长度标识长度为2个字节,读取时从第二个字节开始读取(此处即跳过长度标识),这里可以理解成解码时去掉前两个字节
因为跳过了用于表示长度的2个字节,所以此处直接读取HELLO, WORLD
1 | ini复制代码lengthFieldOffset = 2 (= the length of Header 1) |
长度标识前面还有2个字节的其他内容(0xCAFE),第三个字节开始才是长度标识,长度表示长度为3个字节(0x00000C)
Header1中有附加信息,读取长度标识时需要跳过这些附加信息来获取长度
1 | ini复制代码lengthFieldOffset = 0 |
从0开始即为长度标识,长度标识长度为3个字节,长度标识之后还有2个字节的其他内容(0xCAFE)
长度标识(0x00000C)表示的是从其后lengthAdjustment(2个字节)开始的数据的长度,即HELLO, WORLD
,不包括0xCAFE
1 | ini复制代码lengthFieldOffset = 1 (= the length of HDR1) |
长度标识前面有1个字节的其他内容,后面也有1个字节的其他内容,读取时从长度标识之后3个字节处开始读取,即读取 0xFE HELLO, WORLD
使用
通过 EmbeddedChannel 对 handler 进行测试
1 | java复制代码public class EncoderStudy { |
运行结果
本文转载自: 掘金