Netty源码分析 解决TCP粘包拆包问题
本文使用
netty-4.1.5.Final
版本源码进行分析
客户端或者服务端在进行消息发送和接收的时候,需要考虑TCP底层的粘包拆包机制。一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。一般需要上层的应用协议栈设计上,来在应用层,识别出单个完整的数据包。
粘包、拆包发生原因
发生TCP粘包或拆包有很多原因,常见的几点:
要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。等等。
粘包、拆包解决策略
TCP底层无法识别业务上一个完整的数据包,所以需要在设计应用协议栈的时候,在应用层,识别出单个完整的数据包。一般需要在消息接受方做应用层协议栈的处理,协议需要能识别出当前是否已经读取完了一个完整的数据包,如果读取到多个数据包,需要分割多个数据包,拆分成多个完整的数据包;如果没有读取到完整的数据包,需要继续读取,直到读取到一个完整的数据包,最终将单个完整的数据包交由应用做进一步处理。
主流协议的解决方案:
- 消息定长。优点是最简单;缺点就是消息体长度受限,长度太长浪费,长度不够又限制应用数据传输,所以一般应用上不会采取
- 包尾增加分隔符。需要保证分割符不会出现在应用传输的数据中
- 消息中包含消息的总长度。比如,在消息的前缀增加4个字节int值作为当前数据包总长度;比如dubbo协议,由一个定长包头和变长包体组成,包头中的特定字节中包含变长包体的总长度。这种方式相对更灵活。
Netty解决策略
为了解决TCP粘包、拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包,或者可以根据实际情况自行实现ChannelHandler来定制自己的应用协议栈,一般可以直接实现ByteToMessageDecoder。使用时只需要将需要的编解码器添加到channel的责任链上即可,熟练掌握这些类库的使用,就可以非常容易的处理TCP粘包拆包带来的问题。
Netty默认提供的编解码器:
定长消息解码器:
FixedLengthFrameDecoder
从ByteBuf中读取自定义的长度的数据,做为一个完整数据包。
分隔符消息解码器:
LineBasedFrameDecoder
依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置。
DelimiterBasedFrameDecoder
依次遍历ByteBuf中的可读字节,判断看是否有自定义的分隔符,如果有,就以此位置为结束位置。
定长字段标识消息长度的消息解码器:
- LengthFieldBasedFrameDecoder
ProtobufVarint32FrameDecoder
读取ByteBuf的某几个字节,转化为长度值,做为消息体的总长度。
源码分析
我们以定长消息解码器FixedLengthFrameDecoder源码来看下Netty如何处理粘包拆包问题的
|
|
|
|
总结
通过看FixedLengthFrameDecoder,再对比其他几种处理粘包拆包的编解码器,可以总结出来,Netty处理粘包拆包问题的核心思路就是:每次读取的时候,如果能读取到一个完整的数据包,才真正读取出来,一直读到没有数据可读,如果有半包消息,则保存下来未处理的半包消息,下次读事件触发的时候,将未处理的半包消息和新的消息内容合并在一起再继续处理。最后将所有解析出来的完整数据包依次进行fireChannelRead事件的传播,进行后续的业务处理。