TCP粘包拆包问题解决
什么TCP粘包和拆包问题
假设客户端向服务端连续发送了两个数据包,分别用ABC和DEF来表示,那么服务端收到的数据可以分为以下三种情况:
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象。
第二种情况,接收端只收到一个数据包,这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。
第三种情况,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。
为什么会发生TCP粘包和拆包
发生TCP粘包的原因:
- 发送方写入的数据小于套接字缓冲区大小
- 接收方读取套接字缓冲区数据不及时
发生TCP拆包的原因:
- 发送方写入的数据大于套接字缓冲器大小
- 发送方写入的数据大于MTU(Maximum Transmission Unit,最大传输单元),必须拆包
一个TCP报文最大能传输65536个字节,也就是16Kb。
根本原因:TCP是流式协议,数据无边界。
怎么解决
解决问题的根本手段就是找出消息的边界。
netty提供了以下三种方式解决TCP粘包和拆包问题:
- DelimiterBasedFrameDecoder:分隔符。
- LineBasedFrameDecoder:结束符\n。
- FixedLengthFrameDecoder:固定长度。
- LengthFieldBasedFrameDecoder+LengthFieldPrepender:自定义消息长度
DelimiterBasedFrameDecoder分隔符
DelimiterBasedFrameDecoder是通过发送方每条报文结束都添加特殊符号(例如_)对报文进行切割。
发送方需要自行编码,添加分隔符,编码如下:
ctx.writeAndFlush(Unpooled.copiedBuffer(("hello" + i + "$_").getBytes())); // 以$_结尾
接收方的解码如下:
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1 << 10, Unpooled.copiedBuffer("$_".getBytes())));
缺点:发送的内容本身可能会出现分隔符,需要对发送的内容进行扫描并转义,接收到的内容也要进行反转义。
DelimiterBasedFrameDecoder提供了多个构造方法,最终调用的都是以下构造方法:
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters)
参数说明:
- maxLength:表示一行最大的长度,如果超过这个长度依然没有检测到分隔符,将会抛出TooLongFrameException。
- failFast:与maxLength联合使用,表示超过maxLength后,抛出TooLongFrameException的时机。如果为true,则超出maxLength后立即抛出TooLongFrameException,不继续进行解码;如果为false,则等到完整的消息被解码后,再抛出TooLongFrameException异常。
- stripDelimiter:解码后的消息是否去除分隔符。
- delimiters:分隔符。我们需要先将分割符,写入到ByteBuf中,然后当做参数传入。
LineBasedFrameDecoder结束符\n
LineBasedFrameDecoder可以当成是一种特殊的DelimiterBasedFrameDecoder,其分隔符为\n或者\r\n。
发送方的编码如下:
ctx.writeAndFlush(Unpooled.copiedBuffer(("hello" + i + "\n").getBytes())); // 以\n结尾
接收方的解码如下:
ch.pipeline().addLast(new LineBasedFrameDecoder(1 << 10));
FixedLengthFrameDecoder固定长度
FixedLengthFrameDecoder是通过发送方固定每条报文长度均为n个字节,接收方也通过n个字节长度切分报文。
发送方需要自行补齐长度,编码如下:
ctx.writeAndFlush(Unpooled.copiedBuffer(("hello" + i + " ").getBytes())); // 补齐长度为16
接收方的解码如下:
ch.pipeline().addLast(new FixedLengthFrameDecoder(16));
缺点:如果发送的内容比较小,需要补齐长度,空间浪费,如果要发送的内容突然变大,需要调整发送方和接收方的长度。
LengthFieldBasedFrameDecoder+LengthFieldPrepender自定义消息长度
LengthFieldPrepender对自定义消息长度进行编码。
LengthFieldBasedFrameDecoder对自定义消息长度进行解码。
LengthFieldBasedFrameDecoder的构造方法如下:
public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
参数说明:
- byteOrder:数据存储采用大端模式或小端模式
- maxFrameLength:发送的数据帧最大长度
- lengthFieldOffset: 发送的字节数组中从下标lengthFieldOffset开始存放的是报文数据的长度。
- lengthFieldLength: 在发送的字节数组中,报文数据的长度占几位,也就是字节数组bytes[lengthFieldOffset, lengthFieldOffset+lengthFieldLength]存放的是报文数据的长度
- lengthAdjustment: 长度域的偏移量矫正。如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长-长度域的值-长度域偏移 – 长度域长。
- initialBytesToStrip:接收到的发送数据包,去除前initialBytesToStrip位
- failFast:为true表示读取到长度域超过maxFrameLength,就抛出一个TooLongFrameException。为false表示只有真正读取完长度域的值表示的字节之后,才会抛出TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。
场景一
- lengthFieldOffset=0
- lengthFieldLength=2
- lengthAdjustment=0
- initialBytesToStrip=0
场景二
- lengthFieldOffset=0
- lengthFieldLength=2
- lengthAdjustment=0
- initialBytesToStrip=2
场景三
- lengthFieldOffset=0
- lengthFieldLength=2
- lengthAdjustment=整个包长(14)-长度域的值(14)-长度域偏移(0)-长度域长(2)=-2
- initialBytesToStrip=0
场景四
- lengthFieldOffset=2
- lengthFieldLength=3
- lengthAdjustment=0
- initialBytesToStrip=0
场景五
- lengthFieldOffset=0
- lengthFieldLength=3
- lengthAdjustment=2
- initialBytesToStrip=0
场景六
- lengthFieldOffset=1
- lengthFieldLength=2
- lengthAdjustment=1
- initialBytesToStrip=3
场景七
- lengthFieldOffset=1
- lengthFieldLength=2
- lengthAdjustment=整个包长(16)-长度域的值(16)-长度域偏移(1)-长度域长(2)=-3
- initialBytesToStrip=3
更多精彩内容关注本人公众号:架构师升级之路