首先说前提:
对收发数据双方事先定义一个数据包头(包头格式可以自定,但是必须包含整个报文长度),发送数据格式为:数据包头+数据;
思路:
Qt自身封装的readyRead作为接收网络数据接口,可以关联一个槽函数,每次接收到网络数据就会响应此槽函数,对数据进行拆包在这个槽函数中进行;
connect(Socket, &QTcpSocket::readyRead, this, &TcpClient::ReceiveData);
然后就是最重要的拆包环节,事先需要定义两个缓冲变量,一个是QByteArray类型的,主要用于存放拆包完成的整包数据,并在emit之后清空。另一个是unsigned long类型,用于存放数据包头里面的报文总长度值。
核心方法就是利用QTcpSocket::read();它可以从tcp内部缓冲区中以固定字节读取数据,这样就能按照一开始获取到的报文总长度来接收数据存进事先定义好的缓冲变量,这样就避免了使用QTcpSocket::readAll()进而导致数据难以拆包问题的出现(按字节接收的话再不济也能单个字节进行,能够确保不会多读导致后续无法处理)。
附上完整拆包算法的代码:
void TcpClient::ReceiveData()
{
if (NULL == LinkedRevBuf.size())//如果数据包为空,则从头开始接受数据
{
loop: data = Socket->read(sizeof(NetData_Head));
if (data.size() < sizeof(NetData_Head))//数据包头不完整,需要再接收数据
{
LinkedRevBuf.append(data);
data.clear();
return;
}
NetData_Head stNetData_Head;
memcpy(&stNetData_Head, data, sizeof(NetData_Head));
ulCurrentBufLen = stNetData_Head.lLength;//保存当前包长度,方便为下一次拼包做准备
LinkedRevBuf.append(data);
data.clear();
data = Socket->read(stNetData_Head.lLength - sizeof(NetData_Head));
LinkedRevBuf.append(data);
if (stNetData_Head.lLength == LinkedRevBuf.size())//接收完一整包
{
emit SendNetData(LinkedRevBuf);//向主窗口发射数据
LinkedRevBuf.clear();
data.clear();
ulCurrentBufLen = 0;
goto loop; //接收完整包后从头开始接收
}
if (stNetData_Head.lLength < LinkedRevBuf.size()) //未接收完一整包
{
data = Socket->read(stNetData_Head.lLength - LinkedRevBuf.size());
LinkedRevBuf.append(data);
if (stNetData_Head.lLength - LinkedRevBuf.size() < data.size())//未获取到则缓冲区已经接收完毕,此时需要重新接收新的网络数据
{
data.clear();
return;
}
}
}
else//数据包不为空则说明有残留数据,此时需要将新的网络数据拼到数据包内
{
if (LinkedRevBuf.size() < sizeof(NetData_Head)) //上一包数据头未接收完整
{
data = Socket->read(sizeof(NetData_Head) - LinkedRevBuf.size());
LinkedRevBuf.append(data);
data.clear();
if (LinkedRevBuf.size() < sizeof(NetData_Head)) //若数据包头还是不完整,则再次重新接收
{
return;
}
else //数据包头完整
{
NetData_Head stNetData_Head;
memcpy(&stNetData_Head, LinkedRevBuf, sizeof(NetData_Head));
ulCurrentBufLen = stNetData_Head.lLength;
return;
}
}
else //数据头完整的数据包
{
data = Socket->read(ulCurrentBufLen - LinkedRevBuf.size()); //首先拆出前一包不够的数据,然后再处理剩余数据
LinkedRevBuf.append(data);
if (ulCurrentBufLen == LinkedRevBuf.size())//接收完一整包
{
emit SendNetData(LinkedRevBuf);//向主窗口发射数据
LinkedRevBuf.clear();
data.clear();
ulCurrentBufLen = 0;
goto loop;
}
}
}
}
关于TCP粘包和拆包的问题也困绕了我挺久的,也看过很多博客的一些方法和思路,但是都没有提到过用固定字节读取的方法(我也是敲代码的时候偶然间发现的)。思路和逻辑其实并不难,无非就是多判断几次数据长度,当长度够了就发射数据,然后清空重来,如此往复。
作为一个刚入门C++不到一年的小白,平时都是跟着导师的项目走,代码水平其实不高,但是能自己解决这个问题已经很欣慰了,这个小算法已经经过多次测试,基本没啥问题,如果有写的不合理的地方或者大佬还有更好的算法也欢迎留言或者私信。