笔记:Qt C++建立子线程做一个socket TCP常连接通信

10 阅读4分钟

一、前言

        有时程序已经基本完善,但是发现需要加入一个TCP常连接去连接服务器获取所需数据和推送数据和日志,此时比较推荐的是新建一个线程去连接服务器,本篇文章将推荐一个方法,安全管理该线程以及线程所需资源,并且保证程序不中断的情况下,可以对服务器IP进行纠正并自动重连。

二、Qt 子线程TCP客户端建立方法

        直接使用Qt自带的QTcpSocket来创建客户端,直接在项目中添加模块然后在源文件添加头文件即可直接调用:

//.pro文件中添加
QT += network

//头文件
#include <QTcpSocket>

//调用时,例如此时服务器地址为172.0.0.1:12345:
QTcpSocket socket;
QString ip = "172.0.0.1";
int port = 12345;
socket.connectToHost(ip, port);//发起连接
socket.write("ping");//写数据 
socket.readAll();//读取数据
socket.state();//连接状态
socket.waitForBytesWritten(1000);//写超时ms
socket.waitForReadyRead(1000);//读超时ms

        子线程使用QThread建立,我们使用创建工作管理类移入线程的方式来让线程工作,这样可以更安全的管理线程资源,避免资源泄漏问题:

//.pro文件
QT += core
//头文件
#include <QThread> 

//调用时
QThread *mTcpThread;
TcpWorker *mWorker;//TcpWorker 需要继承 QObject
mTcpThread = new QThread(this);
mWorker = new TcpWorker;
mWorker->moveToThread(mTcpThread);//将工作移入到线程内部
connect(mTcpThread, &QThread::started, mWorker, &mWorker::process);//process为工作函数,循环或非循环执行任务
connect(mTcpThread, &QThread::finished, mWorker, &QObject::deleteLater);//回收绑定,线程结束自动回收资源
mTcpThread->start();//开始工作,开始执行前面绑定的函数process(该函数里来建立TCP连接)

//析构函数中添加回收
mWorker->stop();//循环执行任务时需要设置退出标志来结束循环,封装stop来设置
mTcpThread->quit();
mTcpThread->wait();

三、实际使用实例

        前面已经给了线程的创建了,我这里直接给出TcpWorker 的代码,调用时结合前面子线程的代码。

tcpworker.h:

#pragma once

#include <QObject>
#include <QTcpSocket>
#include <atomic>

class TcpWorker : public QObject
{
    Q_OBJECT
public:
    explicit TcpWorker(QObject *parent = nullptr);

public slots:
    void process();   // 线程主函数
    void stop();

private:
    std::atomic_bool m_exit{false};//循环结束标志,bool类型原子变量比线程锁更方便
};

tcpworker.cpp:

#include "tcpworker.h"
#include "constant.h"

#include <QThread>
#include <QDebug>
#include <QSettings>


QStringList mPushData;//将需要推送给服务器的内容定义为公共资源
QMutex mPushMutex;//对应的线程锁保证安全

TcpWorker::TcpWorker(QObject *parent)
    : QObject(parent)
{
}

