您的位置:首页 > 理论基础 > 计算机网络

TCP粘包半包问题

2017-01-10 12:15 281 查看
http://blog.csdn.net/cherish_2012/article/details/41681853

粘包产生原因:

先说TCP:由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象(确切来讲,对于基于TCP协议的应用,不应用包来描述,而应
用 流来描述),个人认为服务器接收端产生的粘包应该与linux内核处理socket的方式 select轮询机制的线性扫描频度无关。

再说UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。

分包产生的原因就简单的多:可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

解决办法:

粘包与分包的处理方法:

我根据现有的一些开源资料做了如下总结(常用的解决方案):

一个是采用分隔符的方式,即我们在封装要传输的数据包的时候,采用固定的符号作为结尾符(数据中不能含结尾符),这样我们接收到数据后,如果出现结尾标识,即人为的将粘包分开,如果一个包中没有出现结尾符,认为出现了分包,则等待下个包中出现后 组合成一个完整的数据包,这种方式适合于文本传输的数据,如采用/r/n之类的分隔符;

另一种是采用在数据包中添加长度的方式,即在数据包中的固定位置封装数据包的长度信息(或可计算数据包总长度的信息),服务器接收到数据后,先是解析包长度,然后根据包长度截取数据包(此种方式常出现于自定义协议中),但是有个小问题就是如果客户端第一个数据包数据长度封装的有错误,那么很可能就会导致后面接收到的所有数据包都解析出错(由于TCP建立连接后流式传输机制),只有客户端关闭连接后重新打开才可以消除此问题,我在处理这个问题的时候对数据长度做了校验,会适时的对接收到的有问题的包进行人为的丢弃处理(客户端有自动重发机制,故而在应用层不会导致数据的不完整性);

另一种不建议的方式是TCP采用短连接处理粘包(这个得根据需要来,所以不建议);

/***********************************************************************************************************************************************/


TCP粘包处理-RingBuf方法

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。粘包可能由发送方造成,也可能由接收方造成。TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据,造成多个数据包的粘连。如果接收进程不及时接收数据,已收到的数据就放在系统接收缓冲区,用户进程读取数据时就可能同时读到多个数据包。因为系统传输的数据是带结构的数据,需要做分包处理。

为了适应高速复杂网络条件,我们设计实现了粘包处理模块,由接收方通过预处理过程,对接收到的数据包进行预处理,将粘连的包分开。为了方便粘包处理,提高处理效率,在接收环节使用了环形缓冲区来存储接收到的数据。其结构如表1所示。

                                                            表1 环形缓冲结构

字段名
类型
含义
CS
CRITICAL_SECTION
保护环形缓冲的临界区
pRingBuf
UINT8*
缓冲区起始位置
pRead
UINT8*
当前未处理数据的起始位置
pWrite
UINT8*
当前未处理数据的结束位置
pLastWrite
UINT8*
当前缓冲区的结束位置
环形缓冲跟每个TCP套接字绑定。在每个TCP的SOCKET_OBJ创建时,同时创建一个PRINGBUFFER结构并初始化。这时候,pRingBuf指向环形缓冲区的内存首地址,pRead、pWrite指针也指向它。pLastWrite指针在这时候没有实际意义。初始化之后的结构如图1所示。



图1 初始化后的环形缓冲区

在每次投递一个TCP的接收操作时,从RINGBUFFER获取内存作接收缓冲区,一般规定一个最大值L1作为可以写入的最大数据量。这时把pWrite的值赋给BUFFER_OBJ的buf字段,把L1赋给bufLen字段。这样每次接收到的数据就从pWrite开始写入缓冲区,最多写入L1字节,如图 2。



图2 分配缓冲后的环形缓冲

如果某次分配过程中,pWrite到缓冲区结束的位置pEnd长度不够最小分配长度L1,为了提高接收效率,直接废弃最后一段内存,标记pLastWrite为pWrite。然后从pRingBuf开始分配内存,如图 3。



图 3 使用到结尾的环形缓冲

特殊情况下,如果处理包速度太慢,或者接收太快,可能导致未处理包占用大部分缓冲区,没有足够的缓冲区分配给新的接收操作,如图4。这时候直接报告错误即可。



图 4没有足够接收缓冲的环形缓冲

当收到一个长度为L数据包时,需要修改缓冲区的指针。这时候已经写入数据的位置变为(pWrite+L),如图 5。



图 5收到长度为L的数据的环形缓冲

分析上述环形缓冲的使用过程,收到数据后的情况可以简单归纳为两种:pWrite>pRead,接收但未处理的数据位于pRead到pWrite之间的缓冲区;pWrite<pRead,这时候,数据位于pRead到pLastWrite和pRingbuf到pWrite之间。这两种情况分别对应图6、图 7。

首先分析图6。此时,pRead是一个包的起始位置,如果L1足够一个包头长度,就获取该包的长度信息,记为L。假如L1>L,就说明一个数据包接收完成,根据包类型处理包,然后修改pRead指针,指向下一个包的起始位置(pRead+L)。这时候仍然类似于之前的状态,于是解包继续,直到L1不足一个包的长度,或者不足包头长度。这时退出解包过程,等待后续的数据到来。



图 6有未处理数据的环形缓冲(1)



图 7有未处理数据的环形缓冲(2)

图 8稍微复杂。首先按照上述过程处理L1部分。存在一种情况,经过若干个包处理之后,L1不足一个包,或者不足一个包头。如果这时(L1+L2)足够一个包的长度,就需要继续处理。另外申请一个最大包长度的内存区pTemp,把L1部分和L2的一部分复制到pTemp,然后执行解包过程。

经过上述解包之后,pRead就转向pRingBuf到pWrite之间的某个位置,从而回归情况图 6,继续按照图 6部分执行解包。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: