Developpez.com - C++
X

Choisissez d'abord la catégorieensuite la rubrique :


Programmation réseau en Qt4

Par Kinji1
 

Cet article a pour but de présenter comment utiliser le module QtNetwork afin de réaliser une application client-serveur.

            

Introduction: le module QtNetwork
I. Intéraction client-serveur avec TCP
I-A. Coté serveur
I-A-1. Déclaration des classes
I-A-2. Implémentation des classes
I-B. Coté client
I-B-1. Déclaration des classes
I-B-2. Implémentation des classes
II. Intéraction client-serveur avec UDP
II-A. Coté serveur
II-B. Coté client


Introduction: le module QtNetwork


I. Intéraction client-serveur avec TCP


I-A. Coté serveur

Nous allons créer un serveur qui écoutera un port choisi et qui instanciera, lorsqu'un client se connectera sur celui-ci, un objet qui s'occupera de celui-ci. Cela permet ainsi de différencier facilement les données de différents clients.
De plus le programme possédera une mini interface graphique afin de visualiser que celui-ci est en activité. On pourra s'affranchir de celle-ci, si l'on souhaite réaliser un serveur démon.


I-A-1. Déclaration des classes

Commençons par décrire les deux classes utilisées par le programme : La classe Dlg, qui s'occupera à la fois de l'interface graphique mais également de l'écoute des nouvelles connexions. Et la classe Client, qui s'occupera des de chaque client connecté au serveur.

Voici un squelette sommaire de la classe Dlg, seules les parties concernant l'interface graphique sont présentes.
dlg.h

#ifndef DLG_H
#define DLG_H

#include <QDialog>

class QLabel;

class Dlg: public QDialog
{
	Q_OBJECT

	QLabel* lblInfo;

public:
	Dlg(QWidget* parent = 0);
};

#endif	
Du point de vue de l'interface graphique, la classe hérite de QDialog, et ne n'affiche qu'un simple QLabel.
Afin de pouvoir écouter sur un port TCP, nous devons utiliser un QTcpServer. Nous allons donc ajouter un membre privé à notre classe :
dlg.h

	QTcpServer* serv;
Bien sûr il ne faut pas oublier de déclarer la class QTcpServer avant le début de notre classe.
dlg.h

class QTcpServer;
Qt utilise le système de signals/slots [ref] pour la communication entre objets. QTcpServer utilise le signal newConnection() pour signaler, comme son nom l'indique, une nouvelle connexion. Il faut donc prévoir un slot qui sera appelé par ce signal et qui s'occupera d'instancier l'objet s'occupant du client. Ceci se fait en ajoutant, p.ex. sous les membres privés, les lognes suivantes :
dlg.h

private slots:
	void connexion();
Passons à la classe Client, qui va s'occuper des communications entre le serveur et le client. Nous allons commencer à partir du squelette suivant :
client.h

#ifndef CLIENT_H
#define CLIENT_H

#include <QObject>

class Client : public QObject
{
	Q_OBJECT

public:
	Client(QObject* parent = 0);
};

#endif
La gestion des communications TCP repose sur la classe QTcpSocket, qui s'occupe de la connexion et prévient l'application des divers événements qui se produisent au travers de signaux. Il faut donc ajouter un membre privé à notre classe.
client.h

	QTcpSocket* socket;
Comme nous le verrons dans la partie implémentation, ce socket provient du QTcpServer et doit être fourni à la classe. Nous allons donc modifier le constructeur actuel, afin de lui passer un pointeur sur le socket.
client.h

	Client(QTcpSocket* socket, QObject* parent = 0);
Enfin, passons à la gestion des signaux. Parmis les signaux que peut émettre QTcpSocket, les plus importants sont:
- connected(), émis lorsque la connexion est établie
- disconnected(), émis lorsque la connexion se termine
- readyRead(), émis lorsque de nouvelles données sont reçues
- error(QAbstractSocket::SocketError), émis lorqu'une erreur se produit

Nous n'utiliserons que les trois derniers signaux et pour cela nous allons définir les différents slots qui seront connectés à ces signaux.
client.h

private slots:
	void deconnexion();
	void erreur(QAbstractSocket::SocketError);
	void messageRecu();
Il ne reste plus qu'à inclure l'entête qui va déclarer QTcpSocket et QAbstractSocket::SocketError.
client.h

