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

 

posted @ 2020-04-08 21:31  晨枫1  阅读(7693)  评论(0编辑  收藏  举报