QT下多線程調用TCP的問題及可能的解決方案


背景:在上一篇博文https://www.cnblogs.com/yuanwebpage/p/12638001.html中結尾時,提到QT下所有IO類都不允許跨線程調用,這極大增加了編程難度。本文接着上一篇,主要討論當套接字與UI線程不在同一線程時,如何使TCP的收發實時

1. 能否跨線程調用TCP套接字?

  對於TCP通信,一個常見的操作就是讀寫分開,即讀寫分別在不同線程中執行,這樣實現實時全雙工通信,那么在QT中能否實現讀寫線程分開呢?理論上將是不可以的,但是實際操作發現能實現(會有錯誤警告)。

  這涉及到信號和槽的連接方式。通常QT的信號和槽有三種常用的連接方式:

(1) Qt::AutoConnection:QT默認連接方式。當信號接收方與信號發送方在同一線程時,等價於Qt::DirectConnection;否則等價於Qt::QueuedConnection。

(2) Qt::DirectConnection: 當信號被發送后,槽函數立即執行。這對於實時通信意義重大,如UI界面發送TCP消息之后,需要實時等待TCP響應以進行不同的操作,這種連接方式就能保證TCP能立即發送消息。

(3) Qt::QueuedConnection: 當信號發送時,對應的槽函數加入到槽函數所在線程事件處理隊列中,等待執行。

對於Qt::DirectConnection連接,有一個默認屬性:當信號和槽不在同一個線程時,槽函數不會在槽函數所在線程執行,而是會在在信號發送的線程執行(具體細節見QT官方文檔)。這就為不同線程調用TCP套接字提供了可能,以上一篇(鏈接見開題頭)的程序為基礎,又添加了TCP發送功能,點擊按鈕,需要將對應消息實時發送出去,界面如下:

                   

  其他部分不變,在上一篇文章的基礎上添加了輸入框和發送按鈕。由於TCP所在線程主循環一直調用TCP接收消息,所以點擊按鈕發送消息時,子線程一直忙碌,並不會響應,所以此時我希望UI線程來發送消息。對上一篇文章TcpMoveToThread類構造函數稍加修改:

 1 TcpMoveToThread::TcpMoveToThread(QObject* parent)
 2 {
 3     m_tcp.moveToThread(&m_thread); //加入到子線程
 4 
 5     connect(&m_thread,&QThread::started,&m_tcp,&TcpModel::tcpWork); //一旦線程開始,就調用接收Tcp的函數
 6     connect(&m_tcp,&TcpModel::dataRecved,this,&TcpMoveToThread::dataChangedSlot);
 7     connect(&m_thread,&QThread::finished,&m_tcp,&TcpModel::tcpClose); //線程結束時關閉socket,刪除申請內存
 8 
 9     //直接連接槽函數
10     connect(this,&TcpMoveToThread::senddataSignal,&m_tcp,&TcpModel::tcpSendMsgSlot,Qt::DirectConnection);
11 
12     m_thread.start(); //開啟子線程
13 }

紅色部分是修改部分,即采用了Qt::DirectConnection。由於信號發送方在UI線程,接收方在TCP子線程,所以此時調用的槽函數不會在子線程中執行,而是直接在UI線程執行,這樣收發在不同線程,都能實時響應。這種做法雖然沒有影響收發效果,但是每次會提示Socket notifiers cannot be enabled or disabled from another thread,從提示結果就能看出這是跨線程調用TCP造成的。這種提示的后果未知(因為程序仍能正常運行)。