void TcpWorker::process()
{
    //外层:连接-重连循环
    while (!m_exit.load()) {
        QString ip;
        int port;
        {//读取本地配置 放在大循环里面 离线后配置文件更新即可同步更新
            QSettings ip_port("./netty.ini", QSettings::IniFormat);
            if (!ip_port.contains("ip") || !ip_port.contains("port")) {
                ip_port.setValue("ip", "127.0.0.1");
                ip_port.setValue("port", 24689);
                ip_port.sync();
                qWarning() << "netty.ini not found, set default.";
            }

            ip   = ip_port.value("ip").toString();
            port = ip_port.value("port").toInt();
        }

        qDebug() << "baiyi_gps TCP target =>" << ip << ":" << port;

        QTcpSocket socket;
        bool socketConnectStatus = true;
        socket.connectToHost(ip, port);

        //初步判断是否连接上了
        if (!socket.waitForConnected(10000)) {
            socketConnectStatus = false;
        }

        //测试是否是假连接/半连接 -- 未完成
        if(socketConnectStatus){
            for (int i = 0; i < 11 && !m_exit.load(); ++i) {
                QThread::sleep(1);
            }

            quint64 ret = socket.write("ping");
            socket.waitForBytesWritten(1000);
            socket.flush();
            //解决写缓存不发送,关闭程序才一次性发送问题
            if (socket.waitForReadyRead(10)) {
                socket.readAll(); // 清空接收缓冲
            }
            QThread::sleep(1);

            qDebug() << socket.state() << ret;
            if (socket.state() != QAbstractSocket::ConnectedState) {//判断是否失败
                socketConnectStatus = false;
            }
        }

        //确认是否是真连接
        if(!socketConnectStatus){
            qWarning() << "baiyi_gps TCP connect failed, retry after 60s";

            //放到休眠前面=>休眠结束重连成功发送积攒的报文=>减少报文丢失率
            {//断线后 每5分钟检测超过300条数据就清理 防止缓存过多
                QMutexLocker locker(&mPushMutex);
                qDebug() << "mPushData size :" << mPushData.size();
                if (mPushData.size() > 300) {
                    mPushData.clear();
                    qWarning() << "mPushData size > 300, cleaned.";
                }
            }

            // 连接失败,暂时休眠
            for (int i = 0; i < 60 * 5 && !m_exit.load(); ++i) {
                QThread::sleep(1);
            }
            continue;
        }

        qDebug() << "baiyi_gps TCP connected";

        //发送循环
        while (!m_exit.load() && socket.state() == QAbstractSocket::ConnectedState) {

            QStringList dataToSend;
            int sendCount = 0;
            {
                QMutexLocker locker(&mPushMutex);
                //不直接清空,因为socket.state()需要write()/read()后才会更新
                dataToSend = mPushData;
            }

            if (!dataToSend.isEmpty()) {
                for (const QString &one : dataToSend) {
                    if (m_exit.load() || socket.state() != QAbstractSocket::ConnectedState) {
                        break;
                    }

//                    QByteArray payload = one.toUtf8();//字符串
                    QByteArray payload = QByteArray::fromHex(one.toLatin1());//HEX

                    socket.write(payload);
                    socket.flush();
                    //解决写缓存不发送,关闭程序才一次性发送问题
                    if (socket.waitForReadyRead(10)) {
                        socket.readAll(); // 清空接收缓冲
                    }

                    // 每条之间间隔 100ms(可被退出打断)
                    for (int i = 0; i < 100 && !m_exit.load(); ++i) {
                        QThread::msleep(1);
                    }

                    if (socket.state() != QAbstractSocket::ConnectedState) {//判断是否失败
                        qWarning() << "TCP write failed, treat as disconnected";
                        break;
                    }
                    sendCount++;
                }
                qDebug() << "baiyi_gps TCP send :" << sendCount;
                {
                    QMutexLocker locker(&mPushMutex);
                    if (sendCount > 0 && sendCount <= mPushData.size()) {
                        mPushData.erase(mPushData.begin(), mPushData.begin() + sendCount);
                    } else if (sendCount > mPushData.size()) {
                        // 如果n超过列表长度,直接清空列表
                        mPushData.clear();
                    }
                }
            }
            QThread::sleep(1);
        }

        qWarning() << "baiyi_gps TCP disconnected, reconnect...";
        socket.disconnectFromHost();
        socket.waitForDisconnected(1000);
    }

    qDebug() << "baiyi_gps TCP thread exit";
}

void TcpWorker::stop()
{
    m_exit.store(true);
}

四、其他

如果是需要推送大量数据,为节省开销增强性能可以选择使用QQueue来作为传递公共资源调用时:

QQueue<QByteArray> g_sendQueue;
QMutex g_sendMutex;
QWaitCondition g_sendCond;

//添加数据
void pushData(const QString &hex)
{
    QByteArray data = QByteArray::fromHex(hex.toLatin1());
    QMutexLocker locker(&g_sendMutex);
    g_sendQueue.enqueue(data);
    g_sendCond.wakeOne();  // 唤醒发送线程,在此之前线程是睡眠的状态
}

//取出数据
{
    QMutexLocker locker(&g_sendMutex);
    if (g_sendQueue.isEmpty()) {// 没数据就等待休眠,不会占 CPU
        g_sendCond.wait(&g_sendMutex, 1000); // 最多等 1 秒
        continue;
     }
    data = g_sendQueue.dequeue();// 醒一次只取一条
}

//缓存限流:
{
    QMutexLocker locker(&g_sendMutex);
    if (g_sendQueue.size() > 300) {
        g_sendQueue.clear();
        qWarning() << "send queue overflow, cleared";
    }
}