Communication réseau client/serveur
Question
Concevez une application serveur et une application cliente dont les IHM sont conformes aux suggestions ci-dessous.
Le client comme le serveur doit pouvoir émettre et recevoir des informations.
La gestion des erreurs de communication doit être effective.

Solution
Le fichier CServeurTcp.h
Il s'agit de la définition de la classe de gestion du serveur TCP.
CServeurTcp
s'occupe :
D'initialiser/détruire le serveur TCP sur le port choisi.
Se met à l'écoute des clients.
Mémorise chaque client connecté.
Communique avec la classe IHM (
CIhmAppServeurTcp
) par des signaux/slots.
class CServeurTcp : public QTcpServer
{
Q_OBJECT
public:
explicit CServeurTcp(QObject *parent = 0);
explicit CServeurTcp(QObject *parent = 0, quint16 noPort = 2222);
~CServeurTcp();
int emettreVersClients(QString mess);
signals:
void sigEvenementServeur(QString eve);
void sigErreur(QAbstractSocket::SocketError err);
void sigDataClient(QString adrIpClient, QString data);
void sigAdrClient(QString adrClient);
void sigMaJClients(QList<QTcpSocket *> liste);
public slots:
void onNewConnectionClient();
void onDisconnectedClient();
void onErreurReseau(QAbstractSocket::SocketError err);
void onReadyReadClient();
private:
int init();
quint16 m_noPort;
QList<QTcpSocket *> listeClients;
};
// CSERVEURTCP_H
Vous notez qu'il y a 2 constructeurs, dont 1 paramétré. Par défaut, c'est le port
qui sera utilisé.PORTPARDEFAUT
5 signaux sont gérés :
sigEvenementServeur
: Pour informer l'IHM d'une connexion/déconnexion d'un client.sigDataClient
: Pour envoyer les données reçues vers l'IHM.sigErreur
: Pour signaler une erreur réseau.sigAdrClient
: Pour envoyer l'adresse IP du client connecté.sigMaJClients
: Pour envoyer la liste des clients connectés au serveur.
4 slots sont gérés :
onNewConnectionClient()
: S'exécute lors de la connexion d'un nouveau client.onDisconnectedClient()
: S'exécute lors de la déconnexion d'un client.onErreurReseau()
: S'exécute en cas d'erreur réseau.onReadyReadClient()
: S'exécute lorsqu'un client émet des caractères.
Le fichier CServeurTcp.cpp
Il contient la définition des méthodes de la classe CServeurTcp
.
CServeurTcp::CServeurTcp(QObject *parent) :
QTcpServer(parent)
{
m_noPort = PORTPARDEFAUT;
init();
}
CServeurTcp::CServeurTcp(QObject *parent, quint16 noPort) :
QTcpServer(parent)
{
m_noPort = noPort;
init();
}
CServeurTcp::~CServeurTcp()
{
// destruction optionnelle car déjà pris en charge par le serveur
for (int i=0 ; i<listeClients.size() ; i++) {
listeClients.at(i)->close();
delete listeClients.at(i);
} // for i
}
int CServeurTcp::emettreVersClients(QString mess)
{
for (int i=0 ; i<listeClients.size() ; i++) {
listeClients.at(i)->write(mess.toStdString().c_str());
qDebug() << "Envoi vers " << listeClients.at(i);
} // for i
return 1;
}
int CServeurTcp::init()
{
listen(QHostAddress::Any, m_noPort);
connect(this,SIGNAL(newConnection()), this, SLOT(onNewConnectionClient()));
connect(this, SIGNAL(acceptError(QAbstractSocket::SocketError)), this, SLOT(onErreurReseau(QAbstractSocket::SocketError)));
return 1;
}
/////////////////// SLOTs ////////////////////////
void CServeurTcp::onNewConnectionClient()
{
QString mess="Un client vient de se connecter";
qDebug() << mess;
QTcpSocket *newClient = this->nextPendingConnection();
qDebug() << "Nouvelle connexion : " << newClient;
if (newClient == NULL)
emit sigErreur(QAbstractSocket::ConnectionRefusedError);
connect(newClient, SIGNAL(readyRead()), this, SLOT(onReadyReadClient()));
connect(newClient, SIGNAL(disconnected()), this, SLOT(onDisconnectedClient()));
listeClients.append(newClient); // sauve l'adresse de l'objet dans la liste
emit sigEvenementServeur("CON");
emit sigAdrClient(newClient->localAddress().toString());
emit sigMaJClients(listeClients); // pour IHM
}
void CServeurTcp::onDisconnectedClient()
{
QTcpSocket *client = (QTcpSocket *)sender(); // Déterminer quel client ?
emit sigEvenementServeur("DEC");
listeClients.removeOne(client);
delete client;
emit sigMaJClients(listeClients);
}
void CServeurTcp::onErreurReseau(QAbstractSocket::SocketError err)
{
qDebug() << "Erreur réseau !";
emit sigErreur(err);
}
void CServeurTcp::onReadyReadClient()
{
QByteArray ba;
// Déterminer quel client ?
QTcpSocket *client = (QTcpSocket *)sender();
ba=client->readAll();
qDebug() << "Client : " << client << ba.size() << " Caractères reçus.";
emit sigDataClient(client->localAddress().toString(), QString(ba));
}
Les constructeurs appellent tous deux la méthode privée
init()
. Cette méthode initialise le serveur et le prépare à gérer une connexion avec un client.Le destructeur supprime tous les objets clients connecté avant que le serveur ne se termine. Cette action est optionnelle car la documentation signale que l'objet
QServeurTcp
est conçu pour supprimer automatiquement les objets qu'il a créé. Cependant, la documentation conseille malgré tout de le faire ! ! !emettreVersClients()
Permet d'émettre une information à partir du serveur vers tous les clients connectés.onNewConnectionClient()
mémorise l'adresse de l'objet du nouveau client connecté et la stocke dans une liste dynamique (QList
). La méthode envoie ensuite les signaux pour signifier la connexion, l'adresse IP du client, la liste mise à jour de tous les clients connectés.onDisconnectedClient()
gère la déconnexion d'un client en le supprimant, en mettant à jour la liste des clients connectés. La méthodesender()
permet d'obtenir l'adresse de l'objet ayant déclenché le slot.onErreurReseau()
ne fait qu'émettre le signal vers l'objet appelant (IHM).onReadyReadClient()
est exécuté lorsqu'un client envoi des octets au serveur. La méthodesender()
est encore utilisée pour savoir quel client a émis l'information. Après lecture des informations reçues, le signalsigDataClient()
est émis vers l'IHM.
Le fichier CIhmAppServeurTcp.h
La classe CIhmAppServeurTcp permet de gérer l'IHM de notre application serveur.
namespace Ui {
class CIhmAppServeurTcp;
}
class CIhmAppServeurTcp : public QMainWindow
{
Q_OBJECT
public:
explicit CIhmAppServeurTcp(QWidget *parent = 0);
~CIhmAppServeurTcp();
private slots:
void on_pbEnvoyer_clicked();
void onEvenementServeur(QString eve);
void onDataRecu(QString adrIpClient, QString data);
void onErreurServeur(QAbstractSocket::SocketError err);
void onAdrClient(QString adrClient);
void onListeMaJClients(QList<QTcpSocket *> liste);
private:
Ui::CIhmAppServeurTcp *ui;
CServeurTcp *serv;
};
// CIHMAPPSERVEURTCP_H
on_pbEnvoyer_clicked()
: Slot permettant l'envoi d'informations vers tous les clients connectés.onEvenementServeur()
: Slot permettant l'affichage d'états du serveur TCP (connexion/déconnexion).onDataRecu()
: Slot permettant l'affichage des informations reçues d'un client.onErreurServeur()
: Slot permettant d'afficher le texte d'une erreur de l'application.onAdrClient()
: Slot permettant l'affichage de l'adresse du client nouvellement connecté.onListeMaJClients()
: Slot mettant à jour la liste des client dans l'IHM.serv
: Pointeur vers le serveur TCP de l'application.
Le fichier CIhmAppServeurTcp.cpp
Définition des méthodes de la classe CIhmAppServeurTcp
.
CIhmAppServeurTcp::CIhmAppServeurTcp(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::CIhmAppServeurTcp)
{
ui->setupUi(this);
ui->pbEnvoyer->setEnabled(false);
serv = new CServeurTcp(this, PORT);
connect(serv,SIGNAL(sigEvenementServeur(QString)), this, SLOT(onEvenementServeur(QString)));
connect(serv, SIGNAL(sigDataClient(QString,QString)), this, SLOT(onDataRecu(QString,QString)));
connect(serv, SIGNAL(sigErreur(QAbstractSocket::SocketError)), this, SLOT(onErreurServeur(QAbstractSocket::SocketError)));
connect(serv, SIGNAL(sigAdrClient(QString)), this, SLOT(onAdrClient(QString)));
connect(serv, SIGNAL(sigMaJClients(QList<QTcpSocket*>)), this, SLOT(onListeMaJClients(QList<QTcpSocket*>)));
}
CIhmAppServeurTcp::~CIhmAppServeurTcp()
{
delete serv;
delete ui;
}
void CIhmAppServeurTcp::on_pbEnvoyer_clicked()
{
serv->emettreVersClients(ui->leTexte->text());
}
void CIhmAppServeurTcp::onEvenementServeur(QString eve)
{
if (eve=="DEC") {
ui->teTexte->append("Déconnexion d'un client.");
}
if (eve=="CON") {
ui->teTexte->append("Connexion d'un client.");
ui->pbEnvoyer->setEnabled(true);
}
}
void CIhmAppServeurTcp::onDataRecu(QString adrIpClient, QString data)
{
ui->teTexte->append(adrIpClient+": "+data);
}
void CIhmAppServeurTcp::onErreurServeur(QAbstractSocket::SocketError err)
{
switch (err) {
case QAbstractSocket::ConnectionRefusedError:
ui->teTexte->append("Connexion refusée par le serveur !");
break;
case QAbstractSocket::NetworkError:
ui->teTexte->append("Coupure de liaison réseau !");
break;
default:
ui->teTexte->append("Erreur réseau à déterminer !");
break;
} // sw
}
void CIhmAppServeurTcp::onAdrClient(QString adrClient)
{
ui->teTexte->append(adrClient+" est connecté.");
}
void CIhmAppServeurTcp::onListeMaJClients(QList<QTcpSocket *> liste)
{
ui->cbListe->clear();
for (int i=0 ; i<liste.size() ; i++) {
ui->cbListe->addItem(liste.at(i)->localAddress().toString()+ "->"+QString::number((unsigned long)liste.at(i)));
qDebug() << "CIhmAppServeurTcp::onListeMaJClients"
<< liste.at(i)->localAddress().toString()+ "->"+QString::number((unsigned long)liste.at(i));
} // for i
if (liste.size()==0)
ui->pbEnvoyer->setEnabled(false);
}
Constructeur : Instanciation du serveur TCP de l'application. Captage des signaux du serveur TCP.
onListeMaJClients()
: Met à jour la liste des clients dans le widgetQComboBox
.QList
: Cette classe permet la gestion d'une liste de n'importe quoi ! C'est pourquoi il faut lui préciser entre <> le type des informations à mémoriser. Dans notre cas, il s'agit d'adresses d'objets clients TCP de typeQTcpSocket
.
Le fichier CIhmAppServeurTcp.ui
Fichier XML contenant la description de l'IHM du serveur.
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CIhmAppServeurTcp</class>
<widget class="QMainWindow" name="CIhmAppServeurTcp">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1146</width>
<height>1002</height>
</rect>
</property>
<property name="windowTitle">
<string>Serveur TCP de base</string>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="QLineEdit" name="leTexte">
<property name="geometry">
<rect>
<x>20</x>
<y>60</y>
<width>861</width>
<height>61</height>
</rect>
</property>
</widget>
<widget class="QTextEdit" name="teTexte">
<property name="geometry">
<rect>
<x>20</x>
<y>220</y>
<width>1101</width>
<height>671</height>
</rect>
</property>
</widget>
<widget class="QPushButton" name="pbEnvoyer">
<property name="geometry">
<rect>
<x>900</x>
<y>70</y>
<width>215</width>
<height>48</height>
</rect>
</property>
<property name="text">
<string>Envoyer</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>781</width>
<height>39</height>
</rect>
</property>
<property name="text">
<string>Ecoute sur le port 2222</string>
</property>
</widget>
<widget class="QComboBox" name="cbListe">
<property name="geometry">
<rect>
<x>570</x>
<y>140</y>
<width>551</width>
<height>51</height>
</rect>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>30</x>
<y>140</y>
<width>541</width>
<height>39</height>
</rect>
</property>
<property name="text">
<string>Liste des clients connectés : </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1146</width>
<height>44</height>
</rect>
</property>
</widget>
<widget class="QToolBar" name="mainToolBar">
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>
Le fichier CClientTcp.h
Cette classe gère un client réseau.
class CClientTcp : public QObject
{
Q_OBJECT
public:
explicit CClientTcp(QObject *parent = 0);
~CClientTcp();
int emettre(QString mess);
int connecter(QString adr, QString port);
void deconnecter();
signals:
void sigData(QString data);
void sigErreur(QAbstractSocket::SocketError);
void sigEvenement(QString eve);
public slots:
void onConnected();
void onDisconnected();
void onReadyRead();
void onSocketError(QAbstractSocket::SocketError);
private:
QTcpSocket *sock;
};
// CCLIENTTCP_H
3 signaux sont gérés :
sigData()
: Pour envoyer les données reçues vers l'IHM.sigErreur()
: Pour signaler une erreur réseau.sigEvenement()
: Pour informer l'IHM d'une connexion/déconnexion d'un client.
4 slots sont gérés :
onConnected()
: S'exécute lors de la connexion au serveur.onDisconnected()
: S'exécute lors de la déconnexion du client.onSocketError()
: S'exécute en cas d'erreur réseau.onReadyRead()
: S'exécute lorsque le serveur envoi des informations au client.
Le fichier CClientTcp.cpp
CClientTcp::CClientTcp(QObject *parent) :
QObject(parent)
{
sock = new QTcpSocket(this);
connect(sock, SIGNAL(connected()), this, SLOT(onConnected()));
connect(sock, SIGNAL(disconnected()), this, SLOT(onDisconnected()));
connect(sock, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(sock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));
}
CClientTcp::~CClientTcp()
{
delete sock;
}
int CClientTcp::emettre(QString mess)
{
int nb = sock->write(mess.toStdString().c_str());
if (nb == -1)
qDebug() << "CClientTcp::emettre Erreur écriture.";
return nb;
}
int CClientTcp::connecter(QString adr, QString port)
{
sock->connectToHost(adr, port.toUShort(), QIODevice::ReadWrite);
if (!sock->isOpen())
qDebug() << "CClientTcp::connecter Erreur";
return 1;
}
void CClientTcp::deconnecter()
{
sock->close();
}
///////////// SLOTs /////////////////////////////////////
void CClientTcp::onConnected()
{
qDebug() << "Client connecté.";
emit sigEvenement("CON"); // communication avec IHM
}
void CClientTcp::onDisconnected()
{
qDebug() << "Client déconnecté.";
emit sigEvenement("DEC"); // communication avec IHM
}
void CClientTcp::onReadyRead()
{
int nb = sock->bytesAvailable();
qDebug() << nb << " octets à lire : ";
QByteArray data;
data = sock->readAll();
qDebug() << "CClientTcp::onReadyRead " << data;
emit sigData(QString(data)); // transmission à l'IHM par signal
}
void CClientTcp::onSocketError(QAbstractSocket::SocketError err)
{
qDebug() << "CClientTcp::onSocketError erreur !";
emit sigErreur(err);
}
Le constructeur crée un objet
QTcpSocket
permettant la gestion d'une communication avec le serveur. Le client TCP est préparé à gérer les signaux émis lors de la connexion/déconnexion au serveur, en cas d'erreur réseau, lorsque des informations sont reçues du serveur.connecter() : Utilise la méthode
ConnectToHost()
pour se connecter au serveur.onReadyRead()
: Lit tous les caractères envoyés par le serveur. Le signalsigData()
est ensuite émis pour avertir l'IHM de l'arrivée d'informations du serveur.
Le fichier CIhmAppClientTcp.h
Classe de gestion de l'IHM de l'application.
namespace Ui {
class CIhmAppClientTcp;
}
class CIhmAppClientTcp : public QMainWindow
{
Q_OBJECT
public:
explicit CIhmAppClientTcp(QWidget *parent = 0);
~CIhmAppClientTcp();
private slots:
void on_pbConnecter_clicked();
void on_pbEnvoyer_clicked();
void onEvenementClient(QString eve);
void onDataClient(QString data);
void onErrorClient(QAbstractSocket::SocketError err);
private:
Ui::CIhmAppClientTcp *ui;
CClientTcp *client;
};
// CIHMAPPCLIENTTCP_H
Le fichier CIhmAppClientTcp.cpp
CIhmAppClientTcp::CIhmAppClientTcp(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::CIhmAppClientTcp)
{
ui->setupUi(this);
ui->pbConnecter->setEnabled(true);
ui->pbEnvoyer->setEnabled(false);
client = new CClientTcp(this);
connect(client, SIGNAL(sigEvenement(QString)), this, SLOT(onEvenementClient(QString)));
connect(client, SIGNAL(sigErreur(QAbstractSocket::SocketError)), this, SLOT(onErrorClient(QAbstractSocket::SocketError)));
connect(client, SIGNAL(sigData(QString)), this, SLOT(onDataClient(QString)));
}
CIhmAppClientTcp::~CIhmAppClientTcp()
{
delete client;
delete ui;
}
////////////////////////////////////// SLOTs ///////////////////////////////////////////
/// IHM
void CIhmAppClientTcp::on_pbConnecter_clicked()
{
if (ui->pbConnecter->text() == "Connexion") {
client->connecter(ui->leAdrServeur->text(), ui->lePortServeur->text());
ui->pbConnecter->setText("Déconnexion");
} else {
client->deconnecter();
ui->pbConnecter->setText("Connexion");
} // else
}
void CIhmAppClientTcp::on_pbEnvoyer_clicked()
{
client->emettre(ui->leEmission->text());
ui->leEmission->clear();
ui->leEmission->setFocus();
}
/// AUTRES OBJETS
void CIhmAppClientTcp::onEvenementClient(QString eve)
{
if (eve == "CON") {
ui->pbEnvoyer->setEnabled(true);
ui->teReception->append("Connecté au serveur");
} // CON
if (eve=="DEC") {
ui->pbEnvoyer->setEnabled(false);
ui->teReception->append("Perte de connexion du serveur");
} // DEC
}
void CIhmAppClientTcp::onDataClient(QString data)
{
ui->teReception->append(data);
qDebug() << "CIhmAppClientTcp::onDataClient " << data;
}
void CIhmAppClientTcp::onErrorClient(QAbstractSocket::SocketError err)
{
switch (err) {
case QAbstractSocket::ConnectionRefusedError:
ui->teReception->append("Connexion refusée par le serveur !");
break;
case QAbstractSocket::NetworkError:
ui->teReception->append("Coupure de liaison réseau !");
break;
default:
ui->teReception->append("Erreur réseau à déterminer !");
break;
} // sw
}
Le fichier CIhmAppClientTcp.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CIhmAppClientTcp</class>
<widget class="QMainWindow" name="CIhmAppClientTcp">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1085</width>
<height>764</height>
</rect>
</property>
<property name="windowTitle">
<string>appClientTcp</string>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>353</width>
<height>151</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="laAdr">
<property name="text">
<string>Adresse du serveur : </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="laPort">
<property name="text">
<string>Port du serveur :</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>360</x>
<y>20</y>
<width>281</width>
<height>151</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLineEdit" name="leAdrServeur">
<property name="inputMask">
<string>999.999.999.999</string>
</property>
<property name="text">
<string>192.168.1.47</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lePortServeur">
<property name="inputMask">
<string>99999</string>
</property>
<property name="text">
<string>2222</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QPushButton" name="pbConnecter">
<property name="geometry">
<rect>
<x>670</x>
<y>37</y>
<width>391</width>
<height>121</height>
</rect>
</property>
<property name="text">
<string>Connexion</string>
</property>
</widget>
<widget class="QTextEdit" name="teReception">
<property name="geometry">
<rect>
<x>20</x>
<y>320</y>
<width>1041</width>
<height>321</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="leEmission">
<property name="geometry">
<rect>
<x>20</x>
<y>210</y>
<width>811</width>
<height>71</height>
</rect>
</property>
</widget>
<widget class="QPushButton" name="pbEnvoyer">
<property name="geometry">
<rect>
<x>850</x>
<y>210</y>
<width>211</width>
<height>71</height>
</rect>
</property>
<property name="text">
<string>Envoyer</string>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1085</width>
<height>44</height>
</rect>
</property>
</widget>
<widget class="QToolBar" name="mainToolBar">
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>