背景:在上一篇博文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() 等待鏈接斷開