Attention: cette page se réfère à une ancienne version de SFML. Cliquez ici pour passer à la dernière version.

Utiliser les sockets

Introduction

Dans ce tutoriel vous allez apprendre les rudiments de la programmation réseau à l'aide des sockets, et vous serez capables d'échanger des données entre deux ordinateurs distants.

Qu'est-ce qu'un socket ?

Pour les novices de la programmation réseau, les sockets sont la chose la plus importante à connaître. A peu près tout (à bas niveau) dépend des sockets. Un socket peut être vu comme un tuyau entre deux ordinateurs, et une fois qu'un tel tuyau a été établi, vous pouvez commencer à échanger des données.

Il existe beaucoup de protocoles pour communiquer sur le réseau, et chaque protocole définit plusieurs types de sockets. Mais tout ce que vous avez à connaître est le protocole internet (IP), avec les sockets de type UDP et TCP ; c'est ce que presque tout programme réseau utilise, et cela constitue la base du module réseau de la SFML.

Les sockets TCP et UDP sont assez différents, voici pourquoi :

Pour résumer, les sockets TCP sont plus sûrs mais plus lents. Il n'y a que très peu de cas qui nécessitent les sockets UDP : les applications temps-réel intensives telles que les jeux de shoot/rôle/stratégie, ou encore le broadcasting (envoi de données à tous les ordinateurs d'un sous-réseau).

Manipuler les adresses réseau

Avant de commencer à utiliser les sockets, intéressons nous à une classe utilitaire du module réseau : sf::IPAddress. Elle encapsule les adresses IP (v4) et fournit des fonctions très utiles pour l'initialisation, la comparaison, etc. Etant donné que la programmation réseau implique des communications avec des ordinateurs distants, vous aurez besoin de cette classe à chaque fois que vous voudrez vous connecter / envoyer / recevoir.

Première chose à faire : inclure le fichier en-tête pour le module réseau. Comme d'habitude, il va inclure pour vous tous les en-têtes nécessaires à l'utilisation du module réseau.

#include <SFML/Network.hpp>

Vous pouvez initialiser une instance de sf::IPAddress à partir de différentes sources :

sf::IPAddress Address1;                  // Par défaut : adresse invalide
sf::IPAddress Address2("192.168.0.1");   // D'une représentation sous forme de chaîne
sf::IPAddress Address3("computer_name"); // D'un nom d'hôte
sf::IPAddress Address4(192, 168, 0, 1);  // De 4 octets
sf::IPAddress Address5 = sf::IPAddress::LocalHost; // 127.0.0.1 -- votre propre ordinateur

Vous pouvez également récupérer votre adresses IP, vue du réseau local ou d'internet :

// Votre adresse dans le réseau local (du style 192.168.1.100 -- celle que vous voyez avec ipconfig)
sf::IPAddress Address6 = sf::IPAddress::GetLocalAddress();

// Votre adresse sur internet (du style 83.2.124.68 -- celle que vous voyez sur www.whatismyip.org)
sf::IPAddress Address7 = sf::IPAddress::GetPublicAddress();

Notez bien que GetPublicAddress() est très lente : le seul moyen d'obtenir une adresse publique est de la récupérer depuis l'extérieur (particulièrement lorsque vous êtes derrière un proxy ou un pare-feu), ainsi elle se connecte à un site externe (www.whatismyip.org) et examine la page web renvoyée pour en extraire l'adresse IP publique. Donc, utilisez-la avec parcimonie.

Vous pouvez obtenir la représentation sous forme de chaîne (de type "xxx.xxx.xxx.xxx") d'une adresse avec sa fonction ToString() :

sf::IPAddress Address("computer_name");
std::string IP = Address.ToString();

Vous pouvez également vérifier la validité d'une adresse avec sa fonction IsValid() :

sf::IPAddress Address("computer_name");
if (Address.IsValid())
    // Ok, hôte trouvé
else
    // Adresse invalide

Utiliser un socket UDP

Les sockets UDP ne nécessitant pas de connexion, ils sont les plus faciles à utiliser. Aucune initialisation n'est nécessaire, tout ce que vous avez à faire c'est envoyer et recevoir des données. La seule étape nécessaire est de lier le socket à un port avant de pouvoir recevoir des données à travers celui-ci.

Les sockets SFML permettent d'envoyer des données brutes, définies par un pointeur vers un tableau d'octets et sa taille.

// Création du socket UDP
sf::SocketUDP Socket;

// Création du tableau d'octets à envoyer
char Buffer[] = "Hi guys !";

// Envoi des données à l'adresse "192.168.0.2" sur le port 4567
if (Socket.Send(Buffer, sizeof(Buffer), "192.168.0.2", 4567) != sf::Socket::Done)
{
    // Erreur...
}

