ATTENTION : ceci est la documentation d'une version antérieure de SFML ; la documentation de la dernière version officielle est accessible via le menu principal

Tutoriel - Module réseau - Utiliser et étendre les paquets

Introduction

L'échange de données par le réseau peut être plus compliqué qu'il n'y paraît. Tout peut sembler facile tant que vous testez votre application avec votre propre ordinateur en tant que client et serveur, mais lorsque vous commencez à transiter par internet et sur différentes plateformes, beaucoup de problèmes peuvent apparaître.

Le premier est le boutisme (endianess). Le boutisme est l'ordre dans lequel une plateforme particulière va stocker les octets d'un type primitif. Il existe deux familles principales de plateformes : celles utilisant le grand-boutisme (big-endian -- stockent l'octet de poids fort en premier) et celles utilisant le petit-boutisme (little-endian -- stockent l'octet de poids faible en premier). D'autres plateformes un peu plus exotiques peuvent être en bi-endian ou encore en mixed-endian, deux autres formes d'agencement d'octets.
Pour en revenir aux échanges de données sur le réseau, imaginez que vous envoyez un entier codé sur 16 bits en little-endian (votre processeur est un Intel x86 par exemple), et que le serveur le reçoive et l'interpète en big-endian (son processeur est un PowerPC Apple par exemple) ; si vous envoyez 48 (00000000 00110000) le serveur va en fait voir 768 (00000011 00000000).

Un autre problème est la taille des types primitifs. Différentes plateformes peuvent avoir différentes tailles pour un même type. Si la taille d'un int est de 32 bits sur votre plateforme, peut-être qu'elle sera de 64 bits sur le serveur, et là encore, les données reçues seront faussement interprétées.

Le troisième problème est plus lié au réseau. Les transferts de données via les protocoles TCP et UDP doivent suivre des règles définies par les niveau plus bas d'implémentation. En particulier, un morceau de données peut être découpé et reçu en plusieurs fois ; le recepteur doit trouver un moyen de recomposer le morceau et de renvoyer les données comme s'il les avait reçues en une fois.

Ce sont les problèmes les plus communs impliqués dans la programmation réseau, mais il en a tout un tas d'autres à gérer si vous voulez construire des programmes robustes. Afin de vous aider avec ces tâches bas niveau, la SFML fournit une classe pour manipuler les données réseau via des paquets : sf::Packet.

Utilisation basique

Tout comme les entrées / sorties standards, sf::Packet permet d'insérer et d'extraire très facilement des données de tout type via les opérateurs << et >>. Vous pouvez construire un paquet de données tout comme vous écririez ces données sur la console avec std::cout, sf::Packet se chargera des problèmes de boutisme, de taille et d'autres détails pour vous.

// Insertion

sf::Packet ToSend;
ToSend << 24 << "hello" << 59864.265f;
...
// Extraction

sf::Packet Received;
...
int x;
std::string s;
float f;
Received >> x >> s >> f;

Contrairement à l'écriture, la lecture depuis un paquet peut échouer, notamment si l'on tente de lire plus d'octets que le paquet n'en contient. Dans un tel cas, la lecture échouera et le paquet sera mis dans un état invalide. Pour vérifier l'état d'un paquet il est possible de le tester directement (if (Received)) ou, encore mieux, de tester la lecture (if (Received >> x >> y)).

Received >> x >> s >> f;
if (!Received)
{
    // Erreur... les données n'ont pas pu être lues
}

// Ou

if (!(Received >> x >> s >> f))
{
    // Erreur... les données n'ont pas pu être lues
}

L'envoi et la réception de paquets ne diffère pas de l'envoi et la réception de tableaux d'octets :

// Avec les sockets TCP

Socket.Send(Packet);
Socket.Receive(Packet);
// Avec les sockets UDP

Socket.Send(Packet, Address, Port);
Socket.Receive(Packet, Address);

Les paquets et les classes persos

