阻断 Fortune 客户端
演示如何为网络服务创建客户端。

QTcpSocket 支持两种通用的网络编程方法:
- 异步(非阻塞)方法。操作会被排入队列,并在控制权返回至 Qt 的事件循环时执行。操作完成后,QTcpSocket 会发出一个信号。例如,QTcpSocket::connectToHost() 会立即返回,当连接建立后,QTcpSocket 会发出connected()。
- 同步(阻塞)方法。在非GUI和多线程应用程序中,您可以调用
waitFor...()函数(例如QTcpSocket::waitForConnected())来挂起调用线程,直到操作完成,而无需订阅信号。
其实现与“Fortune Client”示例非常相似,但不同之处在于:不再将QTcpSocket 作为主类的成员在主线程中进行异步网络通信,而是将所有网络操作放在一个单独的线程中,并使用QTcpSocket 的阻塞API。
本示例旨在演示一种模式,您可以通过它简化网络代码,同时又不影响用户界面的响应性。 使用 Qt Network 的阻塞网络 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 的事件循环后,QTcpSocket 将发出connected() 或error() 事件。
if (!socket.waitForConnected(Timeout)) {
emit error(socket.error(), socket.errorString());
return;
}但由于我们在非GUI线程中运行,因此无需担心阻塞用户界面。因此,我们不进入事件循环,而是直接调用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() 启动一个循环,等待 fortune 字符串数据。如果返回 false,则终止操作。执行完该语句后,我们开始一个流读取事务。当QDataStream::commitTransaction() 返回 true 时,即表示 fortune 字符串加载成功,循环即告结束。最终的 fortune 结果通过调用 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、唤醒线程并等待线程退出后才返回的析构函数。这使得 run() 中的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 与Fortune Client示例中的 Client 类非常相似,但在本类中,我们存储的是 FortuneThread 成员,而不是指向QTcpSocket 的指针。当用户点击“获取运势”按钮时,会调用相同的槽,但其实现略有不同:
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);
}在此,我们只需显示作为参数接收到的运势内容。
另请参阅 Fortune 客户端和Fortune 服务器。
© 2026 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.