#include <QTcpSocket>
Voila, les différentes classes sont maintenant déclarées, il ne reste plus qu'à implémenter les diverses fonctions.


I-A-2. Implémentation des classes

Commençons par le serveur et sa boîte de dialogue. Le fichier de départ est présenté ci-dessous. Celui-ci inclus les divers entêtes nécessaires. Ici l'inclusion de QtGui est assez exagéré puisque que seuls QDialog, Qlabel et QHBoxLayout sont utilisés, cependant pour des interfaces plus complexes, cela évite d'inclure les différents composants de l'interface.
La mise en place de l'interface graphique dans le constructeur ne sera pas détaillé puisque ce n'est pas le sujet de cet article et que celle-ci est assez simple.
dlg.cpp - squelette initial

#include <QtGui>
#include <QTcpServer>
#include <QTcpSocket>
#include "dlg.h"
#include "client.h"

Dlg::Dlg(QWidget* parent)
	:QDialog(parent)
{
	lblInfo = new QLabel("");
	QHBoxLayout* layout = new QHBoxLayout;
	layout->addWidget(lblInfo);
	setLayout(layout);
}

void Dlg::connexion()
{}
Il faut maintenant remplir les différentes fonctions. Nous allons commencer par le constructeur. La première chose à faire est d'allouer un QTcpServer dont l'adresse sera conservée dans serv. Nous appelons le constructeur de QTcpServer avec comme argument this, afin que celui-ci soit le fils de notre boîte de dialogue. Ainsi, Qt prend en charge la destruction du serveur à la destruction de la boîte de dialogue.
Puisque nous avons notre serveur, il faut lui dire de commencer son écoute. C'est de que nous faisons en appelant la fonction listen(const QHostAddress& address, quint16 port). Cette fonction spécifie simplement au serveur d'écouter sur l'adresse et le port spécifiés, QHostAddress::Any signifiant d'écouter sur toutes les adresses possibles.
Cet appel retourne un booléen qui indique si l'opération a réussie ou s'il s'est produit une erreur. Dans les deux cas, on indique l'état du serveur.

Le serveur est normalement actif et peut recevoir des connexions. Celles-ci vont être signalées par le serveur par l'émission du signal newConnection(), il faut donc connecter ce signal à notre slot connection().
dlg.cpp - Dlg::Dlg(QWidget* parent)

	serv = new QTcpServer(this);
	if(serv->listen(QHostAddress::Any, 15042))
		lblInfo->setText(tr("Le serveur écoute sur le port 15042."));
	else
		lblInfo->setText(tr("Le serveur ne peux pas écouter le port."));
		
	connect(serv, SIGNAL(newConnection()), this, SLOT(connexion()));
Notre constructeur est terminé, passons au slot qui prend en charge les nouvelles connexions. Son rôle va simplement être d'instancier un objet Client pour chaque connexion.
Lorsqu'un client cherche à se connecter au serveur, Qt place cette connexion au travers de son socket dans un espace d'attente et émet le signal newConnection(). Nous devons donc récupérer les sockets grâce aux méthodes hasPendingConnections() et nextPendingConnection(). La première permet d'itérer dans un while tant que des connexions sont en attentes. La seconde nous renvoit un pointeur sur le QTcpSocket qui correspond à la connexion et nous créons un objet Client en lui passant ce pointeur et spécifiant la boîte de dialogue comme parent.
dlg.cpp - Dlg::connexion()

	while(serv->hasPendingConnections())
	{
		QTcpSocket* socket = serv->nextPendingConnection();
		new Client(socket, this);
	}
Et c'est tout, nous avons maintenant une petite interface et un serveur qui écoute et instancie un objet pour chaque nouveau client qui se connecte. Bien sûr tout ce qui concerne l'échange de données entre le serveur et le client va se faire dans cet objet qu'il ne nous reste plus qu'à implémenter.

Nous commencerons à partir du squelette suivant, qui ne fait que définir des fonctions sans instructions.
client.cpp - squelette initial

#include <QTcpSocket>
#include "client.h"

Client::Client(QTcpSocket* socket, QObject* parent)
	:QObject(parent)
{}

void Client::deconnexion()
{}

void Client::erreur(QAbstractSocket::SocketError)
{}

