Comme je vous l'ai dit, nous allons devoir réaliser 2 projets :
- Un projet "client"
- Un projet "serveur"
Nous commençons par le serveur.
Création du projet
Créez un nouveau projet constitué de 3 fichiers :
- main.cpp
- FenServeur.cpp
- FenServeur.h
Editez le fichier .pro pour demander à Qt de rajouter la gestion du réseau :
Code : Autre1
2
3
4
5
6
7
8
9
10
11
12
13
| ######################################################################
# Automatically generated by qmake (2.01a) mer. 25. juin 14:54:55 2008
######################################################################
TEMPLATE = app
QT += network
TARGET =
DEPENDPATH += .
INCLUDEPATH += .
# Input
HEADERS += FenServeur.h
SOURCES += FenServeur.cpp main.cpp |
Avec
QT += network, Qt sait que le projet va utiliser le réseau et peut préparer un makefile approprié.
La fenêtre du serveur
Le serveur est une application qui tourne en tâche de fond. Normalement, rien ne nous oblige à créer une fenêtre pour ce projet, mais on va quand même en faire une pour que l'utilisateur puisse arrêter le serveur en fermant la fenêtre.
Notre fenêtre sera toute simple, elle affichera le texte "
Le serveur a été lancé sur le port XXXX" et un bouton "
Quitter".
Construire la fenêtre sera donc très simple, la vraie difficulté sera de faire toute la gestion du réseau derrière.
main.cpp
Les main sont toujours très simples et classiques avec Qt :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12 | #include <QApplication>
#include "FenServeur.h"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
FenServeur fenetre;
fenetre.show();
return app.exec();
}
|
FenServeur.h
Voici maintenant le header de la fenêtre du serveur :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | #ifndef HEADER_FENSERVEUR
#define HEADER_FENSERVEUR
#include <QtGui>
#include <QtNetwork>
class FenServeur : public QWidget
{
Q_OBJECT
public:
FenServeur();
void envoyerATous(const QString &message);
private slots:
void nouvelleConnexion();
void donneesRecues();
void deconnexionClient();
private:
QLabel *etatServeur;
QPushButton *boutonQuitter;
QTcpServer *serveur;
QList<QTcpSocket *> clients;
quint16 tailleMessage;
};
#endif
|
Notre fenêtre hérite de QWidget, ce qui nous permet de créer une fenêtre simple. Elle est constituée comme vous le voyez d'un QLabel et d'un QPushButton comme prévu.
En plus de ça, j'ai rajouté d'autres attributs spécifiques à la gestion du réseau :
- QTcpServer *serveur : c'est l'objet qui représente le serveur sur le réseau.
- QList<QTcpSocket *> clients : c'est un tableau qui contient la liste des clients connectés. On aurait pu utiliser un tableau classique, mais on va passer par une QList, un tableau de taille dynamique. En effet, on ne connaît pas à l'avance le nombre de clients qui se connecteront. Chaque QTcpSocket de ce tableau représentera une connexion à un client.
- quint16 tailleMessage : ce quint16 sera utilisé dans le code pour se "souvenir" de la taille du message que le serveur est en train de recevoir. Nous en avons déjà parlé et nous en reparlerons plus loin.
Voilà, à part ces attributs, on note que la classe est constituée de plusieurs méthodes (dont des slots) :
- Le constructeur : il initialise les widgets sur la fenêtre et initialise aussi le serveur (QTcpServer) pour qu'il démarre.
- envoyerATous() : une méthode à nous qui se charge d'envoyer à tous les clients connectés le message passé en paramètre.
- Slot nouvelleConnexion() : appelé lorsqu'un nouveau client se connecte.
- Slot donneesRecues() : appelé lorsque le serveur reçoit des données. Attention, c'est là que c'est délicat, car ce slot est appelé à chaque sous-paquet reçu. Il faudra "attendre" d'avoir reçu le nombre d'octets indiqués dans tailleMessage avant de pouvoir considérer qu'on a reçu le message entier.
- Slot deconnexionClient() : appelé lorsqu'un client se déconnecte.
Implémentons ces méthodes en coeur, dans la joie et la bonne humeur !
FenServeur.cpp
Le constructeur
Le constructeur se charge de placer les widgets sur la fenêtre et de faire démarrer le serveur via le QTcpServer :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | FenServeur::FenServeur()
{
// Création et disposition des widgets de la fenêtre
etatServeur = new QLabel;
boutonQuitter = new QPushButton(tr("Quitter"));
connect(boutonQuitter, SIGNAL(clicked()), qApp, SLOT(quit()));
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(etatServeur);
layout->addWidget(boutonQuitter);
setLayout(layout);
setWindowTitle(tr("ZeroChat - Serveur"));
// Gestion du serveur
serveur = new QTcpServer(this);
if (!serveur->listen(QHostAddress::Any, 50885)) // Démarrage du serveur sur toutes les IP disponibles et sur le port 50585
{
// Si le serveur n'a pas été démarré correctement
etatServeur->setText(tr("Le serveur n'a pas pu être démarré. Raison :<br />") + serveur->errorString());
}
else
{
// Si le serveur a été démarré correctement
etatServeur->setText(tr("Le serveur a été démarré sur le port <strong>") + QString::number(serveur->serverPort()) + tr("</strong>.<br />Des clients peuvent maintenant se connecter."));
connect(serveur, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
tailleMessage = 0;
}
|
J'ai fait en sorte de bien commenter mes codes sources pour vous aider du mieux possible à comprendre ce qui se passe.
Vous voyez bien une première étape où on dispose les widgets sur la fenêtre (classique, rien de nouveau) et une seconde étape où on démarre le serveur.
Quelques précisions sur la seconde étape, la plus intéressante pour ce chapitre. On crée un nouvel objet de type QTcpServer dans un premier temps (ligne 16). On lui passe en paramètre this, un pointeur vers la fenêtre, pour faire en sorte que la fenêtre soit le parent du QTcpServer. Cela permet de faire en sorte que le serveur soit automatiquement détruit lorsqu'on quitte la fenêtre.
Ensuite, on essaie de démarrer le serveur grâce à
serveur->listen(QHostAddress::Any, 50885). Il y a 2 paramètres :
- L'IP : c'est l'IP sur laquelle le serveur "écoute" si de nouveaux clients arrivent. Comme je vous l'avais dit, un ordinateur peut avoir plusieurs IP : une IP interne (127.0.0.1), une IP pour le réseau local, une IP sur internet, etc. La mention QHostAddress::Any autorise toutes les connexions : internes (clients connectés sur la même machine), locales (clients connectés sur le même réseau local) et externes (clients connectés via internet).
- Le port : c'est le numéro du port sur lequel on souhaite lancer le serveur. J'ai choisi un numéro au hasard, compris entre 1 024 et 65 536. J'aurais aussi pu omettre ce paramètre, dans ce cas le serveur aurait choisi un port libre au hasard. N'hésitez pas à changer la valeur si le port n'est pas libre chez vous.
La méthode listen() renvoie un booléen : vrai si le serveur a bien pu se lancer, faux s'il y a eu un problème. On affiche un message en conséquence sur la fenêtre du serveur.
Si le démarrage du serveur a fonctionné, on connecte le signal newConnection() vers notre slot personnalisé nouvelleConnexion() pour traiter l'arrivée d'un nouveau client sur le serveur.
Si tout va bien, la fenêtre suivante devrait donc s'ouvrir :
S'il y a une erreur, vous aurez un message d'erreur adapté. Par exemple, essayez de lancer une seconde fois le serveur alors qu'un autre serveur tourne déjà :
Ici, comme le port 50885 est déjà utilisé par un 1er serveur, notre 2nd serveur n'a pas le droit de démarrer sur ce port. D'où l'erreur.
Slot nouvelleConnexion()
Ce slot est appelé dès qu'un nouveau client se connecte au serveur :
Code : C++ | void FenServeur::nouvelleConnexion()
{
envoyerATous(tr("<em>Un nouveau client vient de se connecter</em>"));
QTcpSocket *nouveauClient = serveur->nextPendingConnection();
clients << nouveauClient;
connect(nouveauClient, SIGNAL(readyRead()), this, SLOT(donneesRecues()));
connect(nouveauClient, SIGNAL(disconnected()), this, SLOT(deconnexionClient()));
}
|
On envoie à tous les clients déjà connectés un message comme quoi un nouveau client vient de se connecter. On verra le contenu de la méthode envoyerATous() un peu plus loin.
Chaque client est représenté par un QTcpSocket. Pour récupérer la socket correspondant au nouveau client qui vient de se connecter, on appelle la méthode nextPendingConnection() du QTcpServer. Cette méthode retourne la QTcpSocket du nouveau client.
Comme je vous l'ai dit, on conserve la liste des clients connectés dans un tableau, appelé
clients.
Ce tableau est géré par la classe QList qui est très simple d'utilisation. On ajoute le nouveau client à la fin du tableau très facilement, comme ceci :
Code : C++ | clients << nouveauClient;
|
(vive la surcharge de l'opérateur <<
)
On connecte ensuite les signaux que peut envoyer le client à des slots. On va gérer 2 signaux :
- readyRead() : signale que le client a envoyé des données. Ce signal est émis pour chaque sous-paquet reçu. Lorsqu'un client enverra un message, ce signal pourra donc être émis plusieurs fois jusqu'à ce que tous les sous-paquets soient arrivés.
C'est notre slot personnalisé donneesRecues() (qui sera coton à écrire
) qui traitera les sous-paquets.
- disconnected() : signale que le client s'est déconnecté. Notre slot se chargera d'informer les autres clients de son départ et de supprimer la QTcpSocket correspondante dans la liste des clients connectés.
Slot donneesRecues()
Voilà sans aucun doute LE point le plus délicat de ce chapitre. C'est un slot qui va être appelé à chaque fois qu'on reçoit un sous-paquet d'un des clients.
On a au moins 2 problèmes pas évidents à résoudre :
- Comme on va recevoir plusieurs sous-paquets, il va falloir "attendre" d'avoir tout reçu avant de pouvoir dire qu'on a reçu le message en entier.
- C'est le même slot qui est appelé quel que soit le client qui a envoyé un message. Du coup, comment savoir quel est le client à l'origine du message pour récupérer les données ?
Il faut utiliser l'objet QTcpSocket du client pour récupérer les sous-paquets qui ont transité par le réseau. Le problème, c'est qu'on a connecté les signaux de tous les clients à un même slot :
Comment le slot sait-il dans quelle QTcpSocket lire les données ?
Vous ne pouviez pas trop le deviner, et à vrai dire je ne savais pas moi-même comment faire avant d'écrire ce chapitre

.
Il se trouve que j'ai découvert qu'on pouvait appeler la méthode
sender() de QObject dans le slot pour retrouver un pointeur vers l'objet à l'origine du message. Très pratique !

Nouveau problème : cette méthode renvoie systématiquement un QObject (classe générique de Qt) car elle ne sait pas à l'avance de quel type sera l'objet. Notre objet QTcpSocket sera donc représenté par un QObject.
Pour le transformer à nouveau en QTcpSocket, il faudra forcer sa conversion à l'aide de la méthode qobject_cast().
En résumé, pour obtenir un pointeur vers la bonne QTcpSocket à l'origine du signal, il faudra écrire :
Code : C++ | QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
|
Ce qui, schématiquement, revient à faire ceci :
- On utilise sender() pour déterminer l'objet à l'origine du signal.
- Comme sender() renvoie systématiquement un QObject, il faut le transformer à nouveau en QTcpSocket. Pour cela, on passe l'objet en paramètre à la méthode qobject_cast(), on indiquant entre les chevrons le type de retour que l'on souhaite obtenir : <QTcpSocket *>.
La méthode qobject_cast() est similaire au dynamic_cast() de la bibliothèque standard du C++. Son rôle est de forcer la transformation d'un objet d'un type vers un autre.
Il se peut que le qobject_cast() n'ait pas fonctionné (par exemple parce que l'objet n'était pas de type QTcpSocket contrairement à ce qu'on attendait). Dans ce cas, il renvoie 0. Il faut que l'on teste si le qobject_cast() a fonctionné avant d'aller plus loin.
On va faire un return qui va arrêter la méthode s'il y a eu un problème :
Code : C++ | QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
if (socket == 0) // Si par hasard on n'a pas trouvé le client à l'origine du signal, on arrête la méthode
return;
|
On peut ensuite travailler à récupérer les données. On commence par créer un flux de données pour lire ce que contient la socket :
Code : C++
Notre objet "in" va nous permettre de lire le contenu du sous-paquet que vient de recevoir la socket du client.
C'est maintenant que l'on va utiliser l'entier tailleMessage défini en tant qu'attribut de la classe. Si lors de l'appel au slot ce tailleMessage vaut 0, cela signifie qu'on est en train de recevoir le début d'un nouveau message.
On demande à la socket combien d'octets ont été reçus dans le sous-paquet grâce à la méthode bytesAvailable(). Si on a reçu moins d'octets que la taille d'un quint16, on arrête la méthode de suite. On attendra le prochain appel de la méthode pour vérifier à nouveau si on a reçu assez d'octets pour récupérer la taille du message.
Code : C++ | if (tailleMessage == 0) // Si on ne connaît pas encore la taille du message, on essaie de la récupérer
{
if (socket->bytesAvailable() < (int)sizeof(quint16)) // On n'a pas reçu la taille du message en entier
return;
in >> tailleMessage; // Si on a reçu la taille du message en entier, on la récupère
}
|
La ligne 6 est exécutée uniquement si on a reçu assez d'octets. En effet, le return a arrêté la méthode avant si ce n'était pas le cas.
On récupère donc la taille du message et on la stocke. On la "retient" pour la suite des opérations.
Pour bien comprendre ce code, il faut se rappeler que le paquet est découpé en sous-paquets :
Notre slot est appelé à chaque fois qu'un sous-paquet a été reçu.
On vérifie si on a reçu assez d'octets pour récupérer la taille du message (première section en gris foncé). La taille de la première section "tailleMessage" peut être facilement retrouvée grâce à l'opérateur sizeof() que vous avez probablement déjà utilisé.
Si on n'a pas reçu assez d'octets, on arrête la méthode (return). On attendra que le slot soit à nouveau appelé et on vérifiera alors cette fois si on a reçu assez d'octets.
Maintenant la suite des opérations. On a reçu la taille du message. On va maintenant essayer de récupérer le message lui-même :
Code : C++ | // Si on connaît la taille du message, on vérifie si on a reçu le message en entier
if (socket->bytesAvailable() < tailleMessage) // Si on n'a pas encore tout reçu, on arrête la méthode
return;
|
Le principe est le même. On regarde le nombre d'octets reçus, et si on en a moins que la taille annoncée du message, on arrête (return).
Si tout va bien, on peut passer à la suite de la méthode. Si ces lignes s'exécutent, c'est qu'on a reçu le message en entier, donc qu'on peut le récupérer dans une QString :
Code : C++ | // Si ces lignes s'exécutent, c'est qu'on a reçu tout le message : on peut le récupérer !
QString message;
in >> message;
|
Notre QString "message" contient maintenant le message envoyé par le client !
Ouf ! Le serveur a reçu le message du client !
Mais ce n'est pas fini : il faut maintenant renvoyer le message à tous les clients comme je vous l'avais expliqué. Pour reprendre notre exemple, Vincent vient d'envoyer un message au serveur, celui-ci l'a récupéré et s'apprête à le renvoyer à tout le monde.
L'envoi du message à tout le monde se fait via la méthode envoyerATous dont je vous ai déjà parlé et qu'il va falloir écrire.
Code : C++ | // 2 : on renvoie le message à tous les clients
envoyerATous(message);
|
On a presque fini. Il manque juste une petite chose : remettre tailleMessage à 0 pour que l'on puisse recevoir de futurs messages d'autres clients :
Code : C++ | // 3 : remise de la taille du message à 0 pour permettre la réception des futurs messages
tailleMessage = 0;
|
Si on n'avait pas fait ça, le serveur aurait cru lors du prochain sous-paquet reçu que le nouveau message est de la même longueur que le précédent, ce qui n'est certainement pas le cas.
Bon, résumons le slot en entier :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | void FenServeur::donneesRecues()
{
// 1 : on reçoit un paquet (ou un sous-paquet) d'un des clients
// On détermine quel client envoie le message (recherche du QTcpSocket du client)
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
if (socket == 0) // Si par hasard on n'a pas trouvé le client à l'origine du signal, on arrête la méthode
return;
// Si tout va bien, on continue : on récupère le message
QDataStream in(socket);
if (tailleMessage == 0) // Si on ne connaît pas encore la taille du message, on essaie de la récupérer
{
if (socket->bytesAvailable() < (int)sizeof(quint16)) // On n'a pas reçu la taille du message en entier
return;
in >> tailleMessage; // Si on a reçu la taille du message en entier, on la récupère
}
// Si on connaît la taille du message, on vérifie si on a reçu le message en entier
if (socket->bytesAvailable() < tailleMessage) // Si on n'a pas encore tout reçu, on arrête la méthode
return;
// Si ces lignes s'exécutent, c'est qu'on a reçu tout le message : on peut le récupérer !
QString message;
in >> message;
// 2 : on renvoie le message à tous les clients
envoyerATous(message);
// 3 : remise de la taille du message à 0 pour permettre la réception des futurs messages
tailleMessage = 0;
}
|
J'espère avoir été clair, car ce slot n'est pas simple et pas très facile à lire je dois bien avouer. La clé, le truc à comprendre, c'est que chaque return arrête la méthode. Le slot sera à nouveau appelé au prochain sous-paquet reçu, donc ces instructions s'exécuteront probablement plusieurs fois pour un message.
Si la méthode arrive à s'exécuter jusqu'au bout, c'est qu'on a reçu le message en entier.
Slot deconnexionClient()
Ce slot est appelé lorsqu'un client se déconnecte.
On va envoyer un message à tous les clients encore connectés pour qu'ils sachent qu'un client vient de partir. Puis, on supprime la QTcpSocket correspondant au client dans notre tableau QList. Ainsi, le serveur "oublie" ce client, il ne considère plus qu'il fait partie des connectés.
Voici le slot en entier :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13 | void FenServeur::deconnexionClient()
{
envoyerATous(tr("<em>Un client vient de se déconnecter</em>"));
// On détermine quel client se déconnecte
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
if (socket == 0) // Si par hasard on n'a pas trouvé le client à l'origine du signal, on arrête la méthode
return;
clients.removeOne(socket);
socket->deleteLater();
}
|
Comme plusieurs signaux sont connectés à ce slot, on ne sait pas quel est le client à l'origine de la déconnexion. Pour le retrouver, on utilise la même technique que pour le slot donneesRecues(), je ne la réexplique donc pas.
La méthode removeOne() de QList permet de supprimer le pointeur vers l'objet dans le tableau. Notre liste des clients est maintenant à jour.
Il ne reste plus qu'à finir de supprimer l'objet lui-même (nous venons seulement de supprimer le pointeur de la QList là).
Pour supprimer l'objet, il faudrait faire un
delete client;. Petit problème : si on supprime l'objet à l'origine du signal, on risque de faire bugger Qt. Heureusement tout a été prévu : on a juste à appeler deleteLater() (qui signifie "supprimer plus tard") et Qt se chargera de faire le delete lui-même un peu plus tard, lorsque notre slot aura fini de s'exécuter.
Méthode envoyerATous()
Ah, cette fois ce n'est pas un slot.

