Création du projet

Créer un nouveau projet sous QT :

Le qtCreator se trouve dans le répertoire :

C:\Qt\Qt5.12.9\Tools\QtCreator\bin\


Créez un nouveau projet Qt Widgets Application constitué de 3 fichiers :

  • main.cpp
  • FenServeur.cpp
  • FenServeur.h


Cette page explique les différentes étapes, il se peut que le projet ne compile pas. Il faut continuer la lecture et faire les modifications comme indiqué dans le document.



Donner comme nom au projet QtConversationServeur comme suit :

Faites suivant (Build system).


Puis Choisissez dans Class name : FenServeur

Faites suivant (Translation).

Faites suivant (Kits).

Puis Terminer

Vous obtenez ceci :


Editez le fichier .pro pour demander à Qt de rajouter la gestion du réseau :

QT += widgets network


Avec QT += widgets 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.

Vérifier que le main.cpp corresponde à ce qui est en dessous : 

main.cpp

#include <QApplication>

#include "FenServeur.h"


int main(int argc, char* argv[])

{

    QApplication app(argc, argv);


    FenServeur fenetre;

    fenetre.show();


    return app.exec();

}


On va éditer l'IHM fenserveur.ui en double cliquant dessus :


Ceci fera apparaître l'éditeur intégré de QtDesigner :



Redimensionner la fenêtre.


Ajouter un Push Button :


Changer le deux propriétes "objectName" et "text" comme suit :



et un texte de présentation de la fenêtre en utilisant un label :


En cliquant sur la fenêtre principale, on peut changer le texte de la fenêtre principale :


Gestion des slots ou événements liées à l'appui sur un bouton :



Dans la fenêtre droit du mode "design" , faites un clic-droit sur l'objet btnQuitter puis "Aller au slot..."



Choisissez clicked()


Cela aura pour effet de vous mettre sur la source pour gérer l'événement :



On utilisera ces deux lignes dans la méthode on_btnQuitter_clicked():


            this->serveur->close();

            QApplication::exit();


pour quitter proprement l'application.



Modification du header "fenserveur.h"


Dans le header, nous aurons besoin d'ajouter 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.

Informations sur les QList, considérez juste que c'est une classe qui permet de gérer un tableau de taille variable, ce qui est très pratique quand on ne sait pas comme ici le nombre de clients qui vont se connecter.

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

On ajoutera également 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 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 on_btnQuitter_clicked() : appelé lorsqu'un client se déconnecte.



FenServeur.h

Voici maintenant le header de la fenêtre du serveur :


#ifndef FENSERVEUR_H

#define FENSERVEUR_H


#include <QMainWindow>

#include <QtWidgets>

#include <QtNetwork>


QT_BEGIN_NAMESPACE

namespace Ui { class FenServeur; }

QT_END_NAMESPACE


class FenServeur : public QMainWindow

{

    Q_OBJECT


public:

    FenServeur(QWidget *parent = nullptr);    //constructeur

    ~FenServeur();                            //destructeur

    void envoyerATous(const QString &message);//envoi un message à tous les clients


private slots:

    void on_btnQuitter_clicked();              //quitte proprement l'application

    void nouvelleConnexion();

    void donneesRecues();

    void deconnexionClient();


private:

    Ui::FenServeur *ui;                        //le formulaire

    QLabel *etatServeur;                        //le bouton label

    QPushButton *boutonQuitter;                 //le bouton quitter


    QTcpServer *serveur;                        //le serveur

    QList<QTcpSocket *> clients;                //la liste des clients connectées

    quint16 tailleMessage;                      //un message

};

#endif // FENSERVEUR_H




FenServeur.cpp

Le constructeur se charge de placer les widgets sur la fenêtre et de faire démarrer le serveur via le QTcpServer :

FenServeur::FenServeur(QWidget *parent) : QMainWindow(parent), ui(new Ui::FenServeur)

