TP 2 • SFML Network
Cartouche
| Champ | Valeur |
|---|---|
| Auteur·e | Élise |
| Édition | 2023-12-05 |
| Durée | 2 séances |
| Taille des équipes | projet individuel |
| Rendu | via git, dépôt 2023_tp_sfml_net, droits en lecture à delivery_collector |
Notation
| Critère | Points |
|---|---|
| Serveur qui se fait attendre | 3 |
| Serveur poli | 3 |
| Serveur moqueur | 3 |
| Mise en pot | 3 |
| Plusieurs clients | 3 |
| Serde Serveur | 3 |
| Serde Client | 3 |
| Points accessibles | 21 |
| Note maximale | 20 |
Un rendu sale et du code qui ne respecte pas la norme peut vous faire perdre une partie ou la totalité des points acquis.
Introduction
L’objectif de ce TP est de vous montrer comment on peut faire communiquer deux processus ensemble par le réseau, ce qui permet la communication entre plusieurs machines.
On va aussi voir comment s’y prendre dans le cas où un serveur doit gérer plusieurs clients.
À partir d’un certain point, vous aurez besoin du code que vous avez fait dans le cadre du tp 1 sur la sérialisation.
Pré-requis
Il faut que la SFML soit installée sur votre système.
Les objets dont on va faire usage sont tous décrits SFML/Network.hpp et leur code compilé est dans libsfml-network.so et libsfml-system.so.
On compilera donc toujours avec -lsfml-network et -lsfml-system.
Objets SFML
Nous allons manipuler les classes suivants :
sf::TcpSocket, peut accueillir une connexion établie en utilisant.connect(IP, PORT)ou depuis unsf::TcpListeneren utilisant.accept();sf::TcpListener, écoute sur un port en l’attente de nouvelles demande de connexion, lorsqu’une connection est disponible on pour l’accepter avec uneTcpSocket;sf::SocketSelector, on lui donne desTcpSocketet desTcpListenner, et il est capable de nous dire sur lesquels il y a quelque chose à lire (sans que ça nous bloque.)
Serveur qui se fait attendre
Fichier à créer : main.cpp
Commençons par faire un serveur simple, qui prend un client, et qui est capable de répéter les messages qu’il reçoit.
Pour recevoir une connexion, il faut commencer par utiliser sf::TcpListener, choisir un port, puis utiliser accept.
accept prend en paramètre une sf::TcpSocket que l’on pourra utiliser pour lire et écrire.
#include <SFML/Network.hpp>
#include <thread>
#include <iostream>
int main(void)
{
sf::TcpListener lstnr;
sf::TcpSocket sock;
lstnr.listen(1234);
lstnr.accept(sock);
std::cout << "a client is connected, closing in 5 seconds...\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
return (0);
}
On peut tester que la connection fonctionne avec la commande nc ou netcat. On peut se connecter avec la commande :
nc 127.0.0.1 1234 -N
Si tout va bien, la connexion devrait rester active 5 secondes sans que rien ne se passe.
Serveur poli
Faisons dire à notre serveur un petit message de bienvenue à la connexion du client.
Il est possible d’envoyer un message sur une TcpSocket en utilisant la fonction membre .send(POINTEUR, TAILLE)
Vous pouvez :
- mettre votre message dans une
std::string; - remplacer
POINTEURparstr.c_str(); et TAILLEparstr.size().
(dans le cas où votre chaîne s’appelle str.)
Je vous conseille de faire une fonction qui prend en paramètre une référence sur une sf::Socket et une std::string et qui envoie le message contenu dans la chaîne sur la socket. Cela vous fera gagner du temps plus tard.
Encore une fois, tentez de vous connecter avec nc :
nc 127.0.0.1 1234 -N
Serveur moqueur
Pour cette étape répétons au client ce qu’il envoie au serveur, pour ce faire, on doit commencer par lire ce qu’il nous dit.
La fonction membre receive nous permet de recevoir du contenu depuis la socket. Elle prend 3 paramètres :
- un pointeur vers une zone mémoire ;
- la taille maximum à écrire dans la zone mémoire ; et
- une référence vers un
std::size_tdans laquelle le nombre d’octets lus va être écrit.
Exemple de lecture dans un char * sur une sf::Socket qui s’appelle sock.
char *c_str;
std::size_t bytes_read;
c_str = new char[100];
sock.receive(c_str, 99, bytes_read);
c_str[bytes_read] = '\0';
Une fois que vous avez réussi à lire : renvoyez au client ce qu’il vous a écrit.
Si jamais vous recevez un message de 0 octets, arrêtez et déconnectez vous du client avec sock.disconnect().
Essayez de vous connecter et d’envoyer des messages avec nc. Quand vous avez fini appuyez sur ctrl+d. C’est sensé provoquer une déconnexion.
nc 127.0.0.1 1234 -N
Mise en pot
Fichiers à créer : {Server.{cpp,hpp},Server.main.cpp}
On va mettre ce que l’on a fait pour le moment dans une classe. Appelons-la Server.
Votre constructeur doit :
- prendre en paramètre un numéro de port ;
- démarrer le
sf::TcpListeneravec ce numéro de port.
Faites une fonction membre echo_loop_single_client qui reprend le comportement de l’étape précédente (accepte un client, attend un message, le répète, attend un message, le répète, etc. jusqu’à une lecture de 0 octets).
Testez votre serveur dans Server.main.cpp.
Plusieurs clients
On a vu les fonctions membres TcpListener::accept et TcpSocket::receive qui vont toutes deux bloquer notre programme tant qu’il n’y a aucune connexion à accepter ou de message à recevoir.
Si on veut gérer plusieurs clients, il va falloir trouver un moyen d’accepter les connexions et recevoir des messages sans bloquer.
En programmation un tel outil s’appelle un sélecteur. D’après l’appel système select. Il existe une déclinaison de cet appel système qui s’appelle poll et qui est un peu plus moderne. Mais nous n’allons pas utiliser d’appel système aujourd’hui.
La SFML prévoit une classe qui s’appelle sf::SocketSelector. Elle peut vérifier pour vous s’il y a quelque chose à lire sur une liste de socket.
Pour utiliser sf::SocketSelector, il suffit de le construire, puis d’utiliser :
.add(SOCKET)en remplaçantSOCKETpar un de vos objetsTcpSocketouTcpListenerpour le surveille ;.wait(DURÉE)en replaçantDURRÉEpar unsf::seconds, pour attendre qu’il se passe quelque chose sur les socket données pedant la durée choisie : s’il se passe quelque chose, elle renvoie immédiatementtrue, sinon elle renvoiefalse;.isReady(SOCKET)pour demander après un.waits’il y a quelque chose à lire sur laSOCKETpassée en paramètre.
Voici un exemple qui fait usage de la classe sf::SocketSelector.
On commence par créer notre Listener et on lui fait écouter le port 1234. Seulement APRÈS on peut l’ajouter au Selector. Autrement ça nous aurait posé problème plus loin.
On accept un premier client et on lui dit bonjour.
Dans notre boucle :
- On attend sur le
Selector. - S’il s’est passé quelque chose :
- sur la
TcpSocketsockon répète le message au client (fonctionecho_onceque vous pouvez créer à l’aide de ce qu’on a fait des les exos précédents) ; - sur le
TcpListenerlstnron dit au revoir au client, on le retire destor, on le déconnecte, puis on enacceptun nouveau (qu’on ajoute austoret auquel on dit bonjour)
- sur la
int main(void)
{
sf::TcpListener lstnr;
sf::TcpSocket sock;
sf::SocketSelector stor;
if (lstnr.listen(1234) != sf::Socket::Done) {
return 89;
}
stor.add(lstnr);
if (lstnr.accept(sock) != sf::Socket::Done) {
return 89;
}
sock.send("hey!\n", 5);
stor.add(sock);
while (true) {
if (stor.wait(sf::Seconds(10.f))) {
if (stor.isReady(sock)) {
echo_once(sock);
} else if (stor.isReady(lstnr)) {
sock.send("bye!\n", 5);
stor.remove(sock);
sock.disconnect();
if (lstnr.accept(sock) != sf::Socket::Done) {
return 89;
}
stor.add(sock);
sock.send("hey!\n", 5);
}
}
}
}
Maintenant à votre tour : dans votre classe Server, écrivez la fonction membre run_once. Elle doit :
.waitsur le selector.- Si le
Listenerest prêt :.acceptun nouveau client,- l’ajouter au
Selector, - l’ajouter à la liste des clients ;
- Si un des clients de la liste est prêt :
- lire son message et lui renvoyer,
- si son message est vide : retirer le client du
Selector, le déconnecter.
Vous devez ajouter aux champs de votre classe une liste de clients en utilisant un conteneur de la STL (comme std::list, std::vector, etc.).
Pour tester, dans votre Server.main.cpp, vous pouvez appeler run_once en boucle.
Sérialisation et dé-sérialisation de Student
Serveur
Servons-nous du travail de sérialisation que l’on a fait dans TP 1 • Sérialisation.
Maintenant, lorsqu’un client vous envoie un message : essayez de construire un objet de type Student avec le contenu du message.
Si la construction réussit, stockez cette instance de Student dans un conteneur (liste, vector, peu importe).
Si la construction échoue, renvoyez au client son message avec pour préfixe : "Not a student: ".
Client
Fichier à créer : client.cpp
Écrivez un programme client qui prend 4 arguments :
- l’identifiant d’un étudiant (un nombre) ;
- le prénom de l’étudiant ;
- le nom de famille de l’étudiant ; et
- l’année d’étude de l’étudiant (un nombre).
Créez une instance de Student avec les données des arguments du programme et sérialisez-là dans un std::stringstream.
Utilisez sf::TcpSocket pour :
- vous connecter au serveur avec
.connect(IP, PORT); et - envoyer l’instance de Student sérialisée.
Testez votre client et votre serveur ensemble.