TCP粘包问题

TCP是面向流的协议,流就像河流中的水,一个字节一个字节地发送,本身是不存在独立包的,包与包之间没有界限,所以会产生粘包现象。而UDP是基于数据报的协议,它有消息边界,不会出现粘包现象。

一. 什么是粘包现象

  TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。例如:
如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构的数据:
1)”hello give me sth abour yourself”
2)”Don’t give me sth abour yourself”
如果发送方连续发送这个两个包出去,接收方一次接收可能会是”hello give me sth abour yourselfDon’t give me sth abour yourself” 这样接收方就傻了,到底是要干嘛?

二. 为什么出现粘包现象

  (1)发送方原因
  我们知道,TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。所以,正是Nagle算法造成了发送方有可能造成粘包现象。
  (2)接收方原因
TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

三.什么时候需要考虑粘包问题

(1)如果TCP短连接,发送完数据就断开连接后,这样就不会发生粘包问题。
(2)如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。
(3)如果是TCP长连接,需要在连接后一段时间内发送不同结构数据,这些数据本毫不相干,甚至是并列的关系,那就必须要处理粘包问题了。

四.如何处理粘包现象

  (1)发送方
  对于发送方造成的粘包现象,我们可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭Nagle算法。但这样会降低TCP的传输效率,一般不会这么做。
  (2)接收方
  遗憾的是,TCP并没有处理接收方粘包现象的机制,我们只能在应用层进行处理。
  (3)应用层处理
  应用层的处理简单易行!并且不仅可以解决接收方造成的粘包问题,还能解决发送方造成的粘包问题。
  解决方法就是循环处理:应用程序在处理从缓存读来的分组时,读完一条数据时,就应该循环读下一条数据,直到所有的数据都被处理;但是如何判断每条数据的长度呢?
  两种途径:
    1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符,这种方法有局限性;
    2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。下面将着重讲解如何用这种方式解决粘包问题。

五.封包处理粘包问题

所谓封包,就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容,包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确地拆分出一个完整的数据包。对于拆包,目前最常用的是以下两种方式:

(1)动态缓冲区暂存方式

之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度。大概过程描述如下:
A. 为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体或类关联。
B. 当接收到数据时首先把此段数据存放在缓冲区中。
C. 判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作。
D. 根据包头数据解析出里面代表包体长度的变量。
E. 判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作。
F. 取出整个数据包,这里的”取”的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉,删除的办法就是把此包后面的数据移动到缓冲区的起始地址。

这种方法有两个缺点:
1.为每个连接动态分配一个缓冲区增大了内存的使用。
2.有三个地方需要拷贝数据,第一个是把数据存放在缓冲区,第二个是把完整的数据包从缓冲区取出来,第三个是把数据包从缓冲区中删除。

下面给出一个改进办法,即采用环形缓冲。环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾,在存放数据和删除数据时只是进行头尾指针的移动。但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方)。第2种拆包方式会解决这两个问题。

(2)利用底层的缓冲区来进行拆包

由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了。另一方面我们知道recv函数有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化。

对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。
相关代码如下:

char PackageHead[1024];
char PackageContext[1024*20];
int len;
PACKAGE_HEAD *pPackageHead;
while( m_bClose == false )
{
    memset(PackageHead,0,sizeof(PACKAGE_HEAD));
    len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
    if( len == SOCKET_ERROR || len == 0)
    {
        break;
    }
    pPackageHead = (PACKAGE_HEAD *)PackageHead;
    memset(PackageContext,0,sizeof(PackageContext));
    if(pPackageHead->nDataLen>0)
    {
        len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
    }
}


int ReceiveSize( char* strData, int iLen )
{
    if( strData == NULL )
        return ERR_BADPARAM;
    int len = iLen;
    int ret = 0;
    int returnlen = 0;
    while( len > 0)
    {
        ret = recv( m_hSocket, strData+(iLen-len), iLen-returnlen, 0 );
        if ( ret == SOCKET_ERROR || ret == 0 )
        {
            return ret;
        }
        len -= ret;
        returnlen += ret;
    }
    return returnlen;
}

//m_bClose 是是否断开连接的bool值,m_TcpSock是一个封装了SOCKET的类的变量
//ReceiveSize函数用于接收一定长度的数据,直到接收了一定长度的数据或者网络出错才返回。

对于非阻塞的SOCKET ,我们也可以利用recv函数的第三个参数,首先提交接收包头长度的数据的请求,函数返回时,判断接收的数据长度是否等于包头长度,若等于说明接收一个完整包头。接着就解析包头得到数据长度,继续提交接收包体长度的数据的请求,若返回值不等于该长度,则提交接收剩余数据的请求。代码与上面的类似,需要处理非阻塞情况下的数据接收。

小结:

选择用哪种方式解决粘包问题,得要具体情况具体分析。直接用底层的缓冲区直接进行拆包,固然可以免于拷贝到缓冲区,但是这样每次接收,都要做逻辑处理,对于频繁接收数据的场景,这种方法效率并不高。

而如果我们一次性接收到很多数据,然后利用动态缓冲区暂存,进行异步处理。一次接收,多次处理,效率反而会更高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值