IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

Framework de paquets réseau en C++ moderne


Sujet :

C++

  1. #1
    Membre chevronné

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Juin 2007
    Messages
    373
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Royaume-Uni

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Santé

    Informations forums :
    Inscription : Juin 2007
    Messages : 373
    Par défaut Framework de paquets réseau en C++ moderne
    Bonjour,

    Je travaille sur un petit projet de jeu sur mon temps libre, et je me casse un peu les dents sur l'architecture du code au niveau du passage d'information entre les clients et le serveur. Je me repose sur la SFML pour la gestion bas niveau du réseau. Si vous ne connaissez pas, l'essentiel est que je dispose d'une classe sf::Packet qui peut sérialiser/dé-sérialiser n'importe quel type de POD pour les acheminer sur le réseau. Un exemple simplifié :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    // Alice
    int i = 5;
    double d = 3.14;
    sf::Packet p;
    p << i << d;
    socket.send(p);
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    // Bob
    int i;
    double d;
    sf::Packet p;
    socket.receive(p);
    p >> i >> d;
    Le principal problème à ce stade est de s'assurer que Alice remplisse correctement le paquet avant de l'envoyer, et que Bob essaye seulement d'en extraire ce qu'il contient. J'ai donc écrit un framework qui automatise le tout.

    Dans ce cadre, Alice peut :
    • envoyer un message informatif à Bob, sans rien attendre en retour (send_message())
    • envoyer une requête à Bob, et enregistrer un callback à exécuter quand la réponse de Bob arrivera (send_request())

    Et Bob peut :
    • enregistrer un callback a exécuter quand il reçoit un message particulier (watch_message())
    • enregister un callback a exécuter quand il reçoit une requête (le callback a alors la responsabilité d'émettre une réponse) (watch_request())

    Bien sûr, Alice peut aussi faire ce que Bob fait, et réciproquement...

    Pour l'automatisation, je dois créer une classe pour chaque type de message/requête. Elle porte en elle :
    • l'identifiant unique correspondant à ce type de paquet (il sert à trouver quel callback appeler une fois le paquet reçu)
    • la liste des types des objets qui sont envoyés dans le paquet


    Un exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template<typename ... Args>
    struct type_list {};
     
    struct client_connected {
        // L'ID doit être unique dans tout le programme et constant
        // d'une unité de compilation à une autre (et bien sûr d'une
        // exécution à une autre)
        static const std::uint16_t id = 486; 
     
        using types = type_list<
            std::size_t, // id du client
            std::string  // nom du client
        >;
    };
    Du coup, quand Alice appelle send_message<client_connected>(12, "toto"), le framework vérifie à la compilation que les arguments correspondent à ceux de la liste client_connected::types, et les envoie. De son côté, Bob peut décider d'effectuer une action à la réception de ce message. Il va alors appeler watch_message<client_connected>([](std::size_t id, std::string name) { ... }); et, là aussi, le framework vérifie à la compilation que les arguments de la fonction sont compatibles avec le contenu attendu du paquet. Le reste est fait sous le capot (réception du paquet, dispatch vers le bon callback, dé-sérialisation des éléments, etc.).

    Je suis assez content du résultat, mais il reste une ombre au tableau : attribuer un ID unique à chaque type de message/requête. Je peux bien sûr faire ça à la main (commencer à 0, puis donner 1 au second message que je vais créer, etc.), mais ça pose des problèmes de maintenance évidents : si je me trompe et donne le même ID à deux types de paquets différents je n'ai aucun mécanisme pour le détecter. Si je décide de supprimer un paquet, alors son ID sera inutilisé (il faudra que je pense à le redonner à un futur nouveau paquet). Bref je voudrai éviter ça.

    Une des solutions temporaire que j'ai trouvé consiste à déclarer tous les messages dans un même header, et d'utiliser la macro __LINE__ pour générer un ID unique. Ça pose aussi son lot de problème : les valeurs retournées par __LINE__ sont certes uniques, mais elles ne sont pas très optimales (il y a plein de trous : on passe de 26 à 42 par exemple), et surtout ça me force à tout déclarer au même endroit, ce qui est très moche en terme de localité du code (au niveau conceptuel et temps de compilation). Par exemple avec client_connected je préfère que le type de l'ID du client soit un typedef, using client_id_t = std::size_t, qui soit utilisé dans le reste du code. Mais du coup je dois le déclarer dans ce header, et tout le monde va en profiter sans forcément en avoir besoin...

    Une autre option serait de déclarer dans un header commun une grosse énumération avec tous les messages possibles :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    enum message_id_t : std::uint16_t {
        client_connected_id,
        client_disconnected_id,
        ...
    };
    Puis d'écrire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    struct client_connected {
        static const std::uint16_t id = client_connected_id; 
        using types = type_list<std::size_t, std::string>;
    };
    Mais ça veut dire que je dois "déclarer" le paquet à deux endroits différents. Le code se répète un peu, et je peux me planter dans l'affectation de l'ID (si je copie/colle le code d'un autre paquet et oublie de changer l'ID par exemple). Ça ne me plaît pas beaucoup non plus.

    Ma seule solution est-elle de recourir à un outil externe qui va générer ces ID à ma place à la compilation, en lisant mon code source ? Si oui, connaissez-vous de bons outils portables (et si possible légers) qui permettent de faire ça proprement ?

  2. #2
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    Salut,

    Pourquoi ne pas simplement utiliser une fonction de hashage (std::hash, en C++11 )

    Tu récupère un entier (64 bits, il me semble) non signé avec les certitudes:
    • qu'il sera unique, à moins de chercher la collision (en fait, tu risques d'avoir des collisions, mais, bon, le risque n'est pas très grand )
    • qu'il sera constant : une valeur identique fournira forcément une clé de hashage identique


    Tu n'es même pas forcément obligé de prendre du très costaud, un simple CRC 32 pourrait tou aussi bien faire l'affaire si tu gardes un nombre de messages plus ou moins limité
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Membre chevronné

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Juin 2007
    Messages
    373
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Royaume-Uni

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Santé

    Informations forums :
    Inscription : Juin 2007
    Messages : 373
    Par défaut
    Et je lui donne le nom du paquet à hasher ?
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    static const std::size_t id = std::hash<std::string>()("client_connected");
    Pas mal comme idée, merci pour ta réponse, mais j'y vois deux problèmes majeurs :
    • les implémentations de std::hash ne sont pas standardisées : une compilation avec gcc ne donnera peut être pas le même résultat qu'une compilation VC++ (embêtant si le serveur est compilé avec gcc, et qu'un gars utilise un client qu'il a construit lui même avec VC++ par exemple).
    • std::size_t, qui est le type de retour de std::hash::operator(), n'est pas un type fixe, donc si je créé un ID sur le serveur en 64 bit, je ne sais pas ce que ça donnera pour un client 32 bit... En tout cas je ne peux pas le faire transiter sur le réseau facilement.

  4. #4
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    C'est pour cela que j'ai parlé de crc_32

    L'algorithme est simple et tu a la certitude d'avoir d'office toujours les mêmes valeurs

    Comme je présumes que tu n'auras, de toutes manières, pas des centaines de milliers de messages, tout ce qu'il te faut, c'est le moyen d'automatiser la génération d'un identifiant unique qui puisse être indépendant de la machine afin que chaque partie en présence puisse savoir exactement ce que l'autre essaye de lui dire

    Il n'est pas nécessaire de sortir l'artillerie lourde pour (en considérant MD-5 ou ShaXXX comme tel ) pour y arriver, car le but ici n'est absolument pas de sécuriser l'information (ça, ça se fait au niveau du paquet, et non au niveau du contenu qu'on y mettra ) mais juste d'être sur que si tu envoies le message "bonjour", celui qui le recevra ne risque pas de l'interpréter comme "tu me fais ch..."

    Je n'ai cité std::hash que pour l'exemple, parce que j'avais remarqué que tu profites déjà des fonctionnalités de C++11
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  5. #5
    Membre chevronné

    Homme Profil pro
    Développeur informatique
    Inscrit en
    Juin 2007
    Messages
    373
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Royaume-Uni

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Santé

    Informations forums :
    Inscription : Juin 2007
    Messages : 373
    Par défaut
    Pardon, j'étais passé à côté... Tu as raison, ça règle les deux problèmes restants.

    Voilà ce que j'ai fini par écrire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // Calcule récursivement le CRC32 d'une chaîne de caractère.
    // J'ai été obligé d'implémenter ça de manière récursive pour
    // pouvoir la déclarer constexpr. Du coup c'est un peu laid...
    constexpr std::uint32_t recursive_crc32(std::uint32_t id, const char* str, std::size_t i,
        std::size_t n, std::size_t j, std::uint32_t poly) {
        return j == 8 ?
            (i == n ? id : recursive_crc32(id, str, i+1, n, 0, poly)) :
            recursive_crc32(
                ((id >> 31) ^ ((str[i] >> j) & 1)) == 1 ?
                    ((id << 1) ^ poly) : id << 1,
                str, i, n, j+1, poly
            );
    }
     
    // User defined litteral pour avoir la taille de la chaîne facilement
    constexpr std::uint32_t operator "" _crc32(const char* str, std::size_t length) {
        return recursive_crc32(0, str, 0, length, 0, 0x4C11DB7);
    }
     
    // Class utilitaire template qui permet d'éviter des problèmes de
    // linkage pour l'ID constant
    namespace crc32_impl {
        template<std::uint32_t ID>
        struct base {
            static const std::uint32_t id = ID;
        };
     
        template<std::uint32_t ID>
        const std::uint32_t base<ID>::id;
    }
     
    // La macro magique
    #define ID_STRUCT(name) struct name : public crc32_impl::base<#name ## _crc32>
    Définir un nouveau message revient simplement à :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    ID_STRUCT(client_connected) {
        using types = type_list<std::size_t, std::string>;
    };
    Et rien d'autre. C'est beau, j'aime beaucoup ! Merci Koala
    Bon, il reste le problème des collisions. La probabilité devrait être assez faible, mais ça serait bon de vérifier. À la limite je peux aussi écrire un petit programme qui va chercher la liste de tous les messages dans le code, et qui vérifie qu'il n'y a pas de collision.

  6. #6
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 635
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 635
    Par défaut
    J'ai trouvé ce lien sur stak-overflow: http://preshing.com/20110504/hash-co...probabilities/

    Si même tu atteins les 927 messages, la probabilité d'avoir une collision est de 1/10000 avec un algorithme de génération sur 32 bits.

    Le risque est là et bien là, mais ca laisse de la marge
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 2
    Dernier message: 02/06/2012, 19h33
  2. Structure des paquets réseau
    Par keyga dans le forum Réseau et multijoueurs
    Réponses: 15
    Dernier message: 10/05/2012, 09h32
  3. SQL Server, taille du paquet réseau
    Par Philippe Robert dans le forum MS SQL Server
    Réponses: 1
    Dernier message: 19/07/2011, 18h35
  4. choix framework - dessin graphe réseau
    Par manik971 dans le forum Frameworks Web
    Réponses: 0
    Dernier message: 27/04/2010, 15h48
  5. [SSL] Passerelle de paquets réseau
    Par baptx dans le forum Réseau
    Réponses: 0
    Dernier message: 21/01/2009, 14h57

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo