财富客户端

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

本示例使用QTcpSocket ,可与财富服务器示例或线程财富服务器示例同时运行。

财富》客户端示例截图

本示例使用基于QDataStream 的简单数据传输协议,向财富服务器(来自财富服务器示例)请求一行文本。客户端只需连接到服务器即可请求财富。然后,服务器响应一个包含财富文本的QString

QTcpSocket 网络编程支持两种一般方法:

  • 异步(非阻塞)方法。当控制返回到 Qt 的事件循环时,将安排并执行操作。操作完成后,QTcpSocket 发出信号。例如,QTcpSocket::connectToHost() 会立即返回,而当连接建立后,QTcpSocket 会发出connected() 信号。
  • 同步(阻塞)方法。在非图形用户界面和多线程应用程序中,可以调用waitFor...() 函数(例如QTcpSocket::waitForConnected()) 来暂停调用线程,直到操作完成,而不是连接到信号。

在本例中,我们将演示异步方法。Blocking Fortune Client示例说明了同步方法。

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

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 对象以及当前显示的财富文本的副本。

套接字在客户端构造函数中初始化。我们将传递主部件作为父部件,这样就不必担心删除套接字的问题:

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() 将用户界面中的主机名和端口作为参数传递给财富服务器。

调用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 以发送数据流为基础,因此我们不能指望一次性获取整个财富信息。尤其是在速度较慢的网络中,数据可能会被分成几个小片段来接收。QTcpSocket 会对所有接收到的数据进行缓冲,并为每一个到达的新数据块发送readyRead() 信号,而我们的任务就是确保在开始解析之前已经接收到了所需的全部数据。

为此,我们使用QDataStream 读取事务。它不断将流数据读入内部缓冲区,并在读取不完整时将其回滚。我们首先调用 startTransaction(),它也会重置流状态,以表明套接字上收到了新数据。接下来,我们使用QDataStream 的流操作符将数据从套接字读入QString 。一旦读取完毕,我们就调用QDataStream::commitTransaction() 来完成事务。如果我们没有收到完整的数据包,该函数会将流数据恢复到初始位置,之后我们就可以等待新的 readyRead() 信号。

读取成功后,我们调用QLabel::setText() 来显示财富。

示例项目 @ code.qt.io

另请参阅 " 财富服务器 "和 "阻塞财富客户端"。

© 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.