阻止Fortune客户端

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

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

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;
};

void FortuneThread::requestNewFortune(const QString &hostName, quint16 port)
{
    QMutexLocker locker(&mutex);
    this->hostName = hostName;
    this->port = port;
    if (!isRunning())
        start();
    else
        cond.wakeOne();
}

void FortuneThread::run()
{
    mutex.lock();
    QString serverName = hostName;
    quint16 serverPort = port;
    mutex.unlock();

在run()函数中,我们首先获取互斥锁,从成员数据中获取主机名和端口,然后再释放锁。我们正在保护自己免受以下情况的侵害:即在获取此数据的同时调用requestNewFortune()。QtString [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()来初始化一个循环,等待接收致富语言的真言字符串数据。如果它返回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的析构函数,唤醒线程,并在返回之前等待线程退出。这允许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);
}

在这里,我们只简单地将接收到的幸运语作为参数显示出来。

示例项目 @ code.qt.io

另请参阅Fortune ClientFortune Server

© 2024 The Qt Company Ltd. 本文档中包含的贡献的版权属于其各自的版权所有者。本文档依据由 Free Software Foundation 发布的 GNU Free Documentation License 版本 1.3 的条款提供许可。Qt 及其相关标志是 The Qt Company Ltd. 在芬兰和其他国家/地区的商标。所有其他商标均为其各自所有者的财产。