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 :
Bien sûr il ne faut pas oublier de déclarer la class QTcpServer
avant le début de notre classe.
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.
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.
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.
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:
void connexion ();
void demandeHeure ();
void deconnexion ();
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.
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() |
|
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.
- ConnectionRefusedError: La connexion a été rejetée par le serveur ou la tentative a atteint le timeout.
- HostNotFoundError : L'adresse du serveur est introuvable.
- SocketTimeoutError : Une action sur le socket (par exemple une écriture) a dépassé le temps limite.
- RemoteHostClosedError : Le serveur a fermé la connexion, ce qui ferme aussi notre socket.
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


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