在 《Netty 系列笔记之开篇》中我们已经对 TCP 粘包、拆包的问题处理有所涉及:
- TCP 协议本身设计就是面向流的,提供可靠传输。
- 正因为面向流,对于应用层的数据包而言,没有边界区分。这就需要应用层主动处理不同数据包之间的组装。
- 发生粘包现象不是 TCP 的缺陷,只是应用层没有主动做数据包的处理。
接下来,让我们详细解析 Netty 如何处理 TCP 的粘包、拆包现象。
数据的发送方发送了 ABC DEF 数据,接收方有可能接收到的是 ABCDEF ,两个数据包粘在一起称为粘包。也有可能接收到的是 AB CD EF ,两个数据包被拆分为三个数据包,称为拆包或半包。
具象到下面这张图:
要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
待发送数据大于 MSS(最大报文长度)即各层的MTU (最大传输单元),TCP 在传输前将进行拆包。
根本原因:TCP 是流式协议,数据之间没有边界。
❀ 关于最大传输单元
如上左图,TCP/IP 协议分处 5 层协议的不同层,每层中都有规定最大的传输大小,如 IPV4 最大传输为64 kb ,即为最大传输单元。
解决粘包和拆包问题的思路是找出数据的边界。
三个 Decoder 都继承自 ByteToMessageDecoder
,它是一个抽象类,其有一个抽象方法 :
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
用于对接收到的数据进行解码。
基于不同的协议,它有非常非常多的实现,以最简单的 FixedLengthFrameDecoder
为例,看如何实现:
decode()
方法:@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
// 读取的数据没有达到预定长度
if (in.readableBytes() < frameLength) {
return null;
} else {
// 达到长度,读取固定长度的数据返回
return in.readRetainedSlice(frameLength);
}
}
只有几行代码,逻辑非常简单,数据只有达到固定的长度才读取并返回。那么,如果长度不够时,这部分数据存储在那里呢?
这部分逻辑在 ByteToMessageDecoder
的 channelRead()
方法中,其有一个数据积累器用于存储读到的信息,感兴趣的小伙伴可自行查看。
上面的我们说的解码器都继承自 ByteToMessageDecoder
,作用是将存在粘包、拆包问题的用户数据转换为真正的用户数据,其本质上还是字节数组。
而通常来说,我们需要的数据时 Java 对象,所以我们需要对解码后的数据进行二次解码,用到的是 MessageToMessageDecoder
,从命名上面很明显看出他们是做什么的。
数据处理的流程就很清晰了:
ByteToMessageDecoder | -> | MessageToMessageDecoder |
---|---|---|
Bytebuff -> ByteBuff | => | ByteBuff -> Java Object |
这样将粘包、拆包问题的处理与具体的协议处理分离开来,耦合性大大降低,可以随时替换不同的协议。