粘包产生的原因
网络通信方式主要有两种:TCP
与UDP
。 UDP
是基于报文传输的,发送几次Write()
,接收端就会用几次Read()
,每次读取一个报文,报文间不合并,多余缓冲区的报文会丢弃。TCP
是基于数据流传输的,Write()
和Read()
的次数不固定,报文间会以随机的方式合并,这就需要在接收时处理粘包了。通过上面的分析,我们可以发现,粘包只可能出现在流式传输中。 其粘包原因可能是下面两种情况:
- 发送端需要等缓冲区才能发送数据,造成发送时就粘包;
- 接收端未及时接收缓冲区的数据,多包一起接收,造成粘包;
粘包的解决方法
为了避免粘包,我们一般可以采取以下三种措施:
- 发送端粘包,可以通过程序设置
push
指令,不等缓冲区满就立即发送数据(默认情况是等缓冲区满后再发送)。这种方法对于通信的传输效率会降低,有时也不是百分百能可靠。 - 接收端粘包,可以通过优化程序、精简进程工作量、提高进程优先级等措施,使其及时接收数据。这种方法,你可以发现,有时候也是无法优化的,实现起来会比较难。
- 采用自定义包头结构,人为控制多次合并,来避免粘包问题。这是常用的做法。
这里我针对项目中遇到的问题主要是接收数据时存在粘包问题,进行分析。TCP
在接收数据时有四种情况:
- 先接收到
data1
、再接收data2
,这是我们所期待的; - 先接收到
data1
的部分数据,再接收到data1
的余下部分以及data2
的全部; - 先接收到
data1
的全部数据和data2
的部分数据,再接收到data2
的部分数据; - 一次性接收
data1
与data2
的全部数据。
对于后三种情况,我们都是要进行粘包处理的。
这里我将客户端接收小车数据的防粘包的处理方法代码示例出来,主要是定义了一个缓冲区,将从socket
中读取的数据放入缓冲区,再从缓冲区中按固定结构去解析数据。
QByteArray m_buffer_car;
void SlotReadyReadDataCar()
{
//socket缓冲区中没有数据直接返回
if(socket->bytesAvailable() <=0)
return;
QByteArray buffer = socket->readAll();
m_buffer_car.append(buffer); //读取数据放入缓冲区
qint64 tune_cmd;
qint64 struct_count;
qint64 total_bytes;
int total_length = m_buffer_car.size();
while(total_length)
{
QDataStream recv_data(m_buffer_car); //从缓冲区中读取
recv_data.setVersion(QDataStream::Qt_4_5);
if(total_length < sizeof(qint64)*3) //不够包头数据长度,等下次解析
break;
recv_data>>tune_cmd>>struct_count>>total_bytes;
if(total_length < total_bytes + sizeof(qint64)*3) //不够数据长度
break;
if(tune_cmd == _TCP_TUNE_CAR_)
{
for(int i = 0; i<struct_count; ++i)
{
recv_data>>st_car_monitor_info;
emit updateCar(st_car_monitor_info, i);
//这里可以对数据进一步处理
}
}
//缓存多余的数据
buffer = m_buffer.car.right(total_length - total_bytes - sizeof(qint64)*3)
//更新长度
total_length = buffer.size();
//更新多余的数据
m_buffer_car = buffer;
}
}
通过上面的代码,我们可以看到,对于socket
接收的数据包,关键在于合理地进行拆包,其拆包过程可以总结如下图: