Bootstrap

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

什么是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);

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

#endif // MSGPACKAGE_H
// 文件名:msgpackage.cpp
// created by yiyfefangzhou24
// 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() << "【错误】未知的封包情况";
    }
    }
}

// 拆出完整数据包后调用该函数
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);
    }
}

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

// 将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; }       // 等待接受剩余的包内容数据
}

使用起来很简单,首先需要绑定该类的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());
;