void Client::messageRecu()
{}
Lorsque le client se connecte, le serveur va simplement attendre une requête de celui-ci, il n'y a donc rien à faire dans le constructeur hormis récupérer le socket donné en argument du constructeur et effectuer les connexions entre les signaux et les slots. Ceci est fait en ajoutant ces quelques lignes au constructeur.
client.cpp - Client::Client(QTcpSocket* socket, QObject* parent)

	this->socket = socket;
	connect(socket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
	connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(erreur(QAbstractSocket::SocketError)));
	connect(socket, SIGNAL(readyRead()), this, SLOT(messageRecu()));
[Faut peut-etre finir...]


I-B. Coté client

Maintenant, intéressons nous au client qui va se connecter à notre serveur. Une interface graphique simple permettra de spécifier tous les paramètres et d'interagir avec notre serveur.


I-B-1. Déclaration des classes

Ici il n'y aura qu'une seule classe à réaliser, celle de l'interface qui se chargera en plus de réaliser les connexions vers le serveur.
Commençons donc directement avec son squelette.
dlg.h - squelette initial

#ifndef DLG_H
#define DLG_H

#include <QtGui>

class Dlg : public QDialog
{
	Q_OBJECT

	QLabel *lblServeur;
	QLineEdit *edtServeur;
	QLabel *lblPort;
	QLineEdit *edtPort;
	QPushButton *btnConnexion;
	QPushButton *btnDeconnexion;
	QPushButton *btnHeure;
	QLabel *lblHeure;
	QLabel *lblStatut;

public:
	Dlg(QWidget *parent = 0);

};

#endif
Comme on peut le voir, la classe hérite aussi de QDialog et présente divers éléments qui ont été déclarés par l'include de QtGui.
Pour réaliser la connexion vers le serveur nous auront besoin d'un QTcpSocket, que nous rajoutons à notre classe.
dlg.h

QTcpSocket *socket;
Maintenant, nous allons rajouter différents slots. Certains seront connectés aux boutons de l'interface graphique, pour que l'on puisse interagir avec notre application. Les autres reliés aux signaux émis par notre QTcpSocket.

private slots:
	//slots pour l'interface
	void connexion();
	void demandeHeure();
	void deconnexion();
	//slots pour le socket
	void connecte();
	void deconnecte();
	void erreur(QAbstractSocket::SocketError);
	void messageRecu();
Il ne reste plus qu'à inclure l'entête pour déclarer QTcpSocket et il ne restera plus qu'à implémenter notre client.
dlg.h

#include <QTcpSocket>

I-B-2. Implémentation des classes

Le fichier de départ présenté ci dessous inclut les divers entêtes nécessaires et prépare l'interface graphique de l'application dans le constructeur de notre classe. Pour décrire rapidement ce qui est réalisé, les différents éléments de l'interface sont d'abord instanciés puis ajoutés à un layout pour être placés correctement dans la boîte de dialogue.
Les dernières lignes du constructeur rendent utilisable le bouton qui permet de se connecter et désactivent les autres boutons, qui ne doivent être utilisables qu'une fois connecté.
dlg.cpp - squelete initial

#include <QtGui>
#include <QTcpSocket>
#include "dlg.h"

Dlg::Dlg(QWidget *parent)
	:QDialog(parent)
{
	lblServeur = new QLabel(tr("Serveur:"));
	edtServeur = new QLineEdit("localhost");
	lblPort = new QLabel(tr("Port:"));
	edtPort = new QLineEdit("15042");
	edtPort->setValidator(new QIntValidator(1, 65535, this));
	btnConnexion = new QPushButton(tr("Connexion"));
	btnDeconnexion = new QPushButton(tr("Déconnexion"));
	btnHeure = new QPushButton(tr("Heure"));
	lblHeure = new QLabel(tr("Heure: Non reçue"));
	lblStatut = new QLabel(tr("Statut: déconnecté"));
	
	QGridLayout *layout = new QGridLayout;
	layout->addWidget(lblServeur, 0, 0);
	layout->addWidget(edtServeur, 0, 1);
	layout->addWidget(lblPort, 0, 2);
	layout->addWidget(edtPort, 0, 3);
	layout->addWidget(btnConnexion, 0, 4);
	layout->addWidget(btnHeure, 1, 0);
	layout->addWidget(lblHeure, 1, 1, 1, 3);
	layout->addWidget(btnDeconnexion, 1, 4);
	layout->addWidget(lblStatut, 2, 0, 1, 5);
	setLayout(layout);
	
	btnConnexion->setEnabled(true);
	btnDeconnexion->setEnabled(false);
	btnHeure->setEnabled(false);
}

