Blockierung von Fortune Client

Zeigt, wie man einen Client für einen Netzwerkdienst erstellt.

QTcpSocket unterstützt zwei allgemeine Ansätze zur Netzwerkprogrammierung:

  • Der asynchrone (nicht-blockierende) Ansatz. Operationen werden geplant und ausgeführt, wenn die Kontrolle an die Ereignisschleife von Qt zurückkehrt. Wenn die Operation beendet ist, gibt QTcpSocket ein Signal aus. Zum Beispiel kehrt QTcpSocket::connectToHost() sofort zurück, und wenn die Verbindung hergestellt wurde, sendet QTcpSocket connected ().
  • Der synchrone (blockierende) Ansatz. In Nicht-GUI- und Multithread-Anwendungen können Sie die Funktionen waitFor...() (z. B. QTcpSocket::waitForConnected()) aufrufen, um den aufrufenden Thread anzuhalten, bis der Vorgang abgeschlossen ist, anstatt eine Verbindung zu Signalen herzustellen.

Die Implementierung ist dem Fortune-Client-Beispiel sehr ähnlich, aber anstatt QTcpSocket als Mitglied der Hauptklasse zu haben und asynchrone Netzwerkoperationen im Hauptthread durchzuführen, werden wir alle Netzwerkoperationen in einem separaten Thread durchführen und die blockierende API von QTcpSocket verwenden.

Der Zweck dieses Beispiels ist es, ein Muster zu demonstrieren, das Sie verwenden können, um Ihren Netzwerkcode zu vereinfachen, ohne die Reaktionsfähigkeit Ihrer Benutzeroberfläche zu verlieren. Die Verwendung der blockierenden Netzwerk-API von Qt führt oft zu einfacherem Code, sollte aber wegen ihres blockierenden Verhaltens nur in Nicht-GUI-Threads verwendet werden, um ein Einfrieren der Benutzeroberfläche zu verhindern. Aber im Gegensatz zu dem, was viele denken, führt die Verwendung von Threads mit QThread nicht zwangsläufig zu einer unüberschaubaren Komplexität Ihrer Anwendung.

Wir beginnen mit der Klasse FortuneThread, die den Netzwerkcode verwaltet.

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 ist eine Unterklasse von QThread, die eine API für die Planung von Glücksanfragen zur Verfügung stellt und Signale für die Übermittlung von Glücksfällen und die Meldung von Fehlern enthält. Sie können requestNewFortune() aufrufen, um ein neues Vermögen anzufordern, und das Ergebnis wird durch das Signal newFortune() geliefert. Wenn ein Fehler auftritt, wird das error()-Signal ausgegeben.

Es ist wichtig zu beachten, dass requestNewFortune() vom GUI-Haupt-Thread aufgerufen wird, aber auf die darin gespeicherten Werte für Hostname und Port wird vom FortuneThread-Thread zugegriffen. Da wir die Datenelemente von FortuneThread von verschiedenen Threads aus gleichzeitig lesen und schreiben werden, verwenden wir QMutex, um den Zugriff zu synchronisieren.

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

Die Funktion requestNewFortune() speichert den Hostnamen und den Port des Fortune-Servers als Mitgliedsdaten, und wir sperren den Mutex mit QMutexLocker, um diese Daten zu schützen. Dann starten wir den Thread, sofern er nicht bereits läuft. Wir werden später auf den Aufruf QWaitCondition::wakeOne() zurückkommen.

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

In der Funktion run() beginnen wir damit, die Mutex-Sperre zu übernehmen, den Hostnamen und den Port aus den Mitgliedsdaten zu holen und die Sperre dann wieder aufzuheben. Der Fall, gegen den wir uns schützen wollen, ist, dass requestNewFortune() zur gleichen Zeit aufgerufen werden könnte, während wir diese Daten abrufen. QString ist ablaufinvariant, aber nicht thread-sicher, und wir müssen auch das unwahrscheinliche Risiko vermeiden, den Hostnamen von einer Anfrage und den Port von einer anderen zu lesen. Und wie Sie vielleicht schon vermutet haben, kann FortuneThread immer nur eine Anfrage auf einmal bearbeiten.

