Cliente de Fortuna de Bloqueo

Demuestra cómo crear un cliente para un servicio de red.

El usuario proporciona los datos del servidor y se utiliza la API de bloqueo de Q TcpSocket para completar las operaciones de red

QTcpSocket Soporta dos enfoques generales para la programación en red:

  • El enfoque asíncrono (sin bloqueo). Las operaciones se programan y realizan cuando el control vuelve al bucle de eventos de Qt. Cuando la operación finaliza, QTcpSocket emite una señal. Por ejemplo, QTcpSocket::connectToHost() retorna inmediatamente, y cuando se ha establecido la conexión, QTcpSocket emite connected().
  • El enfoque síncrono (de bloqueo). En aplicaciones no GUI y multihilo, puede llamar a las funciones waitFor...() (por ejemplo, QTcpSocket::waitForConnected()) para suspender el hilo de llamada hasta que la operación se haya completado, en lugar de conectarse a señales.

La implementación es muy similar a la del ejemplo del Cliente Fortune, pero en lugar de tener QTcpSocket como miembro de la clase principal, haciendo la red asíncrona en el hilo principal, haremos todas las operaciones de red en un hilo separado y utilizaremos QTcpSocket's API de bloqueo.

El propósito de este ejemplo es demostrar un patrón que puedes utilizar para simplificar tu código de red, sin perder capacidad de respuesta en tu interfaz de usuario. El uso de la API de bloqueo de red de Qt a menudo conduce a un código más simple, pero debido a su comportamiento de bloqueo, sólo debe utilizarse en hilos no-GUI para evitar que la interfaz de usuario se congele. Pero al contrario de lo que muchos piensan, el uso de hilos con QThread no añade necesariamente una complejidad inmanejable a su aplicación.

Empezaremos con la clase FortuneThread, que maneja el código de red.

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 es una subclase de QThread que proporciona una API para programar peticiones de fortunas, y tiene señales para entregar fortunas e informar de errores. Puedes llamar a requestNewFortune() para solicitar una nueva fortuna, y el resultado es entregado por la señal newFortune(). Si se produce algún error, se emite la señal error().

Es importante notar que requestNewFortune() es llamada desde el hilo principal, GUI, pero el nombre de host y los valores de puerto que almacena serán accedidos desde el hilo de FortuneThread. Como leeremos y escribiremos los datos de FortuneThread desde diferentes hilos simultáneamente, usamos QMutex para sincronizar el acceso.

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

La función requestNewFortune() almacena el nombre del host y el puerto del servidor de fortune como datos miembros, y bloqueamos el mutex con QMutexLocker para proteger estos datos. A continuación, iniciamos el hilo, a menos que ya se esté ejecutando. Volveremos a la llamada a QWaitCondition::wakeOne() más adelante.

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

En la función run(), comenzamos adquiriendo el bloqueo mutex, obteniendo el nombre de host y el puerto de los datos miembros, y luego liberando el bloqueo de nuevo. El caso contra el que nos estamos protegiendo es que requestNewFortune() podría ser llamado al mismo tiempo que estamos obteniendo estos datos. QString es reentrante pero no seguro para hilos, y también debemos evitar el improbable riesgo de leer el nombre de host de una petición, y el puerto de otra. Y como habrás adivinado, FortuneThread sólo puede manejar una petición a la vez.

La función run() entra ahora en un bucle:

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

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

El bucle continuará solicitando fortunas mientras quit sea falso. Comenzamos nuestra primera petición creando un QTcpSocket en la pila, y luego llamamos a connectToHost(). Esto inicia una operación asíncrona que, después de que el control vuelva al bucle de eventos de Qt, hará que QTcpSocket emita connected() o error().

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

Pero como estamos ejecutando en un hilo no-GUI, no tenemos que preocuparnos de bloquear la interfaz de usuario. Así que en lugar de entrar en un bucle de eventos, simplemente llamamos a QTcpSocket::waitForConnected(). Esta función esperará, bloqueando el hilo de llamada, hasta que QTcpSocket emita connected() o se produzca un error. Si se emite connected(), la función devuelve true; si la conexión falló o expiró (lo que en este ejemplo ocurre después de 5 segundos), se devuelve false. QTcpSocket::waitForConnected(), como las demás funciones de waitFor...(), forma parte de la API de bloqueo de QTcpSocket.

Después de esta sentencia, tenemos un socket conectado con el que trabajar.

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

Ahora podemos crear un objeto QDataStream, pasando el socket al constructor de QDataStream, y como en los otros ejemplos de clientes, establecemos la versión del protocolo de flujo a QDataStream::Qt_6_5.

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

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

Procedemos iniciando un bucle que espera los datos de la cadena de la fortuna llamando a QTcpSocket::waitForReadyRead(). Si devuelve false, abortamos la operación. Después de esta sentencia, iniciamos una operación de lectura del flujo. Salimos del bucle cuando QDataStream::commitTransaction() devuelve true, lo que significa que la carga de la cadena de la fortuna ha tenido éxito. La fortuna resultante se entrega emitiendo newFortune():

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

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

La parte final de nuestro bucle es que adquirimos el mutex para que podamos leer con seguridad los datos de nuestros miembros. A continuación, dejamos que el hilo se vaya a dormir llamando a QWaitCondition::wait(). En este punto, podemos volver a requestNewFortune() y mirar de cerca la llamada a wakeOne():

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

Lo que ocurrió aquí fue que como el hilo se durmió esperando una nueva petición, necesitábamos despertarlo de nuevo cuando llegara una nueva petición. QWaitCondition se utiliza a menudo en hilos para señalar una llamada de despertador como ésta.

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

Para terminar el tutorial de FortuneThread, este es el destructor que establece quit a true, despierta el hilo y espera a que salga antes de volver. Esto permite que el bucle while en run() termine su iteración actual. Cuando run() retorne, el hilo terminará y será destruido.

Ahora la clase 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 es muy similar a la clase Client del ejemplo del Cliente de Fortune, pero en esta clase almacenamos un miembro FortuneThread en lugar de un puntero a QTcpSocket. Cuando el usuario pulsa el botón "Get Fortune", se llama al mismo slot, pero su implementación es ligeramente diferente:

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

Conectamos las dos señales de nuestro FortuneThread newFortune() y error() (que son algo similares a QTcpSocket::readyRead() y QTcpSocket::error() en el ejemplo anterior) a requestNewFortune() y displayError().

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

La ranura requestNewFortune() llama a FortuneThread::requestNewFortune(), que programa la petición. Cuando el hilo ha recibido una nueva fortuna y emite newFortune(), se llama a nuestra ranura showFortune():

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

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

Aquí, simplemente mostramos la fortuna que hemos recibido como argumento.

Proyecto de ejemplo @ code.qt.io

Vea también Cliente Fortune y Servidor 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.