概述
近來(lái)一直接使用WinSocket做網(wǎng)絡(luò)編程,有很長(zhǎng)一段時(shí)間不再使用Qt框架下的相關(guān)網(wǎng)路通信類。有不少之前積壓的問(wèn)題直到現(xiàn)在也沒(méi)怎么弄清楚,在CSDN中亂七八糟的存了好幾篇草稿,亟待整理。最近要寫(xiě)一個(gè)簡(jiǎn)單地相機(jī)升級(jí)程序,于是重操舊業(yè)。
歷史
網(wǎng)絡(luò)通信中,尤其是在收發(fā)工作較為耗時(shí)或交互頻率較高的時(shí)候,為了使得通信過(guò)程不造成UI的卡頓現(xiàn)象,一般要求通信工作在次線程(子線程)中完成。在Windows編程中,我們可以使用Select模式等實(shí)現(xiàn)這一需求。在Qt網(wǎng)絡(luò)編程框架下,也做過(guò)些嘗試。如 《網(wǎng)絡(luò)通信/QTcpSocket/QObject:Cannot create children for a parent that is in a different thread.》 文章中提到的方案(臨時(shí)記為PlanA),它將Qt套接字對(duì)象移動(dòng)到次線程,并在主線程中直接調(diào)用套接字接口,此時(shí)存在 “以其他線程對(duì)象為父對(duì)象,在本線程創(chuàng)建子對(duì)象” 的告警。
草稿中還記錄了另一個(gè)方案(臨時(shí)記為PlanB)
如常見(jiàn)的Qt多線程編程,定義一個(gè)workker類對(duì)象,將其移動(dòng)到次線程中,由其全權(quán)負(fù)責(zé)對(duì)m_socket套接字對(duì)象的操作,包括使用套接字進(jìn)行連接、斷開(kāi)、數(shù)據(jù)發(fā)送等操作。此方案依然存在PlanA中的問(wèn)題,因?yàn)榇藭r(shí)套接字對(duì)象沒(méi)有進(jìn)行過(guò)moveToThread操作,其還是歸屬于創(chuàng)建它的主線程,但相關(guān)函數(shù)調(diào)用線程卻為wirker所在的次線程。
通過(guò)分析以前的失敗經(jīng)驗(yàn),似乎得出了一個(gè)結(jié)論:
套接字的相關(guān)接口只能在套接字對(duì)象所屬的線程內(nèi)調(diào)用(如果套接字對(duì)象沒(méi)有執(zhí)行過(guò)moveToThread操作,那么套接字對(duì)象的所屬線程就是創(chuàng)建它的線程)。因此,如果想支持在次線程中執(zhí)行連接/斷開(kāi)服務(wù)、數(shù)據(jù)收/發(fā)過(guò)程,則必須的要將套接字對(duì)象本身進(jìn)行moveToThread操作,且要將其他線程對(duì)該對(duì)象的操作轉(zhuǎn)換到moveToThread后的線程內(nèi)。
一種可行的實(shí)現(xiàn)
如下方案實(shí)現(xiàn)了,發(fā)送和接收操作同時(shí)運(yùn)行在一個(gè)次線程內(nèi)。其實(shí)通常情況下的交互過(guò)程,不會(huì)在同一時(shí)間段內(nèi)雙向高速通信,在一個(gè)時(shí)間段內(nèi)一般只有一方在高速發(fā)送數(shù)據(jù),或者多設(shè)備發(fā)送然后由集中控制設(shè)備接收處理。因此像WinSocket編程Select模式下實(shí)現(xiàn)在一個(gè)線程內(nèi)執(zhí)行收發(fā)操作,是很常見(jiàn)的方案。需要注意的是,速率要求更高的場(chǎng)景,從本質(zhì)就不適合使用Qt的網(wǎng)絡(luò)通信封裝。
//.h
#pragma once
#include <QTcpSocket>
//該對(duì)象最終運(yùn)行在次線程中
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:
//轉(zhuǎn)換來(lái)自主線程的鏈接操作
void SignalConnectToHost(const QString & address, quint16 port);
signals:
//轉(zhuǎn)換來(lái)自主線程的發(fā)送操作
void SignalSendingData(const QByteArray c_btaData);
signals:
//在次線程中緩沖并滑動(dòng)解析TCP流后/按約定格式再發(fā)布
void SignalPublishFormatRecvData(const QString c_btaData);
private:
//標(biāo)記連接情況
bool m_bOnLine = false;
//緩沖收到的流數(shù)據(jù)
QByteArray m_btaReceiveFromService;
};
//.cpp
#include <QThread>
#include <QDebug>
#include <QHostAddress>
#include "tcp_client.h"
TcpClient::TcpClient(QObject *parent)
: QTcpSocket(parent)
{
//自動(dòng)連接在信號(hào)發(fā)射時(shí)被識(shí)別為隊(duì)列連接/信號(hào)在主線程發(fā)射
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服務(wù)端
QObject::connect(this, &QAbstractSocket::connected, this, [&]() {
//test record# in child thread id 20588
qDebug("SlotHasConnected ThreadID:%d", QThread::currentThreadId());
//
m_bOnLine = true;
}, Qt::DirectConnection);
//斷開(kāi)了TCP服務(wù)端
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服務(wù)的數(shù)據(jù)
QObject::connect(this, &QIODevice::readyRead, this, [&]() {
//test record# in child thread id 20588
qDebug("SlotIODeviceReadyRead ThreadID:%d", QThread::currentThreadId());
//讀取全部數(shù)據(jù)
m_btaReceiveFromService.append(this->readAll());
//
int iFindPos = m_btaReceiveFromService.indexOf("\r\n");
//檢查分隔符
while (-1 != iFindPos)
{
//分割數(shù)據(jù)流
QString strPublish = m_btaReceiveFromService.left(iFindPos);
//發(fā)布解析后的格式數(shù)據(jù)
emit SignalPublishFormatRecvData(strPublish);
//
m_btaReceiveFromService.remove(0, iFindPos + strlen("\r\n"));
//
iFindPos = m_btaReceiveFromService.indexOf("\r\n");
}
}, Qt::DirectConnection);
//執(zhí)行數(shù)據(jù)發(fā)送過(guò)程
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()
{
}
//跨線程轉(zhuǎn)換
void TcpClient::ClientConnectToHost(const QString & address, quint16 port)
{
emit SignalConnectToHost(address, port);
}
//跨線程轉(zhuǎn)換
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)
{
//創(chuàng)建TCP客戶端
m_pmyTcpSocket = new TcpClient();
//
m_pThreadSending = new QThread();
//
m_pmyTcpSocket->moveToThread(m_pThreadSending);
//
m_pThreadSending->start();
//連接到相機(jī)的TCP服務(wù)
connect(ui.pushButton_connect, &QPushButton::clicked, [&]() {
...
m_pmyTcpSocket->ClientConnectToHost(strIPUsing, SER_PORT);
});
//文件發(fā)送
connect(ui.pushButton_file_sending, &QPushButton::clicked, [&]() {
...
//執(zhí)行客戶端文件發(fā)送過(guò)程
m_pmyTcpSocket->ClientSendingData(DataOfBin);
});
//接收服務(wù)端發(fā)送的數(shù)據(jù) /從子線程到主線程的隊(duì)列連接
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);
}
一些總結(jié)
在實(shí)現(xiàn)和測(cè)試上述TCP客戶端的過(guò)程中,也驗(yàn)證和消除了一些 “老問(wèn)題”。
1、由QIODevice::readyRead信號(hào)的DirectConnection連接的lambda槽函數(shù)執(zhí)行結(jié)果,可得出:如果一個(gè)Tcp對(duì)象被歸屬到了子線程X中,那么readyRead信號(hào)最終將從此子線程X發(fā)出。
2.、同上,connected信號(hào)、disconnected信號(hào)等其發(fā)射線程,都是套接字對(duì)象的所在線程。
其他需要注意的是:
1、當(dāng)connect內(nèi)部使用lambda表達(dá)式做槽函數(shù)時(shí),注意選擇有Qt::ConnectionType 參數(shù)的那個(gè)函數(shù)版本,否則將默認(rèn)為直接連接。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-479975.html
//默認(rèn)為直接連接
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、默認(rèn)的連接方式 Qt::AutoConnection 在connect后生效的時(shí)刻是emit發(fā)射的時(shí)候,而不是執(zhí)行connect 語(yǔ)句的時(shí)候。因此先執(zhí)行moveToThread還是先執(zhí)行connect過(guò)程是無(wú)關(guān)緊要的。具體可參見(jiàn)幫助文檔中提及的: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、至此,還沒(méi)有讀過(guò)Qt網(wǎng)絡(luò)通信框架的源碼,因此對(duì)于如下問(wèn)題,還是無(wú)法清晰理解,如:Qt是如何對(duì)WinSocket進(jìn)行封裝的,Qt網(wǎng)絡(luò)通信采用了哪種IO模型,QIODevice的架構(gòu)是怎樣的,readyRead信號(hào)是在什么情景下發(fā)出的,QThread線程是如何對(duì)接QIODevice上的?同一個(gè)Qt套接字對(duì)象到底能否在兩個(gè)不同的子線程中進(jìn)行工作?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-479975.html
到了這里,關(guān)于網(wǎng)絡(luò)通信/QTcpSocket/實(shí)現(xiàn)一個(gè)可在子線程中發(fā)送和接收數(shù)據(jù)的TCP客戶端的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!