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);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.
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;
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 :
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.).
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 >; };
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 :
Puis d'�crire :
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, ... };
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.
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>; };
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 ?
Partager