Tout comme avec les flux standards, il est possible d'étendre les paquets afin de leur permettre de manipuler vos classes persos. Pour permettre l'insertion et l'extraction d'instances d'une classe perso dans un sf::Packet, définissez la bonne version des opérateurs << et >> :

struct Character
{
    int         Age;
    std::string Name;
    float       Height;
};

sf::Packet& operator <<(sf::Packet& Packet, const Character& C)
{
    return Packet << C.Age << C.Name << C.Height;
}

sf::Packet& operator >>(sf::Packet& Packet, Character& C)
{
    return Packet >> C.Age >> C.Name >> C.Height;
}

Les deux opérateurs renvoient une référence vers le paquet : cela permet le chaînage des appels.

A présent vous pouvez insérer et extraire des instances de votre classe comme n'importe quel type primitif :

Character Bob;
sf::Packet Packet;

Packet << Bob;
Packet >> Bob;

Paquets personnalisés

Afin de permettre encore plus de personnalisation, sf::Packet est extensible. Cela signifie que vous pouvez construire vos propres classes de paquets, et personnaliser les données qui vont être envoyées et reçues. Pour se faire, sf::Packet définit deux fonctions virtuelles :

Ces fonctions permettent aux classes dérivées de modifier ce qui sera envoyé, ou ce qui sera lu après réception. Voici un exemple simple de paquet gérant le chiffrage des données :

class EncryptedPacket : public sf::Packet
{
private :

    virtual void OnSend()
    {
        // Copie des données du paquet dans un tampon temporaire
        std::vector<char> Buffer(GetData(), GetData() + GetDataSize());

        // Chiffrage (algorithme surpuissant : on ajoute 1 à chaque caractère !)
        for (std::vector<char>::iterator i = Buffer.begin(); i != Buffer.end(); ++i)
            *i = *i + 1;

        // On remplace les données du paquet par notre tampon chiffré
        Clear();
        Append(&Buffer[0], Buffer.size());
    }

    virtual void OnReceive()
    {
        // Copie des données du paquet dans un tampon temporaire
        std::vector<char> Buffer(GetData(), GetData() + GetDataSize());

        // On déchiffre les données à l'aide de notre super algorithme
        for (std::vector<char>::iterator i = Buffer.begin(); i != Buffer.end(); ++i)
            *i = *i - 1;

        // On remplace les données du paquet par notre tampon déchiffré
        Clear();
        Append(&Buffer[0], Buffer.size());
    }
};

La fonction GetData() donne accès en lecture au tampon interne, ainsi vous pouvez l'utiliser si nécessaire. GetDataSize() donne le nombre d'octets dans ce tampon. Pour modifier ou ajouter des données spécifiques dans le tampon, vous pouvez utiliser la fonction Clear() (pour effacer le tampon interne), Append() (pour ajouter des données brutes), ou l'opérateur <<.

Les paquets personnalisés peuvent se révéler utiles dans de nombreuses utilisations : chiffrement, compression, sommes de contrôle, .. Vous pouvez même fournir des paquets formattés utilisant une structure fixée, ou des paquets agissant comme des fabriques.

Conclusion

La classe sf::Packet gère beaucoup de problèmes réseau de bas niveau, et fournit un moyen facile de transférer des données avec les sockets. Je vous encourage à étendre les paquets à vos besoins, en surchargeant les opérateurs << et >>, et en dérivant vos propres classes de paquets si nécessaire.

Il est important de garder en tête que les paquets SFML utilisent leur propre boutisme et structure, vous ne pouvez donc pas les utiliser pour communiquer avec des serveurs qui ne les utilisent pas. Pour envoyer des données brutes, des requêtes HTTP / FTP, ou n'importe quoi d'autre n'utilisant pas la SFML, utilisez des tableaux d'octets plutôt que des paquets SFML.

Vous êtes maintenant prêts à passer au dernier tutoriel, concernant l'utilisation des sélecteurs.

Téléchargez le code source :