{

    ui->setupUi(this);


    // 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

        ui->txtEtatServeur->setText(tr("Le serveur n'a pas pu être démarré. Raison :<br />") + serveur->errorString());

    }

    else

    {

        // Si le serveur a été démarré correctement

        ui->txtEtatServeur->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;

}


On crée un nouvel objet de type QTcpServer dans un premier temps (ligne en fluo jaune). 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. 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. C'est un numéro au hasard, compris entre 1 024 et 65 535. A adapter si le port est déjà utilisé.

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.

Voici le code source pour pouvoir tester la fermeture de la fenêtre ainsi que le message d'erreur en cas de second serveur.

Consigner vos résultats dans votre compte rendu.

fenServeur.cpp


#include "fenserveur.h"

#include "ui_fenserveur.h"


FenServeur::FenServeur(QWidget *parent) : QMainWindow(parent), ui(new Ui::FenServeur)

{

    ui->setupUi(this);


    // 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

        ui->txtEtatServeur->setText(tr("Le serveur n'a pas pu être démarré. Raison :<br />") + serveur->errorString());

    }

    else

    {

        // Si le serveur a été démarré correctement

        ui->txtEtatServeur->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;

}

FenServeur::~FenServeur()

{

    delete ui;

}


void FenServeur::on_btnQuitter_clicked()

{

    this->serveur->close();

    QApplication::exit();

}

void FenServeur::nouvelleConnexion(){}

void FenServeur::donneesRecues(){};

void FenServeur::deconnexionClient(){};


Complétons les 3 méthodes manquantes :

Slot nouvelleConnexion()

Ce slot est appelé dès qu'un nouveau client se connecte au serveur :


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.

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 :


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 cette activité. 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 :

Plusieurs signaux connectés à un slot


Comment le slot sait-il dans quelle QTcpSocket lire les données ?

Une méthode sender() de QObject dans le slot permet de 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 :

QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());


Ce qui, schématiquement, revient à faire ceci :

Sélection du signal et retransformation en QTcpSocket



  1. On utilise sender() pour déterminer l'objet à l'origine du signal.
  2. 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 :

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 :

QDataStream in(socket);

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.

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 :

Séparation 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().
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 :

// 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 :

// 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 !


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 

// 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 :

// 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 :

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;

}

Ce slot est le plus compliqué à 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 :

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().

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 undelete 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()

On crée bien une méthode et pas un slot !
Séparer du code, cette méthode est appelé plusieurs fois parce qu'on en a besoin plusieurs fois.

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

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

    }


}


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 :

  1. On écrit le nombre 0 de type quint16 pour "réserver" de la place.
  2. On écrit à la suite le message, de type QString. Le message a été reçu en paramètre de la méthode envoyerATous().
  3. 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).
  4. Paquet

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 en entier :

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

    }


}


et le fenserveur.h


#ifndef FENSERVEUR_H

#define FENSERVEUR_H


#include <QMainWindow>

#include <QtWidgets>

#include <QtNetwork>


QT_BEGIN_NAMESPACE

namespace Ui { class FenServeur; }

QT_END_NAMESPACE


class FenServeur : public QMainWindow

{

    Q_OBJECT


public:

    FenServeur(QWidget *parent = nullptr);    //constructeur

    ~FenServeur();                            //destructeur

    void envoyerATous(const QString &message);//envoi un message à tous les clients


private slots:

    void on_btnQuitter_clicked();              //quitte proprement l'application

    void nouvelleConnexion();

    void donneesRecues();

    void deconnexionClient();


private:

    Ui::FenServeur *ui;                        //le formulaire

    QLabel *etatServeur;                        //le bouton label

    QPushButton *boutonQuitter;                 //le bouton quitter


    QTcpServer *serveur;                        //le serveur

    QList<QTcpSocket *> clients;                //la liste des clients connectées

    quint16 tailleMessage;                      //un message

};

#endif // FENSERVEUR_H



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é :


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.

Allez à la suite 

Créé avec HelpNDoc Personal Edition: Faites de la documentation d'aide un jeu d'enfant avec un outil de création d'aide