miércoles, 24 de julio de 2013

Desarrollo de un cliente TCP/IP con el Framework Qt.

clip_image002

En la entrada anterior mostramos la creación de un servidor para un servicio de red simple, a través del protocolo TCP/IP usando el Framework Qt; a continuación se explicara la implementación de un cliente TCP/IP que se conectara a dicho servidor.

clip_image004

Esta aplicación de ejemplo usa un protocolo de trasferencia de datos simple basado en la clase QDataStream para solicitar una línea de texto “fortuna” desde el servidor de textos aleatorios (que ya explicamos en la entrada anterior).

El cliente solicita un texto “fortuna” a través de una conexión simple al servidor. Entonces el servidor le responde con un entero de 16-bit (quint16) que contiene la longitud del texto “fortuna”, seguido por un QString.

QTcpSocket soporta dos mecanismos generales para abordar la programación en red:

  • El mecanismo asíncrono (no-bloqueante): Las operaciones son planificadas y ejecutadas cuando el control retorna a ciclo de eventos de Qt. Cuando las operación es finalizada, QTcpSocket emite una señal. Por ejemplo, QTcpSocket::connectToHost() devuelve inmediatamente, y cuando la conexión ha sido establecida, QTcpSocket emite la señal connected().
  • El mecanismo síncrono (bloqueante): En aplicaciones que no tengan interfaz gráfica y las que son concurrentes, usted puede llamar a la función waitFor… () (por ejemplo: QTcpSocket::waitForConnected()) para suspender la llamada al hilo hasta que las operaciones se hayan completado, en vez de conectar a las señales.

En este ejemplo, le demostraremos el mecanismo asíncrono.

Nuestra clase contiene algunos datos y varios slots privados:

class Client : public QDialog
{
Q_OBJECT
public:
Client(QWidget *parent = 0);
private slots:
void requestNewFortune();
void readFortune();
void displayError(QAbstractSocket::SocketError socketError);
void enableGetFortuneButton();
private:
QLabel *hostLabel;
QLabel *portLabel;
QLineEdit *hostLineEdit;
QLineEdit *portLineEdit;
QLabel *statusLabel;
QPushButton *getFortuneButton;
QPushButton *quitButton;
QDialogButtonBox *buttonBox;
QTcpSocket *tcpSocket;
QString currentFortune;
quint16 blockSize;
#ifdef Q_OS_SYMBIAN
bool isDefaultIapSet;
#endif
};

Además de los widgets que conforman la Interfaz Gráfica de Usuario, los miembros de clase incluyen un puntero a QTcpSocket, una copia del texto “fortuna” que se muestra actualmente, y el tamaño del paquete de datos que actualmente estamos leyendo.

El socket es inicializado en el constructor del Cliente (Client). Nosotros pasaremos el widget principal como padre, de esta forma no tendremos que preocuparnos por eliminar el socket:

Client::Client(QWidget *parent)
: QDialog(parent)
{
...
tcpSocket = new QTcpSocket(this);

Las únicas señales de QTcpSocket que necesitamos en este ejemplo son QTcpSocket::readyRead (), que representa cuando los datos han sido recibidos, y QTcpSocket::error (), el cual usaremos para capturar cualquier error de conexión:

 ...
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(readFortune()));
connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
...

Haciendo clic en el botón Get Fortune invocaremos el slot requestNewFortune ():

void Client::requestNewFortune()
{
getFortuneButton->setEnabled(false);
#ifdef Q_OS_SYMBIAN
if(!isDefaultIapSet) {
qt_SetDefaultIap();
isDefaultIapSet = true;
}
#endif
blockSize = 0;
tcpSocket->abort();
tcpSocket->connectToHost(hostLineEdit->text(),
portLineEdit->text().toInt());
}

En este slot, nosotros inicializamos blockSize a 0, preparando para leer un nuevo bloque de datos. Debido a que se permite al usuario hacer clic en el botón Get Fortune antes que la conexión previa haya finalizado el proceso de cierre, se pone en marcha abortando la conexión previa mediante una llamada a QTcpSocket::abort (). (En un socket desconectado, esta función no hace nada). Entonces procedemos a realizar la conexión al servidor de “Fortunas” mediante la llamada a QTcpSocket::connectToHost (), pasando como argumentos el nombre del host y el puerto, que se obtienen de la interfaz de usuario.

Como resultado de la llamada a connectToHost (), puede suceder una de estas dos acciones:

· La conexión es establecida. En ese caso, el servidor enviara un texto de “Fortuna”. QTcpSocket emitirá la señal readyRead () cada vez que reciba un bloque de datos.

· Ocurre un error. En este caso es necesario informar al usuario si la conexión fallo o fue abortada. De esta forma, QTcpSocket emitirá la llamada a error(), y la función Client::displayError () será llamada.

Revisemos primero el caso de la llamada a error ():

  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);
}

Los errores se muestran al usuario en forma de dialogo usando QMessageBox::information (). QTcpSocket::RemoteHostCloseError es silenciosamente ignorado, porque el protocolo del servidor de “Fortunas” finaliza con el cierre de la conexión al servidor.

Ahora para la alternativa de readyRead(). Esta señal es conectada al método Client::readFortune():

void Client::readFortune()
{
QDataStream in(tcpSocket);
in.setVersion(QDataStream::Qt_4_0);
if (blockSize == 0) {
if (tcpSocket->bytesAvailable() < (int)sizeof(quint16))
return;
in >> blockSize;
}
if (tcpSocket->bytesAvailable() < blockSize)
return;

El protocolo es basado en QDataStream, por lo tanto iniciamos creando un objeto flujo (stream), pasando el socket al constructor de QDataStream. Entonces se establece explícitamente la versión del protocolo para el flujo a QDataStream::Qt_4_0 para estar seguros que estamos usando la misma versión que el servidor de “Fortunas”, no importa cual versión de Qt están usando el cliente y el servidor.

Ahora, TCP es basado en el envío de flujos de datos, por lo tanto nosotros no podemos tener la expectativa de obtener el texto completo de la “fortuna” en un solo intento. Especialmente en una red lenta, los datos pueden ser recibidos en muchos fragmentos pequeños. QTcpSocket almacenara en buffers todos los datos entrantes y emitirá readyRead() para cada nuevo bloque que arribe, y es nuestro trabajo asegurarnos de que hemos recibido todos los datos que necesitamos antes de empezar el proceso de análisis. La respuesta del servidor inicia con el número de paquetes, de esta forma primero necesitamos asegurarnos de que podemos leer el tamaño, y entonces esperaremos hasta que QTcpSocket haya recibido el paquete completo.

QString nextFortune;
in >> nextFortune;
if (nextFortune == currentFortune) {
QTimer::singleShot(0, this, SLOT(requestNewFortune()));
return;
}
currentFortune = nextFortune;
statusLabel->setText(currentFortune);
getFortuneButton->setEnabled(true);
}

Se procede a usar el operador de flujo de la clase QDataStream para leer el texto “Fortuna” desde el socket hacia un QString. Una vez leído, podemos llamar al método QLabel::setText () para mostrar el texto “Fortuna”.

El código fuente de esta aplicación pueden descargarlo desde aquí.





¿Te ha gustado este Post? Compártelo con tus amigos.

No hay comentarios:

Publicar un comentario

IconIconIcon