网络通信/QTcpSocket/实现一个可在子线程中发送和接收数据的TCP客户端

概述

近来一直接使用WinSocket做网络编程,有很长一段时间不再使用Qt框架下的相关网路通信类。有不少之前积压的问题直到现在也没怎么弄清楚,在CSDN中乱七八糟的存了好几篇草稿,亟待整理。最近要写一个简单地相机升级程序,于是重操旧业。

历史

网络通信中,尤其是在收发工作较为耗时或交互频率较高的时候,为了使得通信过程不造成UI的卡顿现象,一般要求通信工作在次线程(子线程)中完成。在Windows编程中,我们可以使用Select模式等实现这一需求。在Qt网络编程框架下,也做过些尝试。如 《网络通信/QTcpSocket/QObject:Cannot create children for a parent that is in a different thread.》 文章中提到的方案(临时记为PlanA),它将Qt套接字对象移动到次线程,并在主线程中直接调用套接字接口,此时存在 “以其他线程对象为父对象,在本线程创建子对象” 的告警。
草稿中还记录了另一个方案(临时记为PlanB)
如常见的Qt多线程编程,定义一个workker类对象,将其移动到次线程中,由其全权负责对m_socket套接字对象的操作,包括使用套接字进行连接、断开、数据发送等操作。此方案依然存在PlanA中的问题,因为此时套接字对象没有进行过moveToThread操作,其还是归属于创建它的主线程,但相关函数调用线程却为wirker所在的次线程。
通过分析以前的失败经验,似乎得出了一个结论:
套接字的相关接口只能在套接字对象所属的线程内调用(如果套接字对象没有执行过moveToThread操作,那么套接字对象的所属线程就是创建它的线程)。因此,如果想支持在次线程中执行连接/断开服务、数据收/发过程,则必须的要将套接字对象本身进行moveToThread操作,且要将其他线程对该对象的操作转换到moveToThread后的线程内。

一种可行的实现

如下方案实现了,发送和接收操作同时运行在一个次线程内。其实通常情况下的交互过程,不会在同一时间段内双向高速通信,在一个时间段内一般只有一方在高速发送数据,或者多设备发送然后由集中控制设备接收处理。因此像WinSocket编程Select模式下实现在一个线程内执行收发操作,是很常见的方案。需要注意的是,速率要求更高的场景,从本质就不适合使用Qt的网络通信封装。

//.h

#pragma once

#include 

//该对象最终运行在次线程中
class TcpClient : public QTcpSocket
{
    Q_OBJECT
public:
    TcpClient(QObject *parent = NULL);
    ~TcpClient();
public:
    //
    void ClientConnectToHost(const QString &address, quint16 port);
    //
    void ClientSendingData(const QByteArray &c_btaData);
    //
    bool IsOnline();
signals:
    //转换来自主线程的链接操作
    void SignalConnectToHost(const QString & address, quint16 port);
signals:
    //转换来自主线程的发送操作
    void SignalSendingData(const QByteArray c_btaData);
signals:
    //在次线程中缓冲并滑动解析TCP流后/按约定格式再发布
    void SignalPublishFormatRecvData(const QString c_btaData);
private:
    //标记连接情况
    bool m_bOnLine = false;
    //缓冲收到的流数据
    QByteArray m_btaReceiveFromService;
};

//.cpp

#include 
#include 
#include 
#include "tcp_client.h"