void Dlg::connexion()
{}

void Dlg::deconnexion()
{}

void Dlg::demandeHeure()
{}

void Dlg::connecte()
{}

void Dlg::deconnecte()
{}

void Dlg::messageRecu()
{}

void Dlg::erreur(QAbstractSocket::SocketError socketError)
{}
Commençons par terminer le constructeur de notre classe. Il ne nous reste plus qu'à instancier le socket et à réaliser les différentes connexions. Il nous faut une connexion pour chacun des boutons et une connexion pour les signaux importants du socket.

connect(btnConnexion, SIGNAL(clicked()), this, SLOT(connexion()));
connect(btnHeure, SIGNAL(clicked()), this, SLOT(demandeHeure()));
connect(btnDeconnexion, SIGNAL(clicked()), this, SLOT(deconnexion()));

socket = new QTcpSocket();
connect(socket, SIGNAL(connected()), this, SLOT(connecte()));
connect(socket, SIGNAL(disconnected()), this, SLOT(deconnecte()));
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(erreur(QAbstractSocket::SocketError)));
connect(socket, SIGNAL(readyRead()), this, SLOT(messageRecu()));
Voyons maintenant, les implémentations des différents slots dans l'ordre de leur utilisation.
Tout d'abord la fonction connexion() s'occupe d'établir une connexion au serveur. Elle appelle pour cela la fonction connectToHost(const QString& hostName, quint16 port) de notre socket en lui donnant l'addresse du serveur ainsi que le port à utiliser. Le reste des instructions change simplement le texte d'état et désactive le bouton de connexion.
dlg.cpp - Dlg::connexion()

lblStatut->setText(tr("Statut: Connexion"));
btnConnexion->setEnabled(false);
socket->connectToHost(edtServeur->text(), edtPort->text().toInt());
					<paragraph>
						Ensuite le client attend que la tentative de connexion aboutisse.
						Cela est souvent bref, mais peut prendre plus de temps en cas
						de problèmes, par exemple si aucun serveur ne répond. L'utilisation
						des signaux et slots permet de ne pas bloquer l'application
						durant cette attente.<br/>
						Lorsque la connexion est établie, notre socket émet le signal <b>connected()</b>
						et la fonction <b>connecte()</b> est alors appelée. Celle-ci
						active simplement les boutons utilisables et change le texte d'état.
					</paragraph>
<code langage="cpp" titre="dlg.cpp - Dlg::connecte()">
btnDeconnexion->setEnabled(true);
btnHeure->setEnabled(true);
lblStatut->setText(tr("Statut: Connecté"));
Une fois connecté, on veut pouvoir exécuter notre requête au serveur, à savoir lui demander l'heure. Il faut donc implémenter le slot demandeHeure() pour qu'il exécute cette requête.
Dans le code ci-dessous, on vérifie tout d'abord que l'on se trouve bien dans un état connecté, via l'accesseur state de notre QTcpSocket. En effet, une erreur dans la conception de l'application pourrait laisser actif le bouton alors que l'on n'est pas ou plusconnecté. Il est donc toujours utile de vérfier que l'on est bien connecté avant de vouloir envoyer des données.
L'envoi de données correpond simplement à une écriture d'une chaîne dans notre socket, la requête étant très simple dans ce protocole.
dlg.cpp - Dlg::demandeHeure()

