阻止财富客户端
演示如何为网络服务创建客户端。
QTcpSocket 支持网络编程的两种一般方法:
- 异步(非阻塞)方法。当控制返回到 Qt 的事件循环时,将安排并执行操作。操作完成后,QTcpSocket 发送信号。例如,QTcpSocket::connectToHost() 会立即返回,而当连接建立后,QTcpSocket 会发出connected() 信号。
- 同步(阻塞)方法。在非图形用户界面和多线程应用程序中,可以调用
waitFor...()
函数(例如QTcpSocket::waitForConnected()) 来暂停调用线程,直到操作完成,而不是连接到信号。
本示例的实现与Fortune Client示例非常相似,但我们并不是将QTcpSocket 作为主类的成员,在主线程中进行异步联网,而是在单独的线程中进行所有网络操作,并使用QTcpSocket 的阻塞 API。
本示例的目的是演示一种模式,您可以用它来简化网络代码,同时又不会降低用户界面的响应速度。使用 Qt 的阻塞网络 API 通常可以简化代码,但由于其阻塞行为,它只能在非 GUI 线程中使用,以防止用户界面冻结。但与许多人的想法相反,在QThread 中使用线程并不一定会给应用程序增加难以管理的复杂性。
我们将从处理网络代码的 FortuneThread 类开始。
class FortuneThread : public QThread { Q_OBJECT public: FortuneThread(QObject *parent = nullptr); ~FortuneThread(); void requestNewFortune(const QString &hostName, quint16 port); void run() override; signals: void newFortune(const QString &fortune); void error(int socketError, const QString &message); private: QString hostName; quint16 port; QMutex mutex; QWaitCondition cond; bool quit; };
FortuneThread 是QThread 的一个子类,它提供了一个用于调度财富请求的 API,并具有发送财富和报告错误的信号。你可以调用 requestNewFortune() 来请求一个新的运势,结果会通过 newFortune() 信号发送。如果发生错误,则会发出 error() 信号。
需要注意的是,requestNewFortune()是由 GUI 主线程调用的,但它存储的主机名和端口值将由 FortuneThread 的线程访问。因为我们将同时从不同的线程读写 FortuneThread 的数据成员,所以我们使用QMutex 来同步访问。
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port) { QMutexLocker locker(&mutex); this->hostName = hostName; this->port = port; if (!isRunning()) start(); else cond.wakeOne(); }
requestNewFortune() 函数存储了 fortune 服务器的主机名和端口作为成员数据,我们使用QMutexLocker 锁定互斥来保护这些数据。然后我们启动线程,除非它已经在运行。稍后我们将再次讨论QWaitCondition::wakeOne() 调用。
void FortuneThread::run() { mutex.lock(); QString serverName = hostName; quint16 serverPort = port; mutex.unlock();
在 run() 函数中,我们首先获取互斥锁,从成员数据中获取主机名和端口,然后再次释放锁。requestNewFortune()
QString 是可重入的,但不是 线程安全的,我们还必须避免从一个请求中读取主机名而从另一个请求中读取端口这种不太可能发生的风险。正如你可能已经猜到的,FortuneThread 一次只能处理一个请求。
现在,run() 函数进入一个循环:
while (!quit) { const int Timeout = 5 * 1000; QTcpSocket socket; socket.connectToHost(serverName, serverPort);
只要quit为 false,循环就会继续请求财富。我们在堆栈上创建一个QTcpSocket ,开始第一个请求,然后调用connectToHost() 。这将启动一个异步操作,在控制权返回 Qt XML 的事件循环后,将导致QTcpSocket 发出connected() 或error() 。
if (!socket.waitForConnected(Timeout)) { emit error(socket.error(), socket.errorString()); return; }
但由于我们是在非图形用户界面线程中运行,因此不必担心阻塞用户界面。因此,我们无需进入事件循环,只需调用QTcpSocket::waitForConnected() 即可。该函数将等待,阻塞调用线程,直到QTcpSocket 发出 connected() 或发生错误。如果发出 connected(),函数返回 true;如果连接失败或超时(本例中为 5 秒后),函数返回 false。QTcpSocket::waitForConnected() 和其他waitFor...()
函数一样,是QTcpSocket阻塞 API 的一部分。
执行此语句后,我们就可以使用已连接的套接字了。
QDataStream in(&socket); in.setVersion(QDataStream::Qt_6_5); QString fortune;
现在我们可以创建一个QDataStream 对象,将套接字传递给QDataStream 的构造函数,并像其他客户端示例一样,将流协议版本设为QDataStream::Qt_6_5 。
do { if (!socket.waitForReadyRead(Timeout)) { emit error(socket.error(), socket.errorString()); return; } in.startTransaction(); in >> fortune; } while (!in.commitTransaction());
接下来,我们将启动一个循环,通过调用QTcpSocket::waitForReadyRead() 来等待财富字符串数据。如果返回 false,我们就中止操作。在这条语句之后,我们开始一个流读取事务。当QDataStream::commitTransaction() 返回 true 时,即财富字符串加载成功,我们就退出循环。通过发出 newFortune() 命令,生成的财富字符串将被传送:
mutex.lock(); emit newFortune(fortune); cond.wait(&mutex); serverName = hostName; serverPort = port; mutex.unlock(); }
循环的最后一部分是获取互斥,以便安全地读取成员数据。然后,我们调用QWaitCondition::wait() 让线程进入休眠状态。此时,我们可以返回 requestNewFortune() 并仔细查看 wakeOne() 的调用:
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port) { ... if (!isRunning()) start(); else cond.wakeOne(); }
这里发生的事情是因为线程在等待新请求时睡着了,所以我们需要在新请求到来时再次唤醒它。线程中经常使用QWaitCondition 来发出类似的唤醒信号。
FortuneThread::~FortuneThread() { mutex.lock(); quit = true; cond.wakeOne(); mutex.unlock(); wait(); }
在 FortuneThread 演示的最后,这是一个析构函数,它将quit设为 true,唤醒线程并等待线程退出后再返回。这样,运行()中的while
循环就能完成当前迭代。当 run() 返回时,线程将终止并被销毁。
现在介绍 BlockingClient 类:
class BlockingClient : public QWidget { Q_OBJECT public: BlockingClient(QWidget *parent = nullptr); private slots: void requestNewFortune(); void showFortune(const QString &fortune); void displayError(int socketError, const QString &message); void enableGetFortuneButton(); private: QLabel *hostLabel; QLabel *portLabel; QLineEdit *hostLineEdit; QLineEdit *portLineEdit; QLabel *statusLabel; QPushButton *getFortuneButton; QPushButton *quitButton; QDialogButtonBox *buttonBox; FortuneThread thread; QString currentFortune; };
BlockingClient 与FortuneClient 示例中的 Client 类非常相似,但在该类中,我们存储的是 FortuneThread 成员,而不是指向QTcpSocket 的指针。当用户点击 "Get Fortune "按钮时,会调用相同的槽,但其实现略有不同:
connect(&thread, &FortuneThread::newFortune, this, &BlockingClient::showFortune); connect(&thread, &FortuneThread::error, this, &BlockingClient::displayError);
我们将 FortuneThread 的两个信号 newFortune() 和 error()(与上一个示例中的QTcpSocket::readyRead() 和QTcpSocket::error() 有点类似)连接到 requestNewFortune() 和 displayError()。
void BlockingClient::requestNewFortune() { getFortuneButton->setEnabled(false); thread.requestNewFortune(hostLineEdit->text(), portLineEdit->text().toInt()); }
requestNewFortune() 槽调用 FortuneThread::requestNewFortune(),它负责调度请求。当线程接收到新的运势并发出 newFortune() 时,我们的 showFortune() 槽就会被调用:
void BlockingClient::showFortune(const QString &nextFortune) { if (nextFortune == currentFortune) { requestNewFortune(); return; } currentFortune = nextFortune; statusLabel->setText(currentFortune); getFortuneButton->setEnabled(true); }
在这里,我们只需显示作为参数收到的财富。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.