Communications TCP par le réseau
Principe de la communication réseau
Les protocoles les plus répandus sur le réseau sont les protocoles des réseaux TCP/IP.
TCP est une famille de protocoles standards et correspond à la couche transport de l'information.
IP correspond à une autre famille de protocoles standards et correspond à la couche réseau (routage de l'information).
Le modèle de communication est dit client/serveur. Le principe est le suivant :
Le client se connecte au serveur à l'écoute sur le réseau.
Une fois la connexion établie, les informations peuvent s'échanger selon un protocole de couche application (cela peut être un protocole maison).
Exemple : Maison / Entreprise
Pour rapidement comprendre l'essentiel concernant la programmation réseau, on peut comparer un ordinateur client à une maison, un ordinateur serveur à une entreprise.
La maison dispose d'une adresse postale.
L'entreprise dispose aussi d'une adresse postale, peut-être même plusieurs.
Lorsqu'on veut acheminer un courrier de la maison jusqu'à l'entreprise, il ne suffit pas d'indiquer l'adresse postale mais aussi le service concerné par ce courrier.
Dès réception du courrier dans l'entreprise, il est acheminé jusqu'à la bonne porte du bureau de service concerné.
-------------------
Il en est de même en programmation réseau.
L'adresse de la maison est l'adresse IP du poste client.
L'adresse de l'entreprise est l'adresse IP du serveur.
Le service concerné correspond au numéro de port (0-65535).
-------------------
Une information envoyée d'un poste client vers un poste serveur utilisera de part et d'autre un port (TCP) de communication.
Côté client, le numéro de port est choisi dynamiquement par le système.
Côté serveur, le numéro de port est défini par le service (logiciel serveur) à l'écoute.
TCP ou UDP ?
Ces deux protocoles sont différents, donc pour un usage différent.
TCP implique l'établissement d'une connexion entre le client et le serveur. Elle peut être refusée par le serveur ou mise en attente.
Une fois les ordinateurs connectés, les échanges sont systématiquement acquittés par le récepteur. La fiabilité des informations échangées est assurée. Le contenu des informations est vérifiable par un CRC16.
UDP ne demande pas de connexion au serveur. Il s'agit d'un envoi direct d'informations appelé datagramme. Pas d'acquittement par le récepteur. Le contenu des informations est vérifiable par un CRC16.
--------------------------
La plupart des protocoles de couche application utilisent le protocole TCP (HTTP, FTP, etc.).
Le protocole UDP est privilégié pour échanger des informations liées à un flux permanent (transmission d'une mesure par exemple). Ainsi, si un datagramme n'arrive pas à destination, cela ne remet pas en question la cohérence de la communication.
Socket de communication
Pour communiquer sur un réseau TCP/IP, il faut définir un certains nombre de renseignements tels que l'adresse IP source/destination, le port source/destination, etc.
Pour un serveur, l'information à personnaliser est le numéro de port d'écoute.
Pour un client, l'information à personnaliser est l'adresse IP du serveur et le numéro de port du service à atteindre.
Les autres informations sont la plupart du temps définie automatiquement par le système.
Ces renseignements sont dans des structures de données et forme "un package" appelé une socket de communication.
Une socket est associée à un port de communication et c'est le moyen utilisé pour émettre ou recevoir des informations.
Complément : Module Qt pour programmer
Pensez à ajouter le module network
dans le fichier .pro à tout projet de développement utilisant les classes Qt de gestion du réseau.
Programmation côté serveur
L'objet QTcpServer
La classe QTcpServer
permet de faciliter grandement la programmation d'un serveur. La création d'un serveur se programme de la manière suivante :
serveur = new QTcpServer(this);
serveur->listen(QHostAddress::Any, 2222);
connect(serveur,SIGNAL(newConnection()), this, SLOT(onNewConnection()));
Ligne 1 : serveur
désigne l'objet serveur TCP, rattaché à notre IHM.
Ligne 2 : La méthode listen()
permet de définir le port TCP d'écoute (2222), ainsi que les interfaces d'écoute (QHostAddress::Any
).
QHostAddress::Any
ordonne que toutes les interfaces réseau disponibles sur le serveur soient à l'écoute. Il est aussi possible de limiter le serveur de manière à n'écouter que sur une seule interface réseau.
Ligne 3 : Connexion du signal newConnection()
au slot onNewConnection()
. Le signal est émis par l'objet serveur lorsqu'un client se connecte.
Ce slot permet de gérer la communication avec le client :
void CIhmAppServeurTcp::onNewConnection()
{
qDebug() << "onNewConnection";
ui->teTexte->append("onNewConnection");
sock = serveur->nextPendingConnection();
connect(sock, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
connect(sock, SIGNAL(disconnected()), this, SLOT(onDisConnected()));
}
La méthode nextPendingConnection() permet de récupérer l'adresse d'un objet QTcpSocket
disposant de toutes les informations du client pour communiquer avec lui.
Il est ensuite possible de connecter le signal :
readyRead()
pour être prévenu de l'arrivée de caractères en provenance du client.disconnected()
pour être prévenu si le client se déconnecte.
Examinons un exemple de slot onReadyRead()
:
void CIhmAppServeurTcp::onReadyRead()
{
ui->teTexte->append(sock->readAll());
}
La méthode readAll()
de l'objet QTcpSocket
permet de lire les caractères reçus (envoyés par le client) et les affiche dans un widget QTexteEdit
.
Conseil : Gestion des clients
Un serveur gère en générale plusieurs clients. Pour les différencier, il est nécessaire de mémoriser les adresses des objets QTcpSocket
dans une liste pour les distinguer. Cette liste doit être maintenue à jour en cas de déconnexion.
Il est souvent utile que les clients soient gérés de manière indépendante. La solution est que chaque objet de gestion d'un client devienne un thread ou processus indépendant. Un serveur peut ainsi gérer simultanément plusieurs clients.
C'est le cas d'Apache2, le serveur WEB bien connu. La commande ps -e vous permettra de découvrir les multiples processus formant Apache2.
Chaque processus Apache2 gère un client connecté ou est en attente de connexion (pour un gain de temps).
Un processus Apache2 reste à l'écoute d'une connexion entrante d'un client.

La partie multi processus sera étudiée plus loin.
Programmation côté client
L'objet QTcpSocket
L'objet QTcpSocket
encapsule toutes les informations d'une socket et fourni des méthodes simplifiant la programmation d'un client réseau.
Initialisation du client
Cela commence par la déclaration dans la classe IHM d'un pointeur sur l'objet QTcpSocket
.
QTcpSocket *sock;
L'instanciation peut se faire dans le constructeur de la classe IHM :
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)));
Dès l'instanciation effectuée, les différents signaux sont connectés aux slots correspondants.
onConnected()
pour gérer l'après connexion au serveur.
onDisconnected()
pour gérer la déconnexion du serveur.
onReadyRead()
pour lire les informations émises par le le serveur.
error(QAbstractSocket::SocketError)
pour gérer les erreurs de communication. Le paramètre reçu détaille l'erreur qui est survenue.
Connexion au serveur
Exemple après clique sur un bouton de connexion. Au préalable, l'adresse et le port du serveur ont été saisie dans des QLineEdit
.
void CIhmAppClientTcp::on_pbConnecter_clicked()
{
sock->connectToHost(ui->leAdrServeur->text(), ui->lePortServeur->text().toInt());
}
Réception d'informations du serveur
Le signal readyRead()
permet d'être averti de l'arrivée d'informations. Exemple de slot :
void CIhmAppClientTcp::onReadyRead()
{
QByteArray qba = sock->readAll();
ui->teReception->append(QString(qba));
}
La méthode readAll()
permet de lire les octets reçus et les stocker dans un tableau sous forme d'objet QByteArray
.
Heureusement, pour afficher le texte correspondant, il existe la possibilité de construire un QString
à partir d'un QByteArray
.
Envoi d'informations vers le serveur
On utilise la méthode write()
ou writeData()
. Exemple de slot :
void CIhmAppClientTcp::on_pbEnvoyer_clicked()
{
sock->write(ui->leEmission->text().toStdString().c_str());
}
Le paramètre attendu dans la méthode write()
est une chaîne de caractères standard C.
Heureusement, la classe QString
le prévoit par la méthode toStdString()
qui elle même dispose d'un moyen de récupérer l'adresse de la chaîne : c_str()
.
Gestion des erreurs réseau
Le signal errorSocket()
fourni la valeur de l'erreur. Cette valeur est récupérée dans le slot correspondant. En voici un exemple :
void CIhmAppClientTcp::onSocketError(QAbstractSocket::SocketError error)
{
switch (error) {
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 !");
break;
} // sw
}
Seuls sont décrits ici quelques cas d'erreurs possibles. Il en existe davantage décrits dans la documentation Qt.