福布斯客户端

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

此示例使用QTcpSocket,旨在与福布斯服务器示例或线程化福布斯服务器示例一起运行。

Screenshot of the Fortune Client example

此示例使用一种基于QDataStream的数据传输协议,从福布斯服务器(来自福布斯服务器示例)请求一行文本。客户端通过简单地连接到服务器来请求福气。服务器随后响应一个包含福气文本的QString

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

  • 异步(非阻塞)方法。操作将在控制权返回Qt的事件循环时安排和执行。操作完成后,QTcpSocket会发出一个信号。例如,QTcpSocket::connectToHost()将立即返回,并且在连接建立后,QTcpSocket会发出connected()。
  • 同步(阻塞)方法。在非GUI和单线程应用程序中,你可以调用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;
};

除了构成GUI的小部件之外,数据成员还包括一个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,我们都在使用相同的版本。

在这个示例中,我们需要使用的唯一QTcpSocket信号是表示已接收数据的QTcpSocket::readyRead()以及我们将用以捕获任何连接错误的QTcpSocket::errorOccurred()

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

点击获取福气按钮将触发requestNewFortune()

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

因为我们允许用户在先前的连接完成关闭之前点击“获取幸运”,所以我们首先通过调用QTcpSocket::abort()来终止先前的连接。(在一个未连接的套接字上,此函数不起作用。)然后,我们通过调用QTcpSocket::connectToHost()来连接到幸运服务器,将用户界面提供的主机名和端口号作为参数传递。

调用connectToHost()可能导致以下两种情况之一

  • 连接已建立。在这种情况下,服务器将向我们发送一个幸运信息。《a href="qtcpsocket.html" translate="no">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被静默忽略,因为幸运服务器协议以服务器关闭连接结束。

现在,让我们来看看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

另请参阅Fortune ServerBlocking Fortune Client

© 2024 The Qt Company Ltd。包括在内的文档贡献是各自所有者的版权。《本文档》根据Free Software Foundation发布的版本1.3的GNU自由文档许可证的条款获得许可。Qt和相应商标是芬兰的The Qt Company Ltd.以及世界其他国家的商标。所有其他商标均为其各自所有者的财产。