Fortune 客户端

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

本示例使用QTcpSocket ,建议与“Fortune 服务器”示例或“多线程 Fortune 服务器”示例配合运行。

Fortune Client 示例的屏幕截图

本示例使用一种基于QDataStream 的简单数据传输协议,从(来自“Fortune服务器”示例的)Fortune服务器请求一行文本。客户端只需连接到服务器即可请求一句箴言。随后,服务器将通过QString 对象返回包含箴言文本的响应。

QTcpSocket 支持两种通用的网络编程方法:

  • 异步(非阻塞)方法。操作会被排入队列,并在控制权返回 Qt 事件循环时执行。操作完成后,QTcpSocket 会发出一个信号。例如,QTcpSocket::connectToHost() 会立即返回,当连接建立后,QTcpSocket 会发出connected() 信号。
  • 同步(阻塞)方法。在非 GUI 和多线程应用程序中,您可以调用waitFor...() 函数(例如QTcpSocket::waitForConnected())来挂起调用线程,直到操作完成,而无需订阅信号。

在本示例中,我们将演示异步方法。示例“阻塞式 Fortune 客户端”则演示了同步方法。

我们的类包含一些数据和几个私有槽:

class Client : public QDialog
{
    Q_OBJECT

public:
    explicit Client(QWidget *parent = nullptr);

private slots:
    void requestNewFortune();
    void readFortune();
    void displayError(QAbstractSocket::SocketError socketError);
    void enableGetFortuneButton();

private:
    QComboBox *hostCombo = nullptr;
    QLineEdit *portLineEdit = nullptr;
    QLabel *statusLabel = nullptr;
    QPushButton *getFortuneButton = nullptr;

    QTcpSocket *tcpSocket = nullptr;
    QDataStream in;
    QString currentFortune;
};

除了构成图形用户界面的控件外,数据成员还包括一个QTcpSocket 指针、一个用于操作套接字的QDataStream 对象,以及当前显示的 fortune 文本的副本。

套接字在 Client 构造函数中初始化。我们将主控件作为父控件传递,这样就无需担心删除套接字的问题:

Client::Client(QWidget *parent)
    : QDialog(parent)
    , hostCombo(new QComboBox)
    , portLineEdit(new QLineEdit)
    , getFortuneButton(new QPushButton(tr("Get Fortune")))
    , tcpSocket(new QTcpSocket(this))
{
    ...
    in.setDevice(tcpSocket);
    in.setVersion(QDataStream::Qt_6_5);

该协议基于QDataStream ,因此我们将流设备设置为新创建的套接字。然后,我们显式地将流的协议版本设置为QDataStream::Qt_6_5 ,以确保无论客户端和服务器使用哪个版本的Qt,都能与fortune服务器使用相同的版本。

本示例中,我们仅需使用QTcpSocket 中的两个信号:QTcpSocket::readyRead()(表示已接收到数据)和QTcpSocket::errorOccurred()(用于捕获任何连接错误):

    ...
    connect(tcpSocket, &QIODevice::readyRead, this, &Client::readFortune);
    connect(tcpSocket, &QAbstractSocket::errorOccurred,
    ...
}

点击“Get Fortune ”按钮将触发requestNewFortune() 槽:

void Client::requestNewFortune()
{
    getFortuneButton->setEnabled(false);
    tcpSocket->abort();
    tcpSocket->connectToHost(hostCombo->currentText(),
                             portLineEdit->text().toInt());
}

由于我们允许用户在前一次连接尚未关闭完成前就点击“Get Fortune ”按钮,因此我们首先通过调用QTcpSocket::abort()来终止前一次连接。(对于未连接的套接字,此函数不执行任何操作。)随后,我们通过调用QTcpSocket::connectToHost()并传入用户界面中的主机名和端口作为参数,来连接 fortune 服务器。

调用connectToHost() 后,可能发生以下两种情况之一:

  • 连接建立成功。此时,服务器将向我们发送一条 fortune 消息。QTcpSocket 每次接收到一组数据时,都会调用readyRead()。
  • 发生错误。如果连接失败或中断,我们需要通知用户。此时,QTcpSocket 将触发errorOccurred(),并调用Client::displayError() 。

让我们先分析errorOccurred() 的情况:

void Client::displayError(QAbstractSocket::SocketError socketError)
{
    switch (socketError) {
    case QAbstractSocket::RemoteHostClosedError:
        break;
    case QAbstractSocket::HostNotFoundError:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The host was not found. Please check the "
                                    "host name and port settings."));
        break;
    case QAbstractSocket::ConnectionRefusedError:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The connection was refused by the peer. "
                                    "Make sure the fortune server is running, "
                                    "and check that the host name and port "
                                    "settings are correct."));
        break;
    default:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The following error occurred: %1.")
                                 .arg(tcpSocket->errorString()));
    }

    getFortuneButton->setEnabled(true);
}

我们使用QMessageBox::information() 通过对话框弹出所有错误。QTcpSocket::RemoteHostClosedError 会被静默忽略,因为 fortune 服务器协议的结束方式就是由服务器关闭连接。

现在来看readyRead()的替代方案。该信号连接到了Client::readFortune() :

void Client::readFortune()
{
    in.startTransaction();

    QString nextFortune;
    in >> nextFortune;

    if (!in.commitTransaction())
        return;

    if (nextFortune == currentFortune) {
        QTimer::singleShot(0, this, &Client::requestNewFortune);
        return;
    }

    currentFortune = nextFortune;
    statusLabel->setText(currentFortune);
    getFortuneButton->setEnabled(true);
}

由于 TCP 基于数据流传输,因此我们无法指望一次性接收完整的 fortune 内容。 特别是在网络速度较慢的情况下,数据可能会以多个小片段的形式接收。QTcpSocket 会将所有传入数据缓冲,并在每个新数据块到达时触发readyRead() 信号,而我们的任务是确保在开始解析之前已接收到了所有所需的数据。

为此,我们使用QDataStream 的读取事务。它会持续将流数据读入内部缓冲区,并在读取不完整时回滚操作。我们首先调用startTransaction(),该方法还会重置流状态,以指示套接字上已收到新数据。 接下来,我们使用QDataStream 的流式操作符,将 fortune 从套接字读取到QString 中。读取完成后,通过调用QDataStream::commitTransaction()来完成该事务。如果未接收到完整的数据包,该函数会将流数据恢复到初始位置,之后我们可以等待新的 readyRead() 信号。

读取操作成功后,我们调用QLabel::setText()来显示运势。

示例项目 @ code.qt.io

另请参阅 “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.