Cliente Fortune

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

Este ejemplo utiliza QTcpSocket, y está pensado para ser ejecutado junto con el ejemplo del Servidor Fortune o el ejemplo del Servidor Fortune enhebrado.

Captura de pantalla del ejemplo de cliente Fortune

Este ejemplo utiliza un sencillo protocolo de transferencia de datos basado en QDataStream para solicitar una línea de texto a un servidor de Fortune (del ejemplo del Servidor de Fort une). El cliente solicita una fortuna simplemente conectándose al servidor. El servidor responde con un QString que contiene el texto de la fortuna.

QTcpSocket soporta dos enfoques generales de 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.

En este ejemplo, demostraremos el enfoque asíncrono. El ejemplo Cliente de Fortuna Bloqueante ilustra el enfoque síncrono.

Nuestra clase contiene algunos datos y algunos slots privados:

class Client : public QDialog
{
    Q_OBJECT

public:
    explicit Client(QWidget *parent = nullptr);

private slots:
    void requestNewFortune();
    void readFortune();
    void displayError(QAbstractSocket::SocketError socketError);
    void enableGetFortuneButton();

private:
    QComboBox *hostCombo = nullptr;
    QLineEdit *portLineEdit = nullptr;
    QLabel *statusLabel = nullptr;
    QPushButton *getFortuneButton = nullptr;

    QTcpSocket *tcpSocket = nullptr;
    QDataStream in;
    QString currentFortune;
};

Aparte de los widgets que componen la GUI, los miembros de datos incluyen un puntero QTcpSocket, un objeto QDataStream que opera sobre el socket, y una copia del texto de la fortuna que se muestra actualmente.

El socket se inicializa en el constructor Cliente. Pasaremos el widget principal como padre, para no tener que preocuparnos de borrar el socket:

Client::Client(QWidget *parent)
    : QDialog(parent)
    , hostCombo(new QComboBox)
    , portLineEdit(new QLineEdit)
    , getFortuneButton(new QPushButton(tr("Get Fortune")))
    , tcpSocket(new QTcpSocket(this))
{
    ...
    in.setDevice(tcpSocket);
    in.setVersion(QDataStream::Qt_6_5);

El protocolo está basado en QDataStream, así que establecemos el dispositivo de flujo al socket recién creado. A continuación, establecemos explícitamente la versión del protocolo del flujo a QDataStream::Qt_6_5 para asegurarnos de que estamos utilizando la misma versión que el servidor de fortuna, independientemente de la versión de Qt que utilicen el cliente y el servidor.

Las únicas señales QTcpSocket que necesitamos en este ejemplo son QTcpSocket::readyRead(), que significa que se han recibido datos, y QTcpSocket::errorOccurred(), que utilizaremos para detectar cualquier error de conexión:

    ...
    connect(tcpSocket, &QIODevice::readyRead, this, &Client::readFortune);
    connect(tcpSocket, &QAbstractSocket::errorOccurred,
    ...
}

Al hacer clic en el botón Get Fortune se invocará la ranura requestNewFortune():

void Client::requestNewFortune()
{
    getFortuneButton->setEnabled(false);
    tcpSocket->abort();
    tcpSocket->connectToHost(hostCombo->currentText(),
                             portLineEdit->text().toInt());
}

Como permitimos que el usuario pulse Get Fortune antes de que la conexión anterior haya terminado de cerrarse, empezamos abortando la conexión anterior llamando a QTcpSocket::abort(). (En un socket desconectado, esta función no hace nada.) Entonces procedemos a conectar con el servidor de fortuna llamando a QTcpSocket::connectToHost(), pasando el nombre de host y el puerto de la interfaz de usuario como argumentos.

Como resultado de llamar a connectToHost(), una de dos cosas puede suceder:

  • Se establece la conexión. En este caso, el servidor nos enviará una fortuna. QTcpSocket emitirá readyRead() cada vez que reciba un bloque de datos.
  • Se produce un error. Necesitamos informar al usuario si la conexión ha fallado o se ha roto. En este caso, QTcpSocket emitirá errorOccurred(), y se llamará a Client::displayError().

Veamos primero el caso errorOccurred():

void Client::displayError(QAbstractSocket::SocketError socketError)
{
    switch (socketError) {
    case QAbstractSocket::RemoteHostClosedError:
        break;
    case QAbstractSocket::HostNotFoundError:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The host was not found. Please check the "
                                    "host name and port settings."));
        break;
    case QAbstractSocket::ConnectionRefusedError:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The connection was refused by the peer. "
                                    "Make sure the fortune server is running, "
                                    "and check that the host name and port "
                                    "settings are correct."));
        break;
    default:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The following error occurred: %1.")
                                 .arg(tcpSocket->errorString()));
    }

    getFortuneButton->setEnabled(true);
}

Mostramos todos los errores en un diálogo usando QMessageBox::information(). QTcpSocket::RemoteHostClosedError es silenciosamente ignorado, porque el protocolo del servidor de fortuna termina con el servidor cerrando la conexión.

Ahora la alternativa readyRead(). Esta señal está conectada a Client::readFortune():

void Client::readFortune()
{
    in.startTransaction();

    QString nextFortune;
    in >> nextFortune;

    if (!in.commitTransaction())
        return;

    if (nextFortune == currentFortune) {
        QTimer::singleShot(0, this, &Client::requestNewFortune);
        return;
    }

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

Ahora, TCP se basa en el envío de un flujo de datos, por lo que no podemos esperar obtener toda la fortuna de una sola vez. Especialmente en una red lenta, los datos pueden recibirse en varios fragmentos pequeños. QTcpSocket almacena en búfer todos los datos entrantes y emite readyRead() por cada nuevo bloque que llega, y es nuestro trabajo asegurarnos de que hemos recibido todos los datos que necesitamos antes de empezar a parsear.

Para ello utilizamos una transacción de lectura QDataStream. Sigue leyendo los datos del flujo en un búfer interno y los devuelve en caso de lectura incompleta. Comenzamos llamando a startTransaction() que también reinicia el estado del flujo para indicar que se han recibido nuevos datos en el socket. Procedemos utilizando el operador de flujo de QDataStream para leer la fortuna del socket en QString. Una vez leída, completamos la transacción llamando a QDataStream::commitTransaction(). Si no recibimos un paquete completo, esta función restaura los datos del flujo a la posición inicial, tras lo cual podemos esperar una nueva señal readyRead().

Después de una transacción de lectura exitosa, llamamos a QLabel::setText() para mostrar la fortuna.

Proyecto de ejemplo @ code.qt.io

Vea también Servidor de Fortuna y Cliente de Fortuna Bloqueante.

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