首页 > 编程语言 >基于QT的TCP传输拆包和组包算法

基于QT的TCP传输拆包和组包算法

时间:2022-12-23 19:04:49浏览次数:60  
标签:QByteArray QT int 数据 组包 TCP MsgPackage sig data

什么是TCP传输的粘包,以及为什么要拆包组包就不仔细叙述了,简而言之就是TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,数据的顺序和内容都是可靠的,但因为是机遇字节流传输的,所以一次send的数据有可能需要几次recv才能接收完全,或者几次send的内容,一次recv就全部接收了。

​知识扫盲​

分享一个基于QT的TCP传输的拆包组包算法,其实该算法也适用于Libevent、muduo等网络库,甚至WSAEventSelect模型(只要是非阻塞的基于事件驱动的模型都可以)。

可能存在BUG,我会定时纠正,也欢迎留言指出问题。

#ifndef MSGPACKAGE_H
#define MSGPACKAGE_H

// 文件名:msgpackage.h
// 类名:MsgPackage
// 该类主要为网络通讯的封包协议,类代表一个封包的完整数据
// 2022/12/18
//
// 包的格式为:
// {
// QByteArray sig; // [4字节] 头标志
// quint8 checkSum; // [1字节] 校验和(将完整包带入校验)
// quint32 len; // [4字节] pkgData长度
// QByteArray pkgData; // [n字节] 包含的数据内容
// }


#include <QObject>
#include <QByteArray>

#define MAX_DATA_LEN 5 * 1024 * 8 //数据长度阈值为5M

class MsgPackage : public QObject
{
Q_OBJECT
public:
MsgPackage();

// 重载构造函数,可控制是否开启校验和
MsgPackage(bool checksum);

// 获取固定的协议包头长度
int headerLength();

// 清理包头和包内容数据
void clearPkg();

// 拆包函数
void unPkg(QByteArray data);

// 组包函数
QByteArray mkPkg(QByteArray data);

signals:

// 自定义信号,用于通知调用者有完整数据包,调用者需要使用connect绑定到自己的槽函数
void sigPkgReady(QByteArray data);

private:

// 内部函数,查找包头的位置,未找到返回-1
int seekHeader(QByteArray data, int from = 0);

// 内部函数,int转QByteArray
QByteArray intToBytes(int i);

// 内部函数,QByteArray转int
quint32 bytesToInt(QByteArray bytes);

// 内部函数,计算校验和
quint8 getCheckSum(QByteArray data);

// 内部函数,检测包的状态
int checkWhatToDo();

// 组成完整的包后调用该函数
void pkgReady(QByteArray data);
private:
bool isCheckSum; //是否验证校验和
QByteArray sig; //头标志
quint32 len; //数据长度
quint8 checksum; //校验和
QByteArray pkgData; //包数据内容
QByteArray tmpPkgData; //临时数据,用拼接上一次的包数据,进入下一次循环迭代处理
};

#endif // MSGPACKAGE_H
// 文件名:msgpackage.cpp
// create by yiyefangzhou24
// 2022/12/22

#include "msgpackage.h"
#include "crcchecksum.h"
#include <QString>
#include <QDebug>

MsgPackage::MsgPackage()
{
// 预设头标志和长度
sig.resize(4);
sig[0] = 0xff;
sig[1] = 0xfd;
sig[2] = 0xfe;
sig[3] = 0xff;
len = 0;
checksum = 0;
isCheckSum = false;
}

MsgPackage::MsgPackage(bool c)
{
// 预设头标志和长度
sig.resize(4);
sig[0] = 0xff;
sig[1] = 0xfd;
sig[2] = 0xfe;
sig[3] = 0xff;
len = 0;
checksum = 0;
isCheckSum = c;
}

// 返回包头长度
int MsgPackage::headerLength()
{
return sig.length() + sizeof (quint8) + sizeof (uint);
}

// 清空数据包
void MsgPackage::clearPkg()
{
pkgData.clear();
tmpPkgData.clear();
len = 0;
checksum = 0;
}

// 封装数据包
QByteArray MsgPackage::mkPkg(QByteArray data)
{
// 首先将校验和置0
quint8 ckSum = 0;
// 计算数据长度
quint32 dataLen = data.length();
// 组包
QByteArray resData;
resData.append(sig);
resData.append(ckSum);
resData.append(intToBytes(dataLen));
resData.append(data);
// 重新计算校验和并写入对应位置
ckSum = crcCheckSum::Crc8(resData);
resData[4] = ckSum;
return resData;
}