if(socket->state() == QAbstractSocket::ConnectedState)
{
	socket->write("HEURE\n");
}
Comme précédemment, on attend ensuite une réponse à notre requête, sans pour autant bloquer notre application grâce au mécanismes signal/slot. Lorsque l'on recevra une réponse du serveur, le signal readyRead() sera émis par le socket. En fait celui-ci est émis à chaque fois qu'une nouvelle donnée arrive.
Le protocole étant basé sur des requêtes et réponses par ligne, on cherche d'abord à lire une ligne sur notre socket, que l'on traitera. Puisqu'il est possible que plusieurs réponses arrivent soit en même temps, soit simplement avant que le programme n'entre dans la fonction, il faut réaliser ce traitement tant qu'il est possible de lire des lignes de réponses du serveur.
On vérifie la présence d'une ligne avec canReadLine() et on lit cette ligne avec readLine(), celle-ci renvoit un QByteArray que l'on convertit en en QString pour la suite.
Viens ensuite le traitement de la réponse. Ici il n'y a qu'une réponse possible de la part du serveur, cependant pour des protocoles plus fournis, c'est là que l'on traitera, ou du moins on différenciera, les différentes réponses. On utilise ici les expressions régulières pour vérifier le format de la requête via la classe QRegExp. L'utilisation de cette classe, ni des expressions régulières en général ne sera pas détaillée ici, reportez-vous entre autres à la documentation de Qt [ref] sur ce sujet. On crée donc une expression régulière vérifiant la présence des différentes informations de l'heure. Puis on vérifie si la chaîne reçue correspond bien à celle-ci. En cas de succès, on affiche l'heure que l'on a extrait de la réponse du serveur.
dlg.cpp - Dlg::messageRecu()

while(socket->canReadLine())
{
	QString ligne = QString(socket->readLine());
	
	QRegExp expr("HEURE: (\\d\\d:\\d\\d:\\d\\d)\\n");
	if(expr.exactMatch(ligne))
	{
		lblHeure->setText(expr.cap(1));
		lblStatut->setText(tr("Statut: heure reçue"));
	}
	else
	{
		lblStatut->setText(tr("Statut: message inconnu"));
	}
}
Enfin, il faut implémenter la fonction qui assure la déconnexion du serve. Son code est tès simple, puisq'il suffit d'appeler la fonction close() du socket.
dlg.cpp - Dlg::deconnexion()

	socket->close();
Même si toutes les fonctions importantes gérant la connexions et les requêtes ont été traitées, il nous reste encore un slot à implémenter. Il s'agit de celui qui va s'occuper de traiter les différentes erreurs qui peuvent survenir lors de la connexion.
Il existe plus d'une dizaine d'erreurs différentes que peut rapporter le signal error(QAbstractSocket::SocketError socketError), cependant seules quelques unes ne sont vraiment utiles à interpréter poour notre client.

Voici l'implémentation de cette fonction. Elle consiste en un switch sur les différentes erreurs et affiche une boîte de dialogue pour la signaler. Ici l'implémentation des différentes erreurs en diffère que par le message affiché, mais on peut imaginer un comportement différent de l'application selon l'erreur qui s'est produite. Enfin, on ferme explicitement le socket et on remet les boutons dans leur état initial.
dlg.cpp - Dlg::erreur(QAbstractSocket::SocketError socketError)

switch(socketError)
{
case QAbstractSocket::ConnectionRefusedError:
	QMessageBox::critical(this, tr("Erreur de connexion"), tr("Connexion refusée."));
	break;
case QAbstractSocket::HostNotFoundError:
	QMessageBox::critical(this, tr("Erreur de connexion"), tr("Serveur non trouvé."));
	break;
case QAbstractSocket::SocketTimeoutError:
	QMessageBox::critical(this, tr("Erreur de connexion"), tr("Le serveur met trop de temps à répondre."));
	break;
case QAbstractSocket::RemoteHostClosedError:
	QMessageBox::critical(this, tr("Erreur de connexion"), tr("Le serveur a interrompu la connexion."));
	break;
default:
	QMessageBox::critical(this, tr("Erreur de connexion"), tr("Erreur de connexion."));
}
socket->close();
btnConnexion->setEnabled(true);
btnDeconnexion->setEnabled(false);
btnHeure->setEnabled(false);
lblStatut->setText(tr("Statut: déconnecté"));
Voilà, c'est fini pour notre unique classe. Il ne reste plus qu'à écrire la fonction main qui crée une QApplication, notre interface graphique et lance l'application.
main.cpp

#include <QApplication>
#include "dlg.h"

int main(int argc, char** argv)
{
	QApplication app(argc, argv);
	Dlg gui;
	gui.show();
	return app.exec();
}

II. Intéraction client-serveur avec UDP


II-A. Coté serveur


II-B. Coté client



            

Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 Kinji1. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Contacter le responsable de la rubrique C++