C'est juste une méthode que j'ai décidé d'écrire dans la classe pour bien séparer le code, et aussi parce qu'on en a besoin plusieurs fois (vous avez remarqué que j'ai appelé cette méthode plusieurs fois dans les codes précédents non ?).
Dans le slot donneesRecues, nous recevions un message. Là, nous voulons au contraire en envoyer un, et ce à tous les clients connectés (tous les clients présents dans la QList).
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | void FenServeur::envoyerATous(const QString &message)
{
// Préparation du paquet
QByteArray paquet;
QDataStream out(&paquet, QIODevice::WriteOnly);
out << (quint16) 0; // On écrit 0 au début du paquet pour réserver la place pour écrire la taille
out << message; // On ajoute le message à la suite
out.device()->seek(0); // On se replace au début du paquet
out << (quint16) (paquet.size() - sizeof(quint16)); // On écrase le 0 qu'on avait réservé par la longueur du message
// Envoi du paquet préparé à tous les clients connectés au serveur
for (int i = 0; i < clients.size(); i++)
{
clients[i]->write(paquet);
}
}
|
Quelques explications bien sûr.

On crée un QByteArray "paquet" qui va contenir le paquet à envoyer sur le réseau. La classe QByteArray représente une suite d'octets quelconque.
On utilise un QDataStream comme tout à l'heure pour écrire dans le QByteArray facilement. Cela va nous permettre d'utiliser l'opérateur "<<".
Ce qui est particulier, c'est qu'
on écrit d'abord le message (QString) et ensuite on calcule sa taille qu'on écrit au début du message.
Voilà ce qu'on fait sur le paquet dans l'ordre :
- On écrit le nombre 0 de type quint16 pour "réserver" de la place.
- On écrit à la suite le message, de type QString. Le message a été reçu en paramètre de la méthode envoyerATous().
- On se replace au début du paquet (comme si on remettait le curseur au début d'un texte dans un traitement de texte).
- On écrase le 0 qu'on avait écrit pour réserver de la place par la bonne taille du message. Cette taille est calculée via une simple soustraction : la taille du message est égale à la taille du paquet moins la taille réservée pour le quint16.
Notre paquet est prêt. Nous allons l'envoyer à tous les clients grâce à la méthode write() du socket.
Pour cela, on fait une boucle sur la QList, et on envoie le message à chaque client.
Et voilà, le message est parti !
FenServeur.cpp en entier
Voici le contenu du fichier FenServeur.cpp que je viens de décortiquer en entier :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 | #include "FenServeur.h"
FenServeur::FenServeur()
{
// Création et disposition des widgets de la fenêtre
etatServeur = new QLabel;
boutonQuitter = new QPushButton(tr("Quitter"));
connect(boutonQuitter, SIGNAL(clicked()), qApp, SLOT(quit()));
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(etatServeur);
layout->addWidget(boutonQuitter);
setLayout(layout);
setWindowTitle(tr("ZeroChat - Serveur"));
// Gestion du serveur
serveur = new QTcpServer(this);
if (!serveur->listen(QHostAddress::Any, 50885)) // Démarrage du serveur sur toutes les IP disponibles et sur le port 50585
{
// Si le serveur n'a pas été démarré correctement
etatServeur->setText(tr("Le serveur n'a pas pu être démarré. Raison :<br />") + serveur->errorString());
}
else
{
// Si le serveur a été démarré correctement
etatServeur->setText(tr("Le serveur a été démarré sur le port <strong>") + QString::number(serveur->serverPort()) + tr("</strong>.<br />Des clients peuvent maintenant se connecter."));
connect(serveur, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
tailleMessage = 0;
}
void FenServeur::nouvelleConnexion()
{
envoyerATous(tr("<em>Un nouveau client vient de se connecter</em>"));
QTcpSocket *nouveauClient = serveur->nextPendingConnection();
clients << nouveauClient;
connect(nouveauClient, SIGNAL(readyRead()), this, SLOT(donneesRecues()));
connect(nouveauClient, SIGNAL(disconnected()), this, SLOT(deconnexionClient()));
}
void FenServeur::donneesRecues()
{
// 1 : on reçoit un paquet (ou un sous-paquet) d'un des clients
// On détermine quel client envoie le message (recherche du QTcpSocket du client)
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
if (socket == 0) // Si par hasard on n'a pas trouvé le client à l'origine du signal, on arrête la méthode
return;
// Si tout va bien, on continue : on récupère le message
QDataStream in(socket);
if (tailleMessage == 0) // Si on ne connaît pas encore la taille du message, on essaie de la récupérer
{
if (socket->bytesAvailable() < (int)sizeof(quint16)) // On n'a pas reçu la taille du message en entier
return;
in >> tailleMessage; // Si on a reçu la taille du message en entier, on la récupère
}
// Si on connaît la taille du message, on vérifie si on a reçu le message en entier
if (socket->bytesAvailable() < tailleMessage) // Si on n'a pas encore tout reçu, on arrête la méthode
return;
// Si ces lignes s'exécutent, c'est qu'on a reçu tout le message : on peut le récupérer !
QString message;
in >> message;
// 2 : on renvoie le message à tous les clients
envoyerATous(message);
// 3 : remise de la taille du message à 0 pour permettre la réception des futurs messages
tailleMessage = 0;
}
void FenServeur::deconnexionClient()
{
envoyerATous(tr("<em>Un client vient de se déconnecter</em>"));
// On détermine quel client se déconnecte
QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
if (socket == 0) // Si par hasard on n'a pas trouvé le client à l'origine du signal, on arrête la méthode
return;
clients.removeOne(socket);
socket->deleteLater();
}
void FenServeur::envoyerATous(const QString &message)
{
// Préparation du paquet
QByteArray paquet;
QDataStream out(&paquet, QIODevice::WriteOnly);
out << (quint16) 0; // On écrit 0 au début du paquet pour réserver la place pour écrire la taille
out << message; // On ajoute le message à la suite
out.device()->seek(0); // On se replace au début du paquet
out << (quint16) (paquet.size() - sizeof(quint16)); // On écrase le 0 qu'on avait réservé par la longueur du message
// Envoi du paquet préparé à tous les clients connectés au serveur
for (int i = 0; i < clients.size(); i++)
{
clients[i]->write(paquet);
}
}
|
Lancement du serveur
Bonne nouvelle, devinez quoi : notre projet "serveur" est terminé !
Nous avons fait le plus dur, l'implémentation du serveur dans FenServeur.cpp. Compilez, et lancez le serveur ainsi créé.
Vous risquez d'avoir une alerte de votre pare-feu (firewall). Par exemple sous Windows :
En effet, notre programme va communiquer sur le réseau. Le pare-feu nous demande si nous voulons autoriser notre programme à le faire : répondez oui en cliquant sur "Débloquer".
Dans cet exemple, j'ai considéré que le pare-feu de votre système d'exploitation était votre seul pare-feu. Si vous comptez utiliser le Chat sur internet et que vous êtes derrière un routeur, vérifiez la configuration du routeur et ouvrez le port 50885 pour les données TCP.
Si le Chat n'a pas l'air de fonctionner, c'est très probablement à cause d'un pare-feu quelque part qui bloque le port.
Notre serveur est maintenant lancé :
Bravo !

Laissez ce programme tourner en fond sur votre ordinateur (vous pouvez réduire la fenêtre). Il va servir à faire la communication entre les différents clients.
Bon, mauvaise nouvelle chers auditeurs : nous avons beaucoup sué, mais nous avons fait seulement 50% du travail !
Il faut maintenant s'attaquer au projet "client" pour réaliser le programme qui sera utilisé par tous les clients pour chatter. Heureusement, nous avons déjà fait le plus dur en analysant le slot donneesRecues, on devrait donc aller un peu plus vite.
