netty 拆包、粘包解决之道
一、什么是拆包、粘包?
拆包是指接收方接收到一个不完整的数据包,粘包则指接收一次接收到多个数据包
二、拆包、粘包的产生的原因
TCP是一个面向流的协议。TCP是传输层协议,其并不清楚应用层数据的具体含义。TCP使用滑动窗口进行流量控制,所以在业务认为是一个完整的包,很有可能会被TCP拆分为多个数据包进行发送。也有可能会将多个小的包组装成一个大的包发送。
三、如何解决拆包、粘包问题
可以通过认为的进行数据包边界的制定和解析。几种常见的思路如下:
-
1、业务数据使用定长报文:在这种情况下,接收方只要读取到足够的数据长度就认为是已经获取到一个业务数据报文
-
2、使用特殊的消息分隔符:此种情况下,接收方检查每一个收到的字节,一旦某段字节序列符合分隔符特征,接收方就分离出这段数据报文
-
3、使用报文头+报文体:此种此情况下,一般来说报文头是一个定长或者不定长的数据,其中会包含报文体的长度或者总报文的长度。这样,接收方应用程序在读取报文头之后就可以直到整体的报文长度后者剩余的报文体长度。按照长度,就可以堆一个数据报文进行分离。
四、netty如何解决拆包、粘包问题
1、使用定长报文协议
正对定长报文协议,Netty提供了io.netty.handler.codec.FixedLengthFrameDecoder
解码器。如果我们接收到下面所示的四个报文片段。
* +---+----+------+----+ * | A | BC | DEFG | HI | * +---+----+------+----+
一个定长为3的FixedLengthFrameDecoder
解码器,那么就会将它们解码为如下的三个片段。
* +-----+-----+-----+ * | ABC | DEF | GHI | * +-----+-----+-----+
该类的核心方法如下:
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//解析数据
Object decoded = decode(ctx, in);
//如果数据不为null,那么加入到out集合。父类会将消息往下传递
if (decoded != null) {
out.add(decoded);
}
}
protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//可读字节数小于需要截取的长度 返回null frameLength 是由入参传入的
if (in.readableBytes() < frameLength) {
return null;
} else {
//返回当前ByteBuf的一个分片,在底层共用数据
return in.readRetainedSlice(frameLength);
}
}
2、使用特殊的消息分隔符
针对使用特殊的消息分隔符作为数据的边界情况,Netty提供了io.netty.handler.codec.DelimiterBasedFrameDecoder
解码器作为支撑。该解码器可以指定一段二进制序列作为分隔符,器会不断的累计从通道读取的数据并检查数据是否符合二进制序列。一旦发现符合要求的,则以分隔符作为边界分离数据报文。
常用的构造方法如下:
/**
* 创建一个实例
*
* @param maxFrameLength 如何读取超过 maxFrameLength ,还未发现指定的分隔符,则抛出TooLongFrameException
* @param delimiter 分隔符
*/
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
this(maxFrameLength, true, delimiter);
}
很多协议是以换行符作为消息分隔的,比如FTP协议。如果是这种情况,Netty直接提供了一个LineBasedFrameDecoder
解码器来支持该场景。其工作原理是遍历ByteBuf
中得可读字节,判断是否有“\r\n”或者“\n"。
public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
this.maxLength = maxLength;
this.failFast = failFast;
this.stripDelimiter = stripDelimiter;
}
3、使用报文头+报文体协议
netty提供了io.netty.handler.codec.LengthFieldBasedFrameDecoder
来支持此种场景。这个编码器比较复杂,有四个重要属性,分别是:
- lengthFieldOffset:该属性意味着报文长度字段在整体报文中的偏移量
- lengthFieldLength:该属性意味着长度字段本身的字节长度,常见取值有1,2,4。该属性的值默认情况下指代报文体的长度。
- lengthAdjustment:该属性意味着在读取完毕
lengthFieldLength
后,剩余需要读取的字节数为lengthFieldLength
+lengthAdjustment
的长度。 - initialBytesToStrip:该属性意味着读取到的完整报文需要跳过一段长度的字节后的结果作为报文向后进行传递。
例如:lengthFieldOffset = 0,lengthFieldLength=2,lengthAdjustment=0,initialBytesToStrip=0
那么我们发生编解码的情况如下:
* BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) * +--------+----------------+ +--------+----------------+ * | Length | Actual Content |----->| Length | Actual Content | * | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | * +--------+----------------+ +--------+----------------+