近来一直接使用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套接字对象到底能否在两个不同的子线程中进行工作?