学过TCP
的都知道,它是属于传输层的协议,传输层除了有TCP
协议外还有UDP
协议,但是UDP
是不存在拆包和粘包
的。UDP
是基于报文发送的,从UDP
的帧结构可以看出,在UDP
首部采用了16bit
来指示UDP
数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。
而TCP
是基于字节流的,虽然应用层和TCP
传输层之间的数据交互是大小不等的数据块,但是TCP
把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP
的帧结构也可以看出,在TCP
的首部没有表示数据长度的字段,基于上面两点,在使用TCP
传输数据时,才有粘包或者拆包现象发生的可能。
D1
和D2
没有粘包和拆包D2
和D1
,被称为TCP
粘包服务端分两次读取到了两个数据包,第一次读取到完整的D1
,D2
部分内容,第二次读取了D2
的剩余内容,这被称之为TCP拆包操作
D1_1
,第二次读取到了D1
包的剩余内容和完整的D2
数据包TCP
接收滑窗非常小,而数据包内容相对较大的情况,很可能发生服务端多次拆包才能将D1
和D2
数据接收完整TCP
发送缓冲区剩余空间大小,将会发生拆包。MSS
(最大报文长度),TCP
在传输前将进行拆包。TCP
发送缓冲区的大小,TCP
将多次写入缓冲区的数据一次发送出去,将会发生粘包。通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:
UDP
),首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了\r\n
),这样,接收端通过这个边界就可以将不同的数据包拆分开。io.netty.handler.codec.callDecode(ChannelHandlerContext ctx, ByteBuf in, List
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List
当上面一个ChannelHandlerContext
传入的ByteBuf
有数据的时候,这里我们可以把in
参数看成网络流
,这里有不断的数据流入,而我们要做的就是从这个byte
流中分离出message
,然后把message
添加给out
分开描述下代码逻辑:
out
中有message
的时候,直接将out
中的内容交给后面的ChannelHandlerContext
去处理ChannelHandlerContext
移除的时候,立即停止对网络数据的处理readableBytes
记录当前in
中可读字节数decode
是抽象方法,交给子类具体实现ChannelHandlerContext
是移除的时候,立即停止对网络数据的处理message
的时候,且子类实现也没有动ByteBuf
中的数据的时候,这里直接跳出,等待后续有数据来了再进行处理message
的时候,且子类实现动了ByteBuf
的数据,则继续循环,直到解析出message
或者不在对ByteBuf
中数据进行处理为止message
但是又没有动ByteBuf
中的数据,那么是有问题的,抛出异常。如果要实现具有处理粘包、拆包功能的子类,及decode实现,必须要遵守上面的规则,我们以实现处理第一部分的第二种粘包情况和第三种情况拆包情况的服务器逻辑来举例:
粘包:decode
需要实现的逻辑对应于将客户端发送的两条消息都解析出来分为两个message
加入out
,这样的话callDecode
只需要调用一次decode
即可。
拆包:decode
需要实现的逻辑主要对应于处理第一个数据包的时候,第一次调用decode
的时候out
的size
不变,从continue
跳出并且由于不满足继续可读而退出循环,处理第二个数据包的时候,对于decode
的调用将会产生两个message
放入out
,其中两次进入callDecode
上下文中的数据流将会合并为一个ByteBuf
和当前ChannelHandlerContext
实例关联,两次处理完毕即清空这个ByteBuf
。
尽管介绍了ByteToMessageDecoder
,用户自己去实现处理粘包、拆包的逻辑还是有一定难度的,Netty
已经提供了一些基于不同处理粘包、拆包规则的实现,我们可以根据规则自行选择使用Netty
提供的Decoder
来进行具有粘包、拆包处理功能的网络应用开发。
通过例子,来更加清晰的认识TCP
粘包/拆包带来的问题,以及使用Netty
内置的解决方案解决粘包/拆包的问题