Die Funktion run() tritt nun in eine Schleife ein:

    while (!quit) {
        const int Timeout = 5 * 1000;

        QTcpSocket socket;
        socket.connectToHost(serverName, serverPort);

Die Schleife wird so lange fortgesetzt, wie quit falsch ist. Wir beginnen unsere erste Anfrage, indem wir ein QTcpSocket auf dem Stack erstellen und dann connectToHost() aufrufen. Dies startet eine asynchrone Operation, die, nachdem die Kontrolle an die Qt-Ereignisschleife zurückgegeben wurde, QTcpSocket veranlasst, connected() oder error() auszugeben.

        if (!socket.waitForConnected(Timeout)) {
            emit error(socket.error(), socket.errorString());
            return;
        }

Da wir aber in einem Nicht-GUI-Thread laufen, müssen wir uns keine Sorgen um die Blockierung der Benutzeroberfläche machen. Anstatt also eine Ereignisschleife zu starten, rufen wir einfach QTcpSocket::waitForConnected() auf. Diese Funktion wartet und blockiert den aufrufenden Thread, bis QTcpSocket connected() ausgibt oder ein Fehler auftritt. Wenn connected() ausgegeben wird, gibt die Funktion true zurück; wenn die Verbindung fehlgeschlagen ist oder die Zeit abgelaufen ist (was in diesem Beispiel nach 5 Sekunden geschieht), wird false zurückgegeben. QTcpSocket::waitForConnected() ist, wie die anderen Funktionen von waitFor...(), Teil der blockierenden API von QTcpSocket.

Nach dieser Anweisung haben wir einen verbundenen Socket, mit dem wir arbeiten können.

        QDataStream in(&socket);
        in.setVersion(QDataStream::Qt_6_5);
        QString fortune;

Jetzt können wir ein Objekt QDataStream erstellen, indem wir den Socket an den Konstruktor von QDataStream übergeben, und wie in den anderen Client-Beispielen setzen wir die Stream-Protokollversion auf QDataStream::Qt_6_5.

        do {
            if (!socket.waitForReadyRead(Timeout)) {
                emit error(socket.error(), socket.errorString());
                return;
            }

            in.startTransaction();
            in >> fortune;
        } while (!in.commitTransaction());

Wir fahren fort, indem wir eine Schleife starten, die auf die Daten der Glückszeichenfolge wartet, indem wir QTcpSocket::waitForReadyRead() aufrufen. Wenn er false zurückgibt, brechen wir den Vorgang ab. Nach dieser Anweisung starten wir eine Stream-Lesetransaktion. Wir verlassen die Schleife, wenn QDataStream::commitTransaction() true zurückgibt, was bedeutet, dass die Glückszeichenfolge erfolgreich geladen wurde. Das resultierende Vermögen wird durch die Ausgabe von newFortune() geliefert:

        mutex.lock();
        emit newFortune(fortune);

        cond.wait(&mutex);
        serverName = hostName;
        serverPort = port;
        mutex.unlock();
    }

Der letzte Teil unserer Schleife besteht darin, dass wir den Mutex erwerben, so dass wir sicher aus unseren Mitgliedsdaten lesen können. Dann lassen wir den Thread schlafen, indem wir QWaitCondition::wait() aufrufen. An diesem Punkt können wir zu requestNewFortune() zurückkehren und den Aufruf von wakeOne() genauer betrachten:

void FortuneThread::requestNewFortune(const QString &hostName, quint16 port)
{
    ...
    if (!isRunning())
        start();
    else
        cond.wakeOne();
}

Da der Thread beim Warten auf eine neue Anfrage eingeschlafen ist, mussten wir ihn wieder aufwecken, wenn eine neue Anfrage eintrifft. QWaitCondition wird oft in Threads verwendet, um einen solchen Weckruf zu signalisieren.

FortuneThread::~FortuneThread()
{
    mutex.lock();
    quit = true;
    cond.wakeOne();
    mutex.unlock();
    wait();
}

Zum Abschluss des FortuneThread-Walkthroughs ist dies der Destruktor, der quit auf true setzt, den Thread aufweckt und darauf wartet, dass der Thread beendet wird, bevor er zurückkehrt. Dadurch kann die while -Schleife in run() ihre aktuelle Iteration beenden. Wenn run() zurückkehrt, wird der Thread beendet und zerstört.

Nun zur Klasse 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 ist der Client-Klasse aus dem Fortune-Client-Beispiel sehr ähnlich, aber in dieser Klasse speichern wir ein FortuneThread-Mitglied anstelle eines Zeigers auf QTcpSocket. Wenn der Benutzer auf die Schaltfläche "Get Fortune" klickt, wird derselbe Slot aufgerufen, aber seine Implementierung ist etwas anders:

    connect(&thread, &FortuneThread::newFortune,
            this, &BlockingClient::showFortune);
    connect(&thread, &FortuneThread::error,
            this, &BlockingClient::displayError);

Wir verbinden die beiden Signale newFortune() und error() unseres FortuneThreads (die in gewisser Weise den Signalen QTcpSocket::readyRead() und QTcpSocket::error() im vorherigen Beispiel ähneln) mit requestNewFortune() und displayError().

void BlockingClient::requestNewFortune()
{
    getFortuneButton->setEnabled(false);
    thread.requestNewFortune(hostLineEdit->text(),
                             portLineEdit->text().toInt());
}

Der requestNewFortune()-Slot ruft FortuneThread::requestNewFortune() auf, der die Anfrage plant. Wenn der Thread ein neues Vermögen erhalten hat und newFortune() sendet, wird unser showFortune()-Slot aufgerufen:

void BlockingClient::showFortune(const QString &nextFortune)
{
    if (nextFortune == currentFortune) {
        requestNewFortune();
        return;
    }

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

Hier zeigen wir einfach das Glück an, das wir als Argument erhalten haben.

Beispielprojekt @ code.qt.io

Siehe auch Fortune Client und Fortune Server.

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