TcpClient::TcpClient(QObject *parent)
    : QTcpSocket(parent)
{
    //自动连接在信号发射时被识别为队列连接/信号在主线程发射
    connect(this, &TcpClient::SignalConnectToHost, this, [&](const QString & address, quint16 port) {
        //test record# in child thread id 20588
        qDebug("SlotConnectToHost ThreadID:%d", QThread::currentThreadId());
        //
        this->connectToHost(QHostAddress(address), port, QIODevice::ReadWrite);
    }, Qt::AutoConnection);

    //连接了TCP服务端
    QObject::connect(this, &QAbstractSocket::connected, this, [&]() {
        //test record# in child thread id 20588
        qDebug("SlotHasConnected ThreadID:%d", QThread::currentThreadId());
        //
        m_bOnLine = true;
    }, Qt::DirectConnection);

    //断开了TCP服务端
    QObject::connect(this, &QAbstractSocket::disconnected, this, [&]() {
        //test record# in child thread id 20588
        qDebug("SlotHasDisconnected ThreadID:%d", QThread::currentThreadId());
        //
        m_bOnLine = false;
    }, Qt::DirectConnection);

    //收到了TCP服务的数据
    QObject::connect(this, &QIODevice::readyRead, this, [&]() {
        //test record# in child thread id 20588
        qDebug("SlotIODeviceReadyRead ThreadID:%d", QThread::currentThreadId());
        //读取全部数据
        m_btaReceiveFromService.append(this->readAll());
        //
        int iFindPos = m_btaReceiveFromService.indexOf("\r\n");
        //检查分隔符
        while (-1 != iFindPos)
        {
            //分割数据流
            QString strPublish = m_btaReceiveFromService.left(iFindPos);
            //发布解析后的格式数据
            emit SignalPublishFormatRecvData(strPublish);
            //
            m_btaReceiveFromService.remove(0, iFindPos + strlen("\r\n"));
            //
            iFindPos = m_btaReceiveFromService.indexOf("\r\n");
        }
    }, Qt::DirectConnection);

    //执行数据发送过程
    QObject::connect(this, &TcpClient::SignalSendingData, this, [&](const QByteArray c_btaData) {
        //test record# in child thread id 20588
        qDebug("SlotSendingData ThreadID:%d", QThread::currentThreadId());
        //
        this->write(c_btaData);
    }, Qt::AutoConnection);
}

//
TcpClient::~TcpClient()
{
}

//跨线程转换
void TcpClient::ClientConnectToHost(const QString & address, quint16 port)
{
    emit SignalConnectToHost(address, port);
}

//跨线程转换
void TcpClient::ClientSendingData(const QByteArray & c_btaData)
{
    emit SignalSendingData(c_btaData);
}

//是否在线
bool TcpClient::IsOnline()
{
    return m_bOnLine;
}

//main /using of my tcp client

UpdateCamera::UpdateCamera(QWidget *parent) : QMainWindow(parent)
{
    //创建TCP客户端
    m_pmyTcpSocket = new TcpClient();
    //
    m_pThreadSending = new QThread();
    //
    m_pmyTcpSocket->moveToThread(m_pThreadSending);
    //
    m_pThreadSending->start();

    //连接到相机的TCP服务
    connect(ui.pushButton_connect, &QPushButton::clicked, [&]() {
    	...
        m_pmyTcpSocket->ClientConnectToHost(strIPUsing, SER_PORT);
    });
    
    //文件发送
    connect(ui.pushButton_file_sending, &QPushButton::clicked, [&]() {
  		...
        //执行客户端文件发送过程
        m_pmyTcpSocket->ClientSendingData(DataOfBin);
    });

    //接收服务端发送的数据 /从子线程到主线程的队列连接
    connect(m_pTcpClient, &TcpClient::SignalPublishFormatRecvData, this, [&](const QString c_btaData) {
        ui.textEdit->append(c_btaData);
        ui.textEdit->moveCursor(QTextCursor::End);
        if (ui.textEdit->toPlainText().size() > 2 * 1024 * 1024)
            ui.textEdit->clear();
    }, Qt::AutoConnection);
}

一些总结

在实现和测试上述TCP客户端的过程中,也验证和消除了一些 “老问题”。
1、由QIODevice::readyRead信号的DirectConnection连接的lambda槽函数执行结果,可得出:如果一个Tcp对象被归属到了子线程X中,那么readyRead信号最终将从此子线程X发出。
2.、同上,connected信号、disconnected信号等其发射线程,都是套接字对象的所在线程。

其他需要注意的是:
1、当connect内部使用lambda表达式做槽函数时,注意选择有Qt::ConnectionType 参数的那个函数版本,否则将默认为直接连接。

//默认为直接连接
connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
//可以配置连接方式 //Qt::UniqueConnections do not work for lambdas
connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type)

2、默认的连接方式 Qt::AutoConnection 在connect后生效的时刻是emit发射的时候,而不是执行connect 语句的时候。因此先执行moveToThread还是先执行connect过程是无关紧要的。具体可参见帮助文档中提及的:If the receiver lives in the thread that emits the signal, Qt::DirectConnection is used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the signal is emitted.
3、至此,还没有读过Qt网络通信框架的源码,因此对于如下问题,还是无法清晰理解,如:Qt是如何对WinSocket进行封装的,Qt网络通信采用了哪种IO模型,QIODevice的架构是怎样的,readyRead信号是在什么情景下发出的,QThread线程是如何对接QIODevice上的?同一个Qt套接字对象到底能否在两个不同的子线程中进行工作?

你可能感兴趣的:(通信设计与实现,C++/Qt,tcp/ip,Qt网络)