2. 如何優化設計

  雖然1所介紹的方案能實現功能,但是畢竟出現錯誤提示,不知道會造成何種后果。所以最好還是不采用以上方式。那么對於TCP的收發,如何做到實時響應呢?

  造成1中的程序原因是在TCP子線程一直循環接收TCP數據,這種操作是非常不明智的。為了讓TCP子線程空閑,只在收發數據時運行,可以將TCP接收也改為信號和槽的形式。TCP收到消息時,會發送信號QTcpSocket::readyRead,通過該信號與TCP接收綁定,就不用在子線程中循環調用TCP接收函數了。改造后的tcpmodel如下:

  1 //tcpmodel.h
  2 
  3 #ifndef TCPMODEL_H
  4 #define TCPMODEL_H
  5 #include <QTcpSocket>
  6 #include <QThread>
  7 #include <QTimer>
  8 
  9 class TcpModel:public QObject
 10 {
 11     Q_OBJECT
 12 public:
 13     TcpModel(QObject* parent=nullptr);
 14 
 15     QString sendmsg;
 16 
 17 signals:
 18     void dataRecved(QString data);  //通知TcpMoveToThread類,數據接收
 19 
 20 public slots:
 21     void tcpWork();
 22     void tcpClose();
 23     void tcpSendMsgSlot(QString msg);
 24     void tcpRecvSlot();  //接收消息的槽函數
 25 
 26 
 27 private:
 28     QTcpSocket* m_socket;
 29     QString msg;
 30 };
 31 
 32 //將TcpModel在QML初始化時移入到子線程
 33 class TcpMoveToThread: public QObject
 34 {
 35     Q_OBJECT
 36     Q_PROPERTY(QString m_data MEMBER m_data)
 37 public:
 38     TcpMoveToThread(QObject* parent=nullptr);
 39     ~TcpMoveToThread();
 40 
 41 signals:
 42     void dataChanged();   //用於通知QML應用,數據接收到
 43     void senddataSignal(QString msg);  //發送數據的信號
 44 
 45 
 46 public slots:
 47     void dataChangedSlot(QString msg);
 48 
 49 
 50 private:
 51     QThread m_thread;
 52     TcpModel m_tcp;
 53     QString m_data;  //保存接收數據
 54 };
 55 
 56 #endif // TCPMODEL_H
 57 
 58 
 59 //tcpmodel.cpp
 60 #include "tcpmodel.h"
 61 #include <QObject>
 62 
 63 TcpModel::TcpModel(QObject* parent)
 64 {
 65 }
 66 
 67 void TcpModel::tcpWork()
 68 {
 69     m_socket=new QTcpSocket();
 70     m_socket->connectToHost("127.0.0.1",8000);
 71     connect(m_socket,&QTcpSocket::readyRead,this,&TcpModel::tcpRecvSlot); //連接TCP接收槽函數
 72 }
 73 
 74 void TcpModel::tcpClose()
 75 {
 76     m_socket->close();
 77     delete m_socket;
 78 }
 79 
 80 void TcpModel::tcpSendMsgSlot(QString msg)
 81 {
 82     m_socket->write(msg.toLocal8Bit(),msg.toLocal8Bit().length());
 83     m_socket->waitForBytesWritten();
 84 }
 85 
 86 void TcpModel::tcpRecvSlot()
 87 {
 88     if(m_socket->bytesAvailable()>0)
 89     {
 90         QByteArray res=m_socket->readAll();
 91         msg=QString::fromLocal8Bit(res.data());
 92         emit dataRecved(msg); //接收完成信號
 93     }
 94 }
 95 
 96 
 97 
 98 TcpMoveToThread::TcpMoveToThread(QObject* parent)
 99 {
100     m_tcp.moveToThread(&m_thread); //加入到子線程
101 
102     connect(&m_thread,&QThread::started,&m_tcp,&TcpModel::tcpWork); //一旦線程開始,就調用接收Tcp的函數
103     connect(&m_tcp,&TcpModel::dataRecved,this,&TcpMoveToThread::dataChangedSlot);
104     connect(&m_thread,&QThread::finished,&m_tcp,&TcpModel::tcpClose); //線程結束時關閉socket,刪除申請內存
105 
106     //直接連接槽函數,功能正常,但提示Socket notifiers cannot be enabled or disabled from another thread
107     connect(this,&TcpMoveToThread::senddataSignal,&m_tcp,&TcpModel::tcpSendMsgSlot);
108 
109     m_thread.start(); //開啟子線程
110 }
111 
112 TcpMoveToThread::~TcpMoveToThread()
113 {
114     m_thread.exit();
115     m_thread.wait();
116 }
117 
118 void TcpMoveToThread::dataChangedSlot(QString msg)
119 {
120     m_data=msg;
121     emit dataChanged();
122 }

主要改動都用紅色標出了,特別注意:

第83行:QT的TCP通信默認都是異步的,所以即時調用了write和read函數,也可能不會立即進行讀寫,表現在程序中就是能立即執行槽函數,但TCP收發有明顯延遲(可以將83注釋掉,再看現象)。所以為了將異步改為同步,QT的TCP規定了以下函數:

waitForConnected() 等待鏈接的建立
waitForReadyRead() 等待新數據的到來
waitForBytesWritten() 等待數據寫入socket
waitForDisconnected() 等待鏈接斷開

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM