Netty之TCP粘包拆包问题

来源:互联网 发布:倍量画线主图公式源码 编辑:程序博客网 时间:2024/06/05 02:30

引言

粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都可能发生。我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界(推荐一篇关于边界处理保护的解释http://blog.csdn.net/zhangxinrun/article/details/6721427),不会发生这个问题,因此这篇文章只讨论发生在传输层的TCP粘包问题。
什么是粘包、拆包?
对于什么是粘包、拆包问题,先举两个简单的应用场景:

1.客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务端的连接。
2.客户端和服务器建立一个连接,客户端连续发送两条连续消息,客户端关闭与服务端的连接。

对于第一种情况,服务端的处理流程:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息,然后进行解码和后续处理。对于第二种情况,当两条消息到达服务端就可能出现如下情况:

第一种情况:
服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网路缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。


这里写图片描述
没有发生粘包、拆包示意图

第二种情况:服务端一共就读到一个数据包,这个数据包包含客户端发送的两条信息,这个时候基于之前的逻辑实现就不行了,因为服务端不知道第一条消息从哪结束和第二条消息从哪开始,这种情况其实是发生了TCP粘包。


这里写图片描述
TCP粘包示意图

第三种情况:
服务端一共收到两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都包含在第二个数据包中,或者第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实就是发送了TCP拆包,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。


这里写图片描述
TCP拆包示意图

为什么会发生TCP粘包、拆包呢?
发生TCP粘包、拆包主要是由于下面一些原因:
1.应用程序写入的数据大于套接字缓冲区大小,这将发生拆包。
2.应用程序写入数据小于套接缓冲区大小,网卡将应用多次写入的数据发送到网络,这将发生粘包。
3.进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
4.接收方不能及时读取套接字缓冲区数据,将发生粘包
5. …….

如何处理粘包、拆包问题?
之前关于Netty中EventLoop机制中有提过ChannelPipeline部分大概讲了Netty网络层数据的流向以及ChannelHandler组件对网络数据的处理,这一小节也会这几到相关重要的组件:
1.ByteToMessageDecoder
2.MessageToMessageDecoder
这两个组件都实现了ChannelInBoundHandler接口,这说明这两个组件都是用来解码网络上过来的数据的。而他们的顺序一般是ByteToMessageDecoder位于head channel handler的后面,MessageToMessageDecode位于ByteToMessageDecoder的后面。Netty中,涉及到粘包、拆包的逻辑主要在ByteToMessageDecoder及其实现中。

ByteToMessageDecoder
顾名思义,ByteToMessageDecoder是用来将从网络缓冲区读取的字节转换成有意义的消息对象的,主要源码实现如下:

protected void callDecode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out){    try    {        while(in.isReadable())        {            int outSize = out.size();            if(outSize>0)            {                fireChannelRead(ctx,out,outSize);                out.clear();                if(ctx.isRemoved())                {                    break;                }                outSize = 0;            }            int oldInputLength = in.readableBytes();            decode(ctx,in,out);            if(ctx.isRemoved())            {                break;            }            if(outSize==out.size())            {                if(oldInputLength==in.readableBytes())                {                    break;                }else{                    continue;                }            }            if(oldInputLength==in.readableBytes())            {                throw new DecoderException(StringUtil.simpleClassName(getClass())+".decode() did not read anything but decoded a message.");            }            if(isSingleDecode())            {                break;            }        }    }catch(DecoderException e)    {        throw e;    }catch(Throwable cause)    {        throw new DecoderException(cause);    }}

为节省篇幅,注释被删除了,当上面一个channel handler传入的ByteBuf有数据时候,这里我们可以把in参数看成网络刘,这里不断的有数据流入,而我们要做的就是从这个Byte流中分离出message,然后把message添加给out。代码逻辑如下:
1.当out中有Message的时候,直接将out中的内容交给后面的channel handler去处理。
2.当用户逻辑把当前channel handler移除的时候,立即停止对网络数据的处理。
3.记录当前in中可读字节数。
4.decode是抽象方法,交给子类具体实现。
5.同样判断当前channel handler移除的时候,立即停止对网络的处理。
6.如果子类实现没有分离出任何message的时候,且子类实现也没有动bytebuf中的数据的时候,这里直接跳出,等待后续有数据来了再进行处理。
7.如果子类实现没有分离出任何message的时候,且子类实现处理了bytebuf中的数据的时候,继续循环,直到解析出message或者不在对bytebuf中数据进行处理为止。
8.如果子类实现解析出了message但是又没有动bytebuf中的数据,那么是有问题的,抛出异常。
9.如果标志位只解码一次,则退出。

可以知道,如果要实现具有处理粘包、拆包功能的子类,及decode实现,必须遵循以上规则,我们以实现处理第一部分的第二种粘包情况和第三种情况拆包情况的服务器逻辑来举例:
对于粘包情况的decode需要实现的逻辑对应于将客户端发送的两条消息都解析出来分为两个message加入out,这样的话callDecode只需要调用一次decode即可。
对于拆包情况的decode需要实现的逻辑主要对应于处理第一个数据包的时候调用decode的时候out的size不变,从continue跳出,并且由于不满足继续可读而退出循环,处理第二个数据包的时候,对于decode的调用将会产生两个message放入out,其中两次进入callDecode上下文中的数据流将会合并为一个bytebuf和当前channel handler实例关联,两次处理完即清空这个bytebuf。

尽管介绍了ByteToMessageDecoder,用户自己去实现处理粘包、拆包的逻辑还是有一定难度的,Netty已经提供了一些基于不同处理粘包、拆包规则的实现:如DelimiterBasedFrameDecoder、FixedLengthFrameDecoder、LengthFieldBasedFrameDecoder和LineBasedFrameDecoder等等。其中:
DemiliterBaseFrameDecoder是基于消息边界的方式进行粘包、拆包处理的。
FixedLengthFrameDecoder是基于固定长度消息进行粘包、拆包处理。
LengthFieldBasedFrameDecoder是基于消息头指定消息长度进行粘包、拆包处理。
LineBasedFrameDecoder是基于行来进行消息粘包、拆包处理。
用户可自行选择规则,然后使用Netty提供的对应的Decoder来进行具有粘包、拆包处理功能的网络应用开发。

最后,在通常高性能网络应用中,客户端通常以长连接的方式和服务端相连,因为每次建立网络连接是一个很耗时的操作。比如在RPC调用过程中,如果一个客户端远程调用的过程中,连续发起多次调用,而如果这些调用对应于同一个连接的时候,自然就需要服务器对这些多次调用消息的粘包拆包处理。用户可适应性选择以上策略进行处理。

原创粉丝点击