Netty精粹之TCP粘包拆包问题

粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都有可能发生。咱们平常的网络应用开发大都在传输层进行,因为UDP有消息保护边界,不会发生这个问题,所以这篇文章只讨论发生在传输层的TCP粘包拆包问题。java


什么是粘包、拆包?服务器

对于什么是粘包、拆包问题,我想先举两个简单的应用场景:网络

  1. 客户端和服务器创建一个链接,客户端发送一条消息,客户端关闭与服务端的链接。性能

  2. 客户端和服务器简历一个链接,客户端连续发送两条消息,客户端关闭与服务端的链接。spa

对于第一种状况,服务端的处理流程能够是这样的:当客户端与服务端的链接创建成功以后,服务端不断读取客户端发送过来的数据,当客户端与服务端链接断开以后,服务端知道已经读完了一条消息,而后进行解码和后续处理...。对于第二种状况,若是按照上面相同的处理逻辑来处理,那就有问题了,咱们来看看第二种状况下客户端发送的两条消息递交到服务端有可能出现的状况:.net

第一种状况:
code

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

没有发生粘包、拆包示意图
blog

第二种状况:接口

服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于以前逻辑实现的服务端就蒙了,由于服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种状况实际上是发生了TCP粘包。

   TCP粘包示意图

第三种状况:

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

TCP拆包示意图


为何会发生TCP粘包、拆包呢?

发生TCP粘包、拆包主要是因为下面一些缘由:

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用屡次写入的数据发送到网络上,这将会发生粘包。

  3. 进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。

  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

  5. ……


如何处理粘包、拆包问题?

知道了粘包、拆包问题及根源,那么如何处理粘包、拆包问题呢?TCP自己是面向流的,做为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?一般会有如下一些经常使用的方法:

  1. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,而后向后读取该长度的内容。

  2. 设置定长消息,服务端每次读取既定长度的内容做为一条完整消息。

  3. 设置消息边界,服务端从网络流中按消息编辑分离出消息内容。

  4. ……


如何基于Netty处理粘包、拆包问题?

个人上一篇文章的ChannelPipeline部分大概讲了Netty网络层数据的流向以及ChannelHandler组件对网络数据的处理,这一小节也会涉及到相关重要组件:

  1. ByteToMessageDecoder

  2. MessageToMessageDecoder

这两个组件都实现了ChannelInboundHandler接口,这说明这两个组件都是用来解码网络上过来的数据的。而他们的顺序通常是ByteToMessageDecoder位于head channel handler的后面,MessageToMessageDecoder位于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等等。其中:

DelimiterBasedFrameDecoder是基于消息边界方式进行粘包拆包处理的。

FixedLengthFrameDecoder是基于固定长度消息进行粘包拆包处理的。

LengthFieldBasedFrameDecoder是基于消息头指定消息长度进行粘包拆包处理的。

LineBasedFrameDecoder是基于行来进行消息粘包拆包处理的。

用户能够自行选择规则而后使用Netty提供的对应的Decoder来进行具备粘包、拆包处理功能的网络应用开发。


最后

在一般的高性能网络应用中,客户端一般以长链接的方式和服务端相连,由于每次创建网络链接是一个很耗时的操做。好比在RPC调用中,若是一个客户端远程调用的过程当中,连续发起了屡次调用,而若是这些调用对应于同一个链接的时候,那么就会出现服务器须要对于这些屡次调用消息的粘包拆包问题的处理。若是是你,你会选择哪一种策略呢?


本文基于Netty4.1主分支代码,若有问题,还请多多指教。

相关文章
相关标签/搜索