// 拆封数据包
void MsgPackage::unPkg(QByteArray data)
{
switch(checkWhatToDo())
{
case 1: // 等待接收新的包
{
// 寻找包头标志位置
int index = seekHeader(data);
// 舍弃包头标志前面的脏数据
if(index >= 0)
{
data = data.right(data.length() - index);
// 如果包头标志后的数据长度大于包头长度,则下一步处理
if(data.length() > headerLength())
{
// 获取包数据长度
len = bytesToInt(data.mid(sig.length() + sizeof (quint8), sizeof (quint32)));
// 获取包校验和
checksum = data[sig.length()];
qDebug() << "接收到包长度:"<< len << "校验和:" << checksum ;
// 剩下的数据迭代自身,交给case3处理
if(len > 0)
{
data = data.right(data.length() - headerLength());
unPkg(data);
}
else
{
clearPkg();
}
}
// 否则将包头标志后的数据存入临时数据等待下一次接收数据后拼接处理
else
{
tmpPkgData = data;
}
}
// 如果找不到可能是包头数据不完整,存入临时数据等待下一次接收数据后拼接处理
else
{
tmpPkgData = data;
}
break;
}
case 2: // 等待接收剩余的包数据
{
QByteArray mergeData = tmpPkgData + data;
clearPkg();
// 这里要设立一个阈值,防止一直脏数据导致mergeData过大导致内存崩溃
// 如果mergeData超过阈值MAX_DATA_LEN,则直接清空包并退出循环
if(mergeData.length() < MAX_DATA_LEN)
{ unPkg(mergeData); }
else
{ qDebug() << "【错误】脏数据超过阈值"; }
break;
}
case 3: // 等待接受剩余的包内容数据
{
// 包剩余未接收的数据长度
int surplusLen = len - pkgData.length();
// 如果剩余包内容数据长度大于当前数据长度,直接写入
if(surplusLen > data.length())
{
pkgData.append(data);
}
// 如果小于等于当前数据长度,则先写入当前包的剩余数据并打包好后发送ready信号
// 然后将剩余数据迭代自身进行进一步处理
else
{
pkgData.append(data.left(surplusLen));
pkgReady(pkgData);
clearPkg();

data = data.right(data.length() - surplusLen);
unPkg(data);
}
break;
}
default:
{
qDebug() << "【错误】未知的封包情况";
}
}
}

// *************************** 以下为类私有函数 ***************************

// 将int数据转化为QByteArray类型
QByteArray MsgPackage::intToBytes(int i)
{
QByteArray abyte0;
abyte0.resize(4);
abyte0[0] = (uchar) (0x000000ff & i);
abyte0[1] = (uchar) ((0x0000ff00 & i) >> 8);
abyte0[2] = (uchar) ((0x00ff0000 & i) >> 16);
abyte0[3] = (uchar) ((0xff000000 & i) >> 24);
return abyte0;
}

// 将QByteArray类型数据转化为int
quint32 MsgPackage::bytesToInt(QByteArray bytes) {
if(bytes.length() < 4)
return 0;
int addr = bytes[0] & 0x000000FF;
addr |= ((bytes[1] << 8) & 0x0000FF00);
addr |= ((bytes[2] << 16) & 0x00FF0000);
addr |= ((bytes[3] << 24) & 0xFF000000);
return addr;
}


// 在QByteArray数据中寻找包头的位置
int MsgPackage::seekHeader(QByteArray data, int from)
{
int index = data.indexOf(sig, from);
qDebug() << "包头位置:" <<index;
return index;
}

// 计算数据的checksum
quint8 MsgPackage::getCheckSum(QByteArray data)
{
// 首先将校验和置0
quint8 ckSum = 0;
// 计算数据长度
quint32 dataLen = data.length();
// 组包
QByteArray resData;
resData.append(sig);
resData.append(ckSum);
resData.append(intToBytes(dataLen));
resData.append(data);
// 重新计算校验和
ckSum = crcCheckSum::Crc8(resData);
return ckSum;
}

// 检测目前包的状态
// 检测len,pkgData,checksum,tmpHeaderData状态,判断是应该接收包头开始的数据还是包剩余数据
int MsgPackage::checkWhatToDo()
{
if(len == 0 && tmpPkgData.isEmpty())
{ return 1; } // 等待接收新的包
else if(len == 0 && !tmpPkgData.isEmpty())
{ return 2; } // 等待接收剩余的包数据
else if(len > 0 && tmpPkgData.isEmpty())
{ return 3; } // 等待接受剩余的包内容数据
}

