Fortune-Client blockieren

Veranschaulicht, wie ein Client für einen Netzwerkdienst erstellt wird.

Der Benutzer gibt die Serverdaten an, und die blockierende API von Q TcpSocket wird zur Durchführung der Netzwerkoperationen verwendet.

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

  • Der asynchrone (nicht blockierende) Ansatz. Operationen werden eingeplant und ausgeführt, sobald die Kontrolle an die Ereignisschleife von Qt zurückgegeben wird. Wenn die Operation abgeschlossen ist, sendet ` QTcpSocket ` ein Signal aus. Beispielsweise kehrt ` QTcpSocket::connectToHost()` sofort zurück, und sobald die Verbindung hergestellt ist, sendet ` QTcpSocket ` das Signal ` connected()` aus.
  • Der synchrone (blockierende) Ansatz. In Nicht-GUI- und Multithread-Anwendungen können Sie die waitFor...() -Funktionen (z. B. QTcpSocket::waitForConnected()) aufrufen, um den aufrufenden Thread so lange anzuhalten, bis der Vorgang abgeschlossen ist, anstatt Signale abzufangen.

Die Implementierung ähnelt stark dem Beispiel „Fortune Client“, doch anstatt „ QTcpSocket “ als Mitglied der Hauptklasse zu verwenden und asynchrone Netzwerkoperationen im Hauptthread auszuführen, werden wir alle Netzwerkoperationen in einem separaten Thread durchführen und die blockierende API von „ QTcpSocket “ nutzen.

Der Zweck dieses Beispiels ist es, ein Muster zu veranschaulichen, mit dem Sie Ihren Netzwerkcode vereinfachen können, ohne dass die Reaktionsfähigkeit Ihrer Benutzeroberfläche darunter leidet. Die Verwendung der blockierenden Netzwerk-API von Qt führt oft zu einfacherem Code, sollte jedoch aufgrund ihres blockierenden Verhaltens nur in Nicht-GUI-Threads eingesetzt werden, um ein Einfrieren der Benutzeroberfläche zu verhindern. Entgegen der weit verbreiteten Meinung führt die Verwendung von Threads mit „ QThread “ jedoch nicht zwangsläufig zu einer unüberschaubaren Komplexität Ihrer Anwendung.

Wir beginnen mit der Klasse „FortuneThread“, die den Netzwerkcode verarbeitet.

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 zur Planung von Anfragen nach Glückssprüchen bereitstellt und über Signale zur Übermittlung von Glückssprüchen sowie zur Meldung von Fehlern verfügt. Mit „requestNewFortune()“ können Sie einen neuen Glücksspruch anfordern; das Ergebnis wird über das Signal „newFortune()“ übermittelt. Tritt ein Fehler auf, wird das Signal „error()“ ausgelöst.

Es ist wichtig zu beachten, dass `requestNewFortune()` vom Haupt-GUI-Thread aus aufgerufen wird, der Zugriff auf die darin gespeicherten Werte für Hostname und Port jedoch vom Thread von `FortuneThread` erfolgt. Da wir die Datenelemente von `FortuneThread` gleichzeitig aus verschiedenen Threads lesen und schreiben, 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 Memberdaten, und wir sperren den Mutex mit ` QMutexLocker `, um diese Daten zu schützen. Anschließend starten wir den Thread, sofern er nicht bereits läuft. Auf den Aufruf von ` QWaitCondition::wakeOne()` kommen wir später zurück.

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

In der Funktion `run()` beginnen wir damit, die Mutex-Sperre zu erwerben, den Hostnamen und den Port aus den Mitgliedsdaten abzurufen und anschließend die Sperre wieder freizugeben. Wir schützen uns vor dem Fall, dass ` requestNewFortune() ` gleichzeitig mit dem Abrufen dieser Daten aufgerufen werden könnte. ` QString ` ist zwar reentrant, aber nicht threadsicher, und wir müssen zudem das unwahrscheinliche Risiko vermeiden, den Hostnamen aus einer Anfrage und den Port aus einer anderen zu lesen. Und wie Sie vielleicht bereits erraten haben, kann `FortuneThread` jeweils nur eine Anfrage bearbeiten.

Die Funktion `run()` geht nun in eine Schleife über:

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

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

