文档章节

Netty精粹之TCP粘包拆包问题

Float_Luuu
 Float_Luuu
发布于 2016/02/27 23:14
字数 2489
阅读 13780
收藏 33

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


什么是粘包、拆包?

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

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

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

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

第一种情况:

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

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

第二种情况:

服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了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主分支代码,如有问题,还请多多指教。




© 著作权归作者所有

共有 人打赏支持
Float_Luuu
粉丝 206
博文 46
码字总数 102357
作品 0
长宁
高级程序员
Netty 粘包/拆包应用案例及解决方案分析

熟悉TCP变成的可以知道,无论是客户端还是服务端,但我们读取或者发送消息的时候,都需要考虑TCP底层粘包/拆包机制,下面我们先看一下TCP 粘包/拆包和基础知识,然后模拟一个没有考虑TCP粘包...

Java/Python
08/21
0
0
TCP 粘包问题浅析及其解决方案

最近一直在做中间件相关的东西,所以接触到的各种协议比较多,总的来说有TCP,UDP,HTTP等各种网络传输协议,因此楼主想先从协议最基本的TCP粘包问题搞起,把计算机网络这部分基础夯实一下。...

haifeiWu
07/24
0
0
Netty5入门学习笔记003-TCP粘包/拆包问题的解决之道(下)

TCP网络通信时候会发生粘包/拆包的问题,上节使用定长解码器解码,本次使用Netty提供的特殊分隔符解码器 还是用上节中的代码例子,但是只需要修改一下发送的消息和配置一下解码器就可以了 客...

山东-小木
2014/12/18
0
4
【Netty】Netty实例开源项目

版权声明:本文为谙忆原创文章,转载请附上本文链接,谢谢。 https://blog.csdn.net/qq_26525215/article/details/81989644 Netty netty-not-sticky-pack-demo 项目地址 Netty 本篇博客讲解:...

谙忆
08/23
0
0
Netty解决半包(TCP粘包/拆包导致)读写问题

TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限没有分割的一串数据。TCP会根据缓冲区的实际情况进行包划分,一个完整的包可能会拆分成多个包进行发送,也用可能把多个小包封装成一个大...

天冰
2014/06/19
0
0

没有更多内容

加载失败,请刷新页面

加载更多

GO冒泡,二分查找

package mainimport("fmt")func main() {var arr [5]int = [5]int{11,13,9,2,25}maopao(&arr)fmt.Println("arr = ", arr) //[2 9 11 13 25]findIndex := binaryFind(&arr, 0......

汤汤圆圆
21分钟前
1
0
工作2年半跳槽面试阿里,成功拿到offer,凭什么?

2015年刚毕业的我,进入了一家小小的公司实习工作,在学校学了三年软件开发的我,还是想去寻找一份互联网行业的工作,这样更能学以致用发挥自己的特长。一直到18年三月份,我辞掉已有的工作,...

java知识分子
25分钟前
1
0
讲述下:Linux的10个最危险的命令

导读 Linux命令行佷有用、很高效,也很有趣,但有时候也很危险,尤其是在你不确定你自己在正在做什么时候。这篇文章将会向你介绍十条命令,但你最好不要尝试着去使用。 当然,以下命令通常都...

问题终结者
29分钟前
1
0
分库分表后如何部署上线?

引言 我们先来讲一个段子 面试官:“有并发的经验没?” 应聘者:“有一点。” 面试官:“那你们为了处理并发,做了哪些优化?” 应聘者:“前后端分离啊,限流啊,分库分表啊。。” 面试官:...

Java烂猪皮
34分钟前
1
0
Redis源码阅读笔记-快速列表

快速列表 快速列表(quicklist)是由压缩列表(ziplist)组成的一个双向链表,链表中,每一个节点都是以压缩列表(ziplist)的结构保存。 在 Redis3.2 后加入的新数据结构,在列表键中取代了双向链...

Jian_Ming
52分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部