Blocage du client Fortune

Ce chapitre explique comment créer un client pour un service réseau.

L'utilisateur fournit les informations relatives au serveur et l'API bloquante de Q TcpSocket est utilisée pour effectuer les opérations réseau.

QTcpSocket Prend en charge deux approches générales de la programmation réseau :

  • L'approche asynchrone (non bloquante). Les opérations sont planifiées et exécutées lorsque le contrôle revient à la boucle d'événements de Qt. Une fois l'opération terminée, la fonction ` QTcpSocket ` émet un signal. Par exemple, la fonction ` QTcpSocket::connectToHost()` renvoie immédiatement un résultat, et une fois la connexion établie, la fonction ` QTcpSocket ` émet le signal ` connected()`.
  • waitFor...() L'approche synchrone (bloquante). Dans les applications non graphiques et multithread, vous pouvez appeler les fonctions de gestion de connexion (par exemple, QTcpSocket::waitForConnected()) pour suspendre le thread appelant jusqu'à ce que l'opération soit terminée, au lieu de vous connecter à des signaux.

L’implémentation est très similaire à celle de l’exemple « Fortune Client », mais au lieu d’avoir QTcpSocket comme membre de la classe principale et d’effectuer les opérations réseau asynchrones dans le thread principal, nous effectuerons toutes les opérations réseau dans un thread distinct et utiliserons l’API bloquante de QTcpSocket.

Le but de cet exemple est de présenter un modèle que vous pouvez utiliser pour simplifier votre code réseau, sans pour autant compromettre la réactivité de votre interface utilisateur. L’utilisation de l’API réseau bloquante de Qt conduit souvent à un code plus simple, mais en raison de son comportement bloquant, elle ne doit être utilisée que dans des threads non liés à l’interface graphique afin d’éviter que celle-ci ne se fige. Cependant, contrairement à ce que beaucoup pensent, l’utilisation de threads avec QThread n’ajoute pas nécessairement une complexité ingérable à votre application.

Nous commencerons par la classe `FortuneThread`, qui gère le code réseau.

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 est une sous-classe de ` QThread ` qui fournit une API permettant de planifier les requêtes de prédictions, et qui dispose de signaux pour la transmission des prédictions et le signalement des erreurs. Vous pouvez appeler `requestNewFortune()` pour demander une nouvelle prédiction, et le résultat est transmis par le signal `newFortune()`. Si une erreur se produit, le signal `error()` est émis.

Il est important de noter que la fonction requestNewFortune() est appelée depuis le thread principal (GUI), mais que les valeurs du nom d’hôte et du port qu’elle stocke seront accessibles depuis le thread de FortuneThread. Comme nous allons lire et écrire simultanément les membres de données de FortuneThread à partir de différents threads, nous utilisons ` QMutex ` pour synchroniser cet accès.

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

La fonction `requestNewFortune()` stocke le nom d’hôte et le port du serveur Fortune en tant que données membres, et nous verrouillons le mutex à l’aide de ` QMutexLocker ` pour protéger ces données. Nous lançons ensuite le thread, s’il n’est pas déjà en cours d’exécution. Nous reviendrons plus tard sur l’appel à ` QWaitCondition::wakeOne()`.

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

Dans la fonction run(), nous commençons par acquérir le verrou du mutex, récupérer le nom d’hôte et le port à partir des données associées, puis relâcher le verrou. Le cas contre lequel nous nous protégeons est celui où requestNewFortune() pourrait être appelé au moment même où nous récupérons ces données. QString est réentrante mais n’est pas thread-safe, et nous devons également éviter le risque, bien que faible, de lire le nom d’hôte d’une requête et le port d’une autre. Et comme vous l’avez sans doute deviné, FortuneThread ne peut traiter qu’une seule requête à la fois.

La fonction run() entre alors dans une boucle :

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

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

