扫描下方二维码或者微信搜索公众号
菜鸟飞呀飞
,即可关注微信公众号,阅读更多Spring源码分析
和Java并发编程
文章。
问题
在上一篇文章中分析到了 Netty 服务端是如何进行新连接的接入的,那么当新连接接入后,就可以开始数据的读写操作了。在进行数据读写操作时,对于 TCP 连接而言,netty 就需要解决 TCP 中粘包、半包的问题,这将是本文今天重点分析的内容。在开始阅读本文之前,可以先思考一下以下两个问题。
-
- 什么是 TCP 的粘包、半包问题?UDP 协议存在粘包半包吗?
-
- netty 是如何解决的
什么是粘包拆包
在 TCP/IP 协议模型中,TCP 和 UDP 协议属于传输层协议,这两个协议在数据传输过程中存在很大的差异。
对于 UDP 协议而言,它传输的数据是基于数据报来进行收发的,在 UDP 协议的头中,会有一个 16bit 的字段来表示 UDP 数据报文的长度,在应用层能很好的将不同的数据报文区分开。可以理解为,UDP 协议传输的数据是有边界的,因此它不会存在粘包、半包的问题。
而对于 TCP 协议而言,它传输数据是基于字节流传输的。应用层在传输数据时,实际上会先将数据写入到 TCP 套接字的缓冲区,当缓冲区被写满后,数据才会被写出去,这就可能造成粘包、半包的问题。而且当接收方接收到数据后,实际上接收到的是一个字节流,所谓的流,可以理解为河流一样。既然是流,多个数据包相互之间是没有边界的,而且在 TCP 的协议头中,没有一个单独的字段来表示数据包的长度,这样在接收方的应用层,从字节流中读取到数据后,是没办法将两个数据包区分开的。
粘包、半包示意图
当发送方连续向接收方发送两个完整的数据包时,如果使用 TCP 协议进行传输,就可能存在以下几种情况。下图中 packet1 和 packet2 分别表示发送方发送的两个完整的数据包。
第一种情况,没有发生粘包、半包的现象,即接收方正常接收到两个独立的完整数据包 packet1、packet2,这种情况是属于正常情况。如图 1 所示。
第二种情况,发生了粘包现象,即发送方将数据包 packet1 写入到自己的 TCP 套接字的缓冲区后,TCP 并没有立即将数据发送出去,因为此时缓冲区可能还没有慢。接着发送方又发送了一个数据包 packet2,仍然是先写入到 TCP 套接字的缓冲区,此时缓冲区满了,然后 TCP 才将缓冲区的数据一起发送出去,这时候接收方接收到的数据看起来只有一个数据包。在 TCP 的协议头中,没有一个单独的字段来表示数据包的长度,这样接收方根本就无法区分出 packet1 和 packet2,这就是所谓的粘包问题。另外,当接收方的 TCP 层接收到数据后,由于应用层没有及时从 TCP 套接字中读取数据,也会造成粘包现象。如图 2 所示。
第三种情况,发生了半包现象,即发送方依旧是先后发送了两个数据包 packet1 和 packet2,但是 TCP 在传输时,分了几次传输,每次传输的内容中包含的不是 packet1 和 packet2 的完整包,只是 packet1 或者 packet2 的一部分,就相当于把两个数据包的内容拆分了,因此也称之为拆包现象。如图 3 所示。
产生粘包、半包的原因
从上面的示意图中,我们大致可以知道产生粘包、半包的主要原因如下。
- 粘包原因
-
- 发送方每次写入的数据小于套接字缓冲区大小;
-
- 接收方读取套接字缓冲区的数据不够及时。
-
- 半包原因
-
- 发送方写入的数据大于套接字缓冲区的大小;
-
- 发送的数据大于协议的 MSS 或者 MTU,必须拆包。(MSS 是 TCP 层的最大分段大小,TCP 层发送给 IP 层的数据不能超过该值;MTU 是最大传输单元,是物理层提供给上层一次最大传输数据的大小,用来限制 IP 层的数据传输大小)。
-
但归根结底,产生粘包、半包的根本原因是因为 TCP 是基于字节流来传输数据的,数据包相互之间没有边界,导致接收方无法准确的分辨出每一个单独的数据包。
netty 如何解决粘包拆包问题
作为一个应用层的开发者,我们无法去改变 TCP 基于字节流来传输数据的特性,除非我们自定义一个类似于 TCP 的协议,但是难度太大,设计出来的性能还不一定比现有的 TCP 协议性能好,况且目前 TCP 协议的使用十分广泛。而 netty 作为一款高性能的网络框架,必然就要有对 TCP 协议的支持,既然支持 TCP 协议,那就要解决 TCP 中粘包、半包的问题,否则如果开发人员自己去解决,那就费时费力了。
netty 中通过提供一系列的编解码器来解决 TCP 的粘包、半包问题,顾名思义,编解码器就是通过将从 TCP 套接字中读取的字节流通过一定的规则,将其进行编码或者解码,编码成二进制字节流或者解析出一个个完整的数据包。在 netty 中提供了很多通用的编解码器,对于解码器而言,它们均继承自抽象类ByteToMessageDecoder;对于编码器而言,它们均继承与抽象类MessageToByteEncoder。
今天主要先简单分析下抽象类解码器ByteToMessageDecoder类的源码,对于具体的解码器实现将在后面两篇文章中详细分析其原理,对于编码器而言,编码过程与解码过程恰好相反,因此就不再单独赘述,有兴趣的朋友可以自行阅读,欢迎分享。
ByteToMessageDecoder实际上就是一个 ChannelHandler,它的具体实现类需要被添加到 pipeline 中才会起作用。当将解码器添加到 pipeline 中后,当出现 OP_READ 事件时,就会通过 pipeline 传播执行所有 handler 的 channelRead() 方法。在抽象类解码器中,就定义了 channelRead()方法。其源码如下。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// 用来存放解码出来的数据对象,可以将它当做一个集合
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
//第一次直接赋值
cumulation = data;
} else {
//累加数据
cumulation