Ici, nous envoyons un tableau d'octets (contenant "Hi guys !") à l'ordinateur dont l'adresse IP est 192.168.0.2 sur le port 4567. Le troisième paramètre est un sf::IPAddress, vous pouvez donc tout aussi bien utiliser un nom d'hôte, une adresse de broadcast ou n'importe quel type d'adresse valide.
Si vous ne voulez pas utiliser un port spécifique, vous pouvez simplement prendre n'importe quel numéro de port libre entre 1024 et 65535 (les ports inférieurs à 1024 sont réservés). Et bien sûr, assurez-vous que votre pare-feu ne bloque pas ce port !

La fonction Send, tout comme les autres fonctions qui peuvent bloquer, renvoie un état de socket (voir sf::Socket::Status) qui peut être :

La réception de données fonctionne exactement de la même manière, excepté que vous devez d'abord lier le socket au port que vous souhaitez écouter.

// Création du socket UDP
sf::SocketUDP Socket;

// On le lit au port 4567
if (!Socket.Bind(4567))
{
    // Erreur...
}

Puis vous pouvez recevoir un tableau d'octets, sa taille ainsi que l'adresse et le port de l'expéditeur.

char Buffer[128];
std::size_t Received;
sf::IPAddress Sender;
unsigned short Port;
if (Socket.Receive(Buffer, sizeof(Buffer), Received, Sender, Port) != sf::Socket::Done)
{
    // Error...
}

// On affiche l'adresse et le port de l'expéditeur
std::cout << Sender << ":" << Port << std::endl;

// On affiche le message reçu
std::cout << Buffer << std::endl; // "Hi guys !"

Notez bien que Receive est bloquante, ce qui signifie qu'elle ne rendra la main que lorsqu'elle aura reçu quelque chose si la socket est en mode bloquant (qui est le mode par défaut).

Lorsque vous n'avez plus besoin du socket, vous devez le fermer (le destructeur ne le fera pas pour vous !) :

Socket.Close();

Utiliser un socket TCP

Les sockets TCP doivent être connectés avant de pouvoir envoyer ou recevoir quoique ce soit. Voici comment ça marche :

Premièrement, vous ouvrez un socket et vous le faites écouter le port choisi.

sf::SocketTCP Listener;
if (!Listener.Listen(4567))
{
    // Erreur...
}

Puis, vous pouvez attendre les connexions sur ce port.

sf::IPAddress ClientAddress;
sf::SocketTCP Client;
if (Listener.Accept(Client, &ClientAddress) != sf::Socket::Done)
{
    // Erreur...
}

La fonction Accept va attendre jusqu'à ce qu'une connexion arrive, puis renvoyer un nouveau socket qui sera utilisé pour échanger les données avec l'ordinateur connecté. Si vous passez une instance de sf::IPAddress à la fonction, celle-ci sera initialisée avec l'adresse du client (pratique pour savoir qui vient de se connecter).
Si la socket est en mode non-bloquant, cette fonction rendra la main immédiatement si aucune connexion n'est en attente, et renverra le code sf::Socket::NotReady.

Regardons maintenant comment ça se passe du côté du client. Tout ce que vous avez à faire est créer un socket puis le connecter au serveur sur le port que ce dernier écoute.

sf::SocketTCP Client;
if (Client.Connect(4567, "192.168.0.2") != sf::Socket::Done)
{
    // Erreur...
}

Maintenant, le client et le serveur sont prêts à communiquer. Les fonctions pour envoyer et recevoir des données sont les mêmes que pour les sockets UDP, excepté que vous n'avez plus besoin de spécifier le port et l'adresse du destinataire. Donc en gros, les seuls paramètres requis sont le tableau à envoyer / recevoir ainsi que sa taille.

char Buffer[] = "Hi guys !";
if (Client.Send(Buffer, sizeof(Buffer)) != sf::Socket::Done)
{
    // Erreur...
}
char Buffer[128];
std::size_t Received;
if (Client.Receive(Buffer, sizeof(Buffer), Received) != sf::Socket::Done)
{
    // Erreur...
}

Pour les sockets TCP, les fonctions Send et Receive peuvent renvoyer sf::Socket::Disconnected, qui signifie que la connexion a été interrompue.

Comme pour les sockets UDP, n'oubliez pas de fermer le socket lorsque vous avez fini.

Conclusion

La SFML fournit un ensemble de classes bas niveau mais facile à utiliser pour échanger des données via le réseau. Mais n'oubliez pas que la programmation réseau n'est pas simple, et vous devrez utiliser des algorithmes et des structures de données efficaces pour construire des programmes robustes et rapides. Je vous recommande de lire quelques bons tutoriels concernant la programmation réseau (pas sur les sockets, mais plutôt les techniques et algorithmes généraux), particulièrement si vous débutez en programmation réseau.

Dans le prochain tutoriel, nous détaillerons une autre classe utilitaire qui permet de manipuler plus facilement les données à travers le réseau.