赞
踩
近来一直接使用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 <QTcpSocket> //该对象最终运行在次线程中 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 <QThread> #include <QDebug> #include <QHostAddress> #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套接字对象到底能否在两个不同的子线程中进行工作?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。