欢迎光临
我们一直在努力

Qt学习日记 Day14:TCP(二)粘包处理+心跳机制

## 今天学什么

今天学习Qt中TCP粘包处理方法-长度头的代码写法,以及心跳机制。

## TCP粘包处理(长度头代码实现)

定义:每个数据包开头加4字节uint32_t大端数字,代表后面正文的字节长度。

打包步骤:

  • 拿到消息字符串,转utf8得到QByteArray data;
  • 计算data长度,转为大端4字节;
  • 头 + 正文拼接后发送;
  • 拆包步骤:

  • 收到的数据追加到持久缓冲区buf;
  • 先判断buf长度≥4,读出头部长度;
  • 判断buf里剩下的数据够不够这个长度;
  •  够:取出完整包,删掉缓冲区已消费部分,交给业务处理;不够:停止,等待下一次数据;
  • ① 打包

    QByteArray packMsg(const QByteArray& data)
    {
        QByteArray res;
        uint32_t len = data.size();
        uint32_t netLen = qToBigEndian(len);
        res.append((char*)&netLen, 4);
        res.append(data);
        return res;
    }

    ② 拆包(在readyRead里循环解析)

    bool unpackMsg(QByteArray& buf, QByteArray& outMsg)  //不要用const
    {
        // 不足4字节,连长度头都收不到
        if(buf.size() < 4)
            return false;

        // 读取头部4字节,解析出正文长度
        uint32_t netLen = qFromBigEndian<uint32_t>(buf.data());
        // 整条包总长度 = 4字节头 + 正文长度
        uint32_t totalLen = netLen + 4;

        // 缓冲区数据不够一整条消息,直接返回
        if(buf.size() < totalLen)
            return false;

        // 截取正文,放到输出参数outMsg
        outMsg = buf.mid(4, netLen);
        // 删掉已经解析完的数据,剩余半包留在buf,下次继续解析
        buf = buf.mid(totalLen);

        return true;
    }

    ③ 使用

    socket->write(packMsg(msg));

    while(unpackMsg(*socbuf,m))
    {
          ui->textEdit->append(m);
    }

    ## 心跳机制

    定义:客户端定时向服务端发送专用小包,用来上报自己在线。

    作用:区分「客户端闲置没发消息」和「断网/程序崩溃失联」。

    工作逻辑:

    • 客户端定时发心跳包;
    • 服务端收到心跳就刷新存活计时;
    • 连续一段时间收不到心跳,判定失联,主动关闭连接。

    单个客户端使用示例:

    客户端步骤:

  • 新建QTimer,间隔3s;
  • 定时器超时:组装心跳文本send ok,调用packMsg打包发出;
  • connected信号触发:启动定时器(可选stop再start,防止多次绑定);
  • disconnected信号触发:停止定时器,缓冲区clear;(客户端构造函数里面new buf:全程复用这一块缓存)
  • 服务端步骤

  • 全局标记:有无客户端、lastHeartTime(毫秒时间戳);
  • 收到新连接:给soc赋值,立刻更新lastHeartTime,标记有客户端;
  • readyRead拆包,判断消息等于send ok:刷新lastHeartTime,回复get over;
  • 单独一个定时器(1s轮询):                                                                                                     无客户端直接return;                                                                                                               当前时间 – lastHeartTime >9000 → 超时,close连接,soc置空,标记无客户端;
  • 客户端断开回调:标记无客户端,对应连接的buf delete;(服务端是每触发一次newConnection,才new一个buf给这个新连接,连接断了就delete这个buf。)
  • 核心代码示例:

    ① 服务端

    // 头文件
    QTcpSocket *soc = nullptr;
    QTimer *heartCheckTimer;
    qint64 lastHeartTime = 0;
    bool hasClient = false;

    // 构造
    heartCheckTimer = new QTimer(this);
    heartCheckTimer->start(1000);

    // 心跳检测轮询
    connect(heartCheckTimer, &QTimer::timeout, this, [this](){
        if(!hasClient) return;
        auto now = QDateTime::currentMSecsSinceEpoch();
        if(now – lastHeartTime > 9000)
        {
            soc->close();
            soc = nullptr;
            hasClient = false;
            qDebug() << "心跳超时,断开";
        }
    });

    // 收到心跳包
    connect(soc, &QTcpSocket::readyRead, this, [this](){
        auto recv = soc->readAll();
        if(recv == "send ok")
        {
            lastHeartTime = QDateTime::currentMSecsSinceEpoch();
            soc->write("get over");
        }
    });

    // 新客户端接入时
    soc = 新拿到的socket;
    hasClient = true;
    lastHeartTime = QDateTime::currentMSecsSinceEpoch();

    ② 客户端

    // 头文件
    QTcpSocket *socket;
    QTimer *HeartTimer;

    // 构造函数
    HeartTimer = new QTimer(this);

    // 定时发心跳
    connect(HeartTimer, &QTimer::timeout, this, [this](){
        socket->write("send ok");
    });

    // 连上之后启动心跳定时器
    connect(socket, &QTcpSocket::connected, this, [this](){
        HeartTimer->start(3000);
    });

    // 断开就停心跳
    connect(socket, &QTcpSocket::disconnected, this, [this](){
        HeartTimer->stop();
    });

    ## 结合心跳机制和长度头方法改写昨天的代码

    代码问题复盘:

    ① 运行以后的换行问题:当输入长段中文+英文字母,会出现在英文衔接处换行问题;原因:Qt文本默认不在英文单词中间截断;当行尾剩余宽度放不下整串英文时,布局引擎选择在中英交界这个合法断行位置换行;暂时无法解决,跳过。

    ② 服务端心跳计时必须加客户端连接判断,不然运行定时器时间到了,没有监听和客户端连接直接闪退。

    ③ 心跳一直自动发包会占用很多内存吗?不会,数据很小。

    赞(0)
    未经允许不得转载:171主机测评 » Qt学习日记 Day14:TCP(二)粘包处理+心跳机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址