// 拆出完整数据包后调用该函数
void MsgPackage::pkgReady(QByteArray data)
{
// 是否开启校验位
if(isCheckSum)
{
// 验证校验和
quint8 cksum = getCheckSum(data);
if(cksum == checksum)
{
//qDebug() << "【完整数据包】大小: " << data.length() << " 内容: " << data.data();
// 发送自定义信号给调用者的槽函数,通知调用者一次完整数据包已生成
emit this->sigPkgReady(data);
}
else
{
qDebug() << "【错误数据包】包校验和错误" << cksum << checksum;
}
}
else
{
//qDebug() << "【完整数据包】大小: " << data.length() << " 内容: " << data.data();
// 发送自定义信号给调用者的槽函数,通知调用者一次完整数据包已生成
emit this->sigPkgReady(data);
}
}

使用起来很简单,首先需要绑定该类的sigPkgReady信号的槽函数,用于响应组成好一个完整封包的信号。

// 绑定信号和槽函数
connect(&recvPkg, &MsgPackage::sigPkgReady, this, &MainWindow::pkgReady);

// 对话框的槽函数
void MainWindow::pkgReady(QByteArray data)
{
qDebug() << "【完整数据包】大小: " << data.length() << " 内容: " << data.data();
}

然后是发送数据

// 伪代码
MsgPackage sendPkg;
QByteArray pkg1("123456789");
QByteArray data1 = sendPkg.mkPkg(pkg1);
QTcpSocket->write(data1);

接收只需要在QTcpSocket的QTcpSocket::readyRead信号的槽函数中这样写即可

// 伪代码
MsgPackage.unPkg(QTcpSocket->readAll());

标签:QByteArray,QT,int,数据,组包,TCP,MsgPackage,sig,data
From: https://blog.51cto.com/yiyefangzhou24/5963322

相关文章

  • QT常识
    1QWidget类有什么用途? 在Qt应用程序中,QWidget用做工作空间.它是底层窗口,在其上可以放置其他对象,如按钮等.QWidget是Qt中所有widget部件(比如QDialog、QPushButton......
  • Qt代码实现上下左右布局
    //实现在上下左右四个方向分别放置按钮#include<QtWidgets>#include"arrowpad.h"ArrowPad::ArrowPad(QWidget*parent):QWidget(parent){upButton=newQPushBu......
  • QT实现CAN通信
    #include"connectdialog.h"#include"ui_connectdialog.h"#include<QCanBus>ConnectDialog::ConnectDialog(QWidget*parent):QDialog(parent),m_ui(newUi::Con......
  • tcp编程
    面试官:描述一下TCP的编程流程?答:TCP服务器端编程的一般步骤是:TCP客户端编程的一般步骤是:1.创建一个socket,用函数socket();......
  • qt中的一些对话框(个人备忘录)
    一、标准对话框   1.对于颜色对话框voidMyWidget::on_pushButton_clicked(){QColorDialogdialog(Qt::red,this);dialog.setOption(QColorDialog::Show......
  • QT QCustomPlot 中用参考线标示曲线的值
    https://blog.csdn.net/qq_37006532/article/details/127537965一、前言嵌入式触摸系统主要介绍qt用QCustomPlot实现曲线绘制后,点击屏幕,在曲线上显示点击位置的曲线值......
  • QT组件说明
    QtCore:核心模块,提供了其它模块所必需的数据类型和非图像类;QtGUI:图形用户界面(GUI)组件的基类;QtQuick(QML):用于构建具有自定义用户界面的高动态应用程序的声明......
  • TCP 链接关闭 -- 客户端为什么需要60秒的time_wait状态
    客户端主动关闭连接时(FIN->ACK<-FIN<-ACK->),在发送最后一个ack后会进入TIME_WAIT状态,停留2个MSL时间,进入CLOSED状态MSL就是maximumsegmentlifetime(最大分节生命......
  • TCP套接字
    TCP服务器端编程一般步骤:创建socket,用函数socket();设置socket属性,用函数setsocket();绑定IP地址、端口等信息到socket上,用函数bind();开启监听,用函数listen();接收从客户端来的......
  • Qt程序打包成单独exe的方法
    (1)QT编译kit安装路径:c:\Qt\6.4.0\mingw_64\(2)打开命令行工具cmd,使用windeployqt对生成的exe文件进行打配置动态库文件:  c:\Qt\6.4.0\mingw_64\bin\windeployqt.......