TCP粘包和半包问题
我们已知TCP全双工的流传输协议,且TCP会将数据写入内核的缓冲区中,再由应用程序读取出数据。在异步传输的场景下,应用程序无法保证读取出的数据是期望的数据内容。
比如我们想要的数据是ABC123,那么应用程序从缓冲区读取出可能是ABC12 ,这时3还没被写入缓冲区;也可能是23ABC123,这是上次23没有被读取走,跟下一条数据一起被读取出来;
当应用程序读取的数据多于正常数据,23ABC123,这就是粘包;
当应用程序读取的数据少于正常数据,ABC12,这就是半包;
粘包和半包都会导致数据格式错误,导致应用程序出错!
解决思路
- 约定消息定长,应用程序按照固定长度,去缓冲区读取
- 消息结束符号:包括最常用的字符串结束符\n 和其他自定义符号,适用于字符串文本传输;
- 自定义消息头部和尾部:通过消息头和消息尾标记一条消息的开始和结束,并在消息头中定义消息体的长度
Netty定长解码器 FixedLengthFrameDecoder
根据类名,翻译过来就能知道这是一个基于固定长度的解码器,什么意思呢?就是在初始化这个解码器时,指定一个 int 类型的数值:frameLength,后面在解码时,每当读到 frameLength 长度的字节时,就解码出一个数据对象。例如:当发送方发送了四次数据,分别是 A、BC、DEFG、HI,一共 9 个字节,如果我们指定解码器的固定长度 frameLength = 3,那么就表示每 3 个字节解一次码,那么解码出来的结果结果就是:ABC、DEF、GHI。
// 入参 maxLength:最大长度
FixedLengthFrameDecoder decoder = new FixedLengthFrameDecoder (1024) ;
Netty换行符解码器 LineBaseFrameDecoder
回车换行解码器,如果消息是以字符串文本形式且以回车换行符(\r\n 或者 \n)作为消息结束的标识,则可以使用该解码器进行解码;
原理:
- 依次遍历ByteBuf中的可读字节,判断是否有 \r\n ,如果则有标记为结束位置,从可读起始索引位到标记结束位为一行数据;
- 支持配置单行长度,若读取至最大长度仍未发现换行符,则抛出异常,并丢弃ByteBuf的数据,防止积压导致内存溢出
// 入参 maxLength 最大长度
LineBasedFrameDecoder decoder = new LineBasedFrameDecoder( 100 ) ;
Netty分隔符解码器
按照特定分割符进行解码的解码器,回车换行符解码器实际就是一种特殊的分隔符解码器;
分隔符Delimiter是以ByteBuf作为入参
// ByteBuf 分隔符
ByteBuf delimiter = UnPooled.copiedBuffer( "XXXX".getBytes() );
/ **
* maxLength : 最大长度
* delimiter :分隔符
*/
DelimiterBasedFrameDecoder decoder = new DelimiterBaseFrameDecoder(1000 , delimiter);
Netty通用字节流消息头解码器
按照私有协议中的消息头作分割,并取消息头中的消息体长度字段,分割出消息体的解码器,是灵活度最高的解码器;
它有4个,重要参数:
- lengthFieldOffset: 长度属性之前的偏移量
- lengthFieldLength:长度属性自身的长度
- lengthAdjustment: 从长度属性到消息体之间的偏移量
- initialBytesToStrip: 分割出消息体之前需要忽略的字节数量
// 1024是最大长度 maxLength
LengthFieldBasedFrameDecoder decoder = new LengthFieldBasedFrameDecoder (1024,2,4,4,6);