Die Schleife fordert so lange Glückssprüche an, wie „quit“ auf „false“ gesetzt ist. Wir starten unsere erste Anfrage, indem wir ein „ QTcpSocket “ auf dem Stack anlegen und anschließend „ connectToHost()“ aufrufen. Dies löst einen asynchronen Vorgang aus, der – nachdem die Kontrolle an die Ereignisschleife von Qt zurückgegeben wurde – dazu führt, dass „ QTcpSocket “ entweder „ connected()“ oder „ error()“ ausgibt.

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

Da wir jedoch in einem Nicht-GUI-Thread arbeiten, müssen wir uns keine Gedanken über eine Blockierung der Benutzeroberfläche machen. Anstatt also in eine Ereignisschleife einzutreten, rufen wir einfach ` QTcpSocket::waitForConnected()` auf. Diese Funktion wartet – und blockiert dabei den aufrufenden Thread –, bis ` QTcpSocket ` `connected()` auslöst oder ein Fehler auftritt. Wird „connected()“ ausgegeben, gibt die Funktion „true“ zurück; ist die Verbindung fehlgeschlagen oder ist die Zeitüberschreitung eingetreten (was in diesem Beispiel nach 5 Sekunden geschieht), wird „false“ zurückgegeben. „ QTcpSocket::waitForConnected()“ ist, wie die anderen „ waitFor...() “-Funktionen, Teil der blockierenden API von „ QTcpSocket “.

Nach dieser Anweisung steht uns ein verbundener Socket zur Verfügung.

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

Nun können wir ein QDataStream -Objekt 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());

Anschließend starten wir eine Schleife, die durch den Aufruf von ` QTcpSocket::waitForReadyRead()` auf die Daten der Glücksspruch-Zeichenkette wartet. Wenn der Aufruf `false` zurückgibt, brechen wir den Vorgang ab. Nach dieser Anweisung starten wir eine Stream-Lese-Transaktion. Wir verlassen die Schleife, sobald ` QDataStream::commitTransaction()` `true` zurückgibt, was das erfolgreiche Laden der Glücksspruch-Zeichenkette bedeutet. Der resultierende Glücksspruch wird durch den Aufruf von `newFortune()` ausgegeben:

        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, damit wir sicher aus unseren Member-Daten lesen können. Anschließend versetzen wir den Thread durch den Aufruf von ` QWaitCondition::wait()` in den Ruhezustand. An dieser Stelle können wir zu `requestNewFortune()` zurückkehren und uns den Aufruf von `wakeOne()` genauer ansehen:

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

Was hier geschah: Da der Thread beim Warten auf eine neue Anfrage in den Ruhezustand versetzt wurde, mussten wir ihn wieder wecken, sobald eine neue Anfrage eintraf. ` QWaitCondition ` wird in Threads häufig verwendet, um einen solchen Weckruf zu signalisieren.

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

Zum Abschluss der FortuneThread-Anleitung: Dies ist der Destruktor, der `quit` auf `true` setzt, den Thread weckt und wartet, bis der Thread beendet ist, 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 im Fortune-Client-Beispiel sehr ähnlich, doch in dieser Klasse speichern wir ein „FortuneThread“-Mitglied anstelle eines Zeigers auf ein „ QTcpSocket “. Wenn der Benutzer auf die Schaltfläche „Get Fortune“ klickt, wird derselbe Slot aufgerufen, dessen Implementierung sich jedoch geringfügig unterscheidet:

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

Wir verbinden die beiden Signale `newFortune()` und `error()` unseres `FortuneThread` (die in gewisser Weise den Signalen ` QTcpSocket::readyRead()` und ` QTcpSocket::error()` aus dem vorherigen Beispiel ähneln) mit den Slots `requestNewFortune()` und `displayError()`.

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

Der Slot `requestNewFortune()` ruft `FortuneThread::requestNewFortune()` auf, wodurch die Anfrage eingeplant wird. Sobald der Thread einen neuen Spruch erhalten hat und `newFortune()` ausgibt, wird unser Slot `showFortune()` 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ücksspruch an, das wir als Argument erhalten haben.

Beispielprojekt @ code.qt.io

Siehe auch „Fortune-Client “ und „Fortune-Server“.

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