Mostrando entradas con la etiqueta TCP/IP. Mostrar todas las entradas
Mostrando entradas con la etiqueta TCP/IP. Mostrar todas las entradas

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




{ Leer Más }


miércoles, 10 de julio de 2013

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

clip_image002

En esta entrada mostraremos un ejemplo de cómo crear un servidor para un servicio de red simple, a través del protocolo TCP/IP. En próximas entradas mostraremos un cliente TCP/IP que se conectara al servidor que a continuación explicaremos.

clip_image004

El servidor usa la clase QtcpServer para aceptar las conexiones TCP entrantes, y un protocolo de trasferencia de datos sencillo basado en la clase QdataStream, para escribir un texto aleatorio al cliente conectado, antes de cerrar la conexión.

class Server : public QDialog
{
Q_OBJECT

public:
Server(QWidget *parent = 0);

private slots:
void sendFortune();

private:
QLabel *statusLabel;
QPushButton *quitButton;
QTcpServer *tcpServer;
QStringList fortunes;
};

Este servidor es implementado usando una clase simple con solo un slot, para manejar las conexiones entrantes.

tcpServer = new QTcpServer(this);
if (!tcpServer->listen()) {
QMessageBox::critical(this, tr("Fortune Server"),
tr("Unable to start the server: %1.")
.arg(tcpServer->errorString()));
close();
return;
}
QString ipAddress;
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
// use the first non-localhost IPv4 address
for (int i = 0; i < ipAddressesList.size(); ++i) {
if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
ipAddressesList.at(i).toIPv4Address()) {
ipAddress = ipAddressesList.at(i).toString();
break;
}
}
// if we did not find one, use IPv4 localhost
if (ipAddress.isEmpty())
ipAddress = QHostAddress(QHostAddress::LocalHost).toString();
statusLabel->setText(tr("The server is running on\n\nIP: %1\nport: %2\n\n"
"Run the Fortune Client example now.")
.arg(ipAddress).arg(tcpServer->serverPort()));

En este constructor, nuestro objeto Server llama a QtcpServer::listen() para indicar a la clase QtcpServer que escuche todas las direcciones, por un puerto arbitrario. En la pantalla se muestra una etiqueta con el puerto que fue escogido para que QtcpServer escuche, de esta forma los usuarios conocen a que puerto se debe conectar la aplicación cliente, para recibir mensajes aleatorios de “fortuna”.

fortunes << tr("You've been leading a dog's life. Stay off the furniture.")

<< tr("You've got to think about tomorrow.")

<< tr("You will be surprised by a loud noise.")

<< tr("You will feel hungry again in another hour.")

<< tr("You might have mail.")

<< tr("You cannot kill time without injuring eternity.")

<< tr("Computers are not intelligent. They only think they are.");

Nuestro servidor genera una lista aleatoria de textos de “fortuna”, que pueden ser enviados a los clientes TCP conectados.

connect(tcpServer, SIGNAL(newConnection()), this, SLOT(sendFortune()));

Cuando un cliente se conecta a nuestro servidor, el objeto de la clase QtcpServer emitirá la señal QtcpServer::newConnection(). En consecuencia, se invocara nuestro slot sendFortune().

void Server::sendFortune()
{
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_0);

El propósito de este slot es seleccionar una línea aleatoria de nuestra lista de textos de “fortuna”, codificarla dentro de un QByteArray usando la clase QdataStream, y entonces escribirlo hacia el socket conectado. Esta es una vía común de transmitir datos binarios usando QtcpSocket. Primero crearemos un QbyteArray y un objeto QdataStream, pasando el bytearray al constructor de la clase QdataStream. Entonces especificamos explícitamente la versión del protocolo de QdataStream a QdataStream::Qt_4_0 para asegurar que nos podamos comunicar con clientes de futuras versiones de Qt.

     out << (quint16)0;
out << fortunes.at(qrand() % fortunes.size());
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16));

Al iniciar nuestro QbyteArray, reservamos espacio para un entero de 16 bits que contendrá el tamaño total de los bloques de datos que serán enviados. Luego colocamos en el QbyteArray un texto aleatorio de “fortuna”. Entonces saltamos hacia el inicio del QbyteArray, y sobrescribimos el valor del entero de 16 bits que habíamos reservado, colocando con el tamaño total del arreglo. Haciendo esto proveemos una vía a los clientes para verificar la cantidad de datos que ellos deben esperar antes de leer el paquete completo.

QTcpSocket *clientConnection = tcpServer->nextPendingConnection();

connect(clientConnection, SIGNAL(disconnected()),

clientConnection, SLOT(deleteLater()));


Entonces llamamos al método QtcpServer::newPendingConnection(), el cual retorna el objeto QtcpSocket representando la conexión del lado del servidor. Conectando QTcpSocket::disconnected() al método Qobject::deleteLater(), nos aseguramos que el socket será eliminado luego de que nos desconectemos.

clientConnection->write(block);

clientConnection->disconnectFromHost();

}


El texto de “fortuna” codificado es escrito usando el método QtcpSocket::write(), y finalmente llamamos al método QtcpSocket::disconnectFromHost(), el cual cerrará la conexión después de que QtcpSocket haya terminado de escribir el texto de “fortuna” a través de la red. Debido a que QtcpSocket trabaja de forma asíncrona, los datos serán escritos después de que la función retorne y el control regrese al lazo de eventos de Qt. El socket entonces será cerrado, lo cual causará una llamada al método Qobject::deleteLater() para eliminarlo.

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

{ Leer Más }


IconIconIcon