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() 等待链接断开