La boucle continuera à demander des citations tant que `quit` sera faux. Nous lançons notre première requête en créant un objet ` QTcpSocket ` sur la pile, puis nous appelons ` connectToHost()`. Cela déclenche une opération asynchrone qui, une fois le contrôle restitué à la boucle d’événements de Qt, provoquera l’émission par ` QTcpSocket ` de ` connected()` ou ` error()`.

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

Mais comme nous exécutons cette opération dans un thread non-GUI, nous n’avons pas à nous soucier du blocage de l’interface utilisateur. Ainsi, au lieu d’entrer dans une boucle d’événements, nous appelons simplement ` QTcpSocket::waitForConnected()`. Cette fonction attendra, en bloquant le thread appelant, jusqu’à ce que ` QTcpSocket ` émette `connected()` ou qu’une erreur se produise. Si connected() est émis, la fonction renvoie true ; si la connexion a échoué ou a expiré (ce qui, dans cet exemple, se produit au bout de 5 secondes), elle renvoie false. QTcpSocket::waitForConnected(), comme les autres fonctions de waitFor...(), fait partie de l’API bloquante de QTcpSocket.

Après cette instruction, nous disposons d’un socket connecté avec lequel travailler.

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

Nous pouvons désormais créer un objet QDataStream en passant le socket au constructeur de QDataStream, et, comme dans les autres exemples de client, nous définissons la version du protocole de flux sur QDataStream::Qt_6_5.

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

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

Nous poursuivons en lançant une boucle qui attend les données de la chaîne de fortune en appelant ` QTcpSocket::waitForReadyRead()`. Si la fonction renvoie `false`, nous interrompons l’opération. Après cette instruction, nous lançons une transaction de lecture du flux. Nous sortons de la boucle lorsque ` QDataStream::commitTransaction()` renvoie `true`, ce qui signifie que le chargement de la chaîne de fortune a réussi. La fortune obtenue est renvoyée via la méthode `newFortune()` :

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

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

La dernière partie de notre boucle consiste à acquérir le mutex afin de pouvoir lire en toute sécurité les données de nos membres. Nous mettons ensuite le thread en veille en appelant ` QWaitCondition::wait()`. À ce stade, nous pouvons revenir à `requestNewFortune()` et examiner de près l’appel à `wakeOne()` :

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

Ce qui s’est passé ici, c’est que, comme le thread se met en veille en attendant une nouvelle requête, nous avons dû le réveiller lorsqu’une nouvelle requête est arrivée. La fonction ` QWaitCondition ` est souvent utilisée dans les threads pour signaler un appel de réveil comme celui-ci.

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

Pour conclure la présentation de FortuneThread, voici le destructeur qui définit `quit` sur `true`, réveille le thread et attend que celui-ci se termine avant de revenir. Cela permet à la boucle ` while ` dans `run()` de terminer son itération en cours. Lorsque `run()` revient, le thread se termine et est détruit.

Passons maintenant à la classe `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 est très similaire à la classe Client de l’exemple Fortune Client, mais dans cette classe, nous stockons un membre `FortuneThread` au lieu d’un pointeur vers un ` QTcpSocket`. Lorsque l’utilisateur clique sur le bouton « Get Fortune », le même slot est appelé, mais son implémentation est légèrement différente :

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

Nous connectons les deux signaux newFortune() et error() de notre FortuneThread (qui sont en quelque sorte similaires à QTcpSocket::readyRead() et QTcpSocket::error() dans l’exemple précédent) à requestNewFortune() et displayError().

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

Le slot requestNewFortune() appelle FortuneThread::requestNewFortune(), qui planifie la requête. Lorsque le thread a reçu une nouvelle prédiction et émet newFortune(), notre slot showFortune() est appelé :

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

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

Ici, nous affichons simplement la prédiction reçue en tant qu’argument.

Exemple de projet sur code.qt.io

Voir également Client Fortune et Serveur Fortune.

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