TCP粘包问题:
最近做项目时,用QTcpSocket做客户端去接收数据,发现有时数据量大的时候,一次只能接收到该次发送的部分数据,或者数据量少的时候,会一次读取到对方两次发送的数据包,查了一些资料发现,这个就是Tcp的粘包问题。因为Tcp发送的数据是以流的形式传送的,就像水龙头放水一样,中间会也有断开的地方,我们需要怎么做才能取得我们需要的数据包呢?也就是说数据该从何处切分?
那么什么时候才需要考虑粘包问题?
1、如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如”hello give me sth about yourself”,然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。
2、如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。
3、如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
a、”hello give me about your message”
b、”Don’t give me about your message”
这样的话,如果发送方连续发送两个这样的包出去,接收方一次接收可能会是”hello give me abour your messageDon’t give me about your message”,这样接收方就傻眼了,到底应该怎么分了?因为没有协议规定怎么拆分这段字符串,所以要处理好分包,需要双方组织一个比较好的包结构,一般会在头上加上消息类型,消息长度等以确保正常接收。
粘包出现原因
粘包只可能出现在流传输中,TCP是基于流传输的,而UDP是不会出现粘包,因为他是基于报文的,也就是说UDP发送端调用几次write,
接收端必须调用相同次数的read读完,他每次最多只能读取一个报文,报文与报文是不会合并的,如果缓冲区小于报文长度,则多出来的部分会被丢掉。TCP不同了,他会合并消息,并且以不确定方式合并,这样就需要我们去粘包处理了,TCP造成粘包主要原因:
1、发送端需要等缓冲区满了才发送出去,造成粘包。 2、接收方不及时接收缓冲区的包,造成多个包一起接收。
解决方法:
为了避免粘包现象,可采取以下几种措施: 1、对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后, 就立即将本段数据发送出去,而不必等待发送缓冲区满; 2、是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽 量避免出现粘包现象; 3、是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。
一般大多数都是使用第三种方法,自己定义包协议格式,然后人为粘包,那么我们就需要知道TCP发送时,大概会有哪几种包情况产生:
1、先接收到data1,然后接收到data2。 这是我们希望的,但是往往不是这样的。 2、先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部。 3、先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据。 4、一次性接收到了data1和data2的全部数据。
上面就是主要的几种情况,一般就是这几种,对于2、3、4就需要我们粘包处理了。
下面是我们处理粘包的案例:
我们采取一种特殊字符数组作为分隔符,每次读取数据时会进行拼接,并且检查数据是否以该分隔符结尾,如超时则结束等待。所以我们接受到的数据可能有以下几种情形:(分隔符 char endTag_m[4] = { 0xF0,0xE1,0xC2,0xB3})
1:“you are my sunshine” //无分隔符 一般是等待数据直到有第一个分隔符出现,超时结束本轮接收
2: “you are my sunshine 分隔符”//理想状态
3: “you are my sunshine 分隔符 you”//跟后一个粘包了,一般是等待数据直到有第二个分隔符出现,超时结束本轮接收
4:“you are my sunshine 分隔符 you are my sunshine too 分隔符”//两个数据包粘在一块,这种数据根据分隔符做一下处理即可
在该代码中,我们遇到比较多是接收到两个数据包粘在一块,原因可能是客户端处理不及时导致的,因为服务端抓包可见数据是分两次发送的,不存在一次发送两个数据。
希望大神能否解决客户端这边处理得更快一点?也就是一旦服务端发送数据,客户端立即处理掉,不会有数据滞留的情况。可以提高线程运行优先级?或者放到主线程中去?
void tcpSocket::slotMsgRecved()//;//接收到客户端消息
{
QByteArray recvMsg;//当前拼接数据流
int cnt = 0;
while (cnt<1000)
{
cnt++;
QByteArray datagram;
datagram.resize(sock_m->bytesAvailable());
sock_m->read(datagram.data(),datagram.size());
recvMsg.append(datagram);
if (recvMsg.endsWith(endTag_m))
{//遇到分隔符就结束
break;
}
if (""==datagram&&""==recvMsg)
{//当前数据是空的并且没有新的数据到来
break;
}
sleepMsec(1);
}
//将数据包放入lstMsgRecv链表中
mutex.lock();
lstMsgRecv.append(recvMsg);
mutex.unlock();
}
//调用线程 一直处理数据接收和拆包分发。
void tcpSocket::run()
{
while(1)
{
if (recvDataFromThread)
{//一直接收数据
slotMsgRecved();
}
if (lstMsgRecv.size()>0)
{//做数据分发
mutex.lock();
QByteArray byteArr = lstMsgRecv.takeFirst();
mutex.unlock();
MsVerify msVerify;
//拆包处理 以分隔符进行分割 放入链表中
byteArr.replace(endTag_m,",");
QList<QByteArray> lstByteArr = byteArr.split(',');
for (int i=0;i<lstByteArr.size();i++)
{
QByteArray temp = lstByteArr.at(i);
if (temp=="")
{
continue;
}
//分发数据到不同的关联数组中
QByteArray byteNoEndTag= msVerify.removeEndTagAndUnicToMByte(temp);
QByteArray respCmd = msVerify.getDataByXmlContent(byteNoEndTag,"Response");
QByteArray CS = msVerify.getDataByXmlContent(byteNoEndTag,"ModuleId");
if (""!=CS)
{
if ("ManipulateData"==respCmd)
{
mapPrepareMsgRecv[CS] = temp + endTag_m;
}
else if ("ModuleStart"==respCmd)
{
mapModuleStartMsgRecv[CS] = temp + endTag_m;
}
}
}
}
sleepMsec(1);
}
}
void sleepMsec(unsigned int millseconds)
{
QTime reachTime = QTime::currentTime().addMSecs(millseconds);
while (QTime::currentTime() < reachTime)
{
QCoreApplication::processEvents(QEventLoop::AllEvents,100);
}
}