Cours programmation réseau en C++

UDP – Découpage et unification de paquets & création d’un protocole ordonné non fiable

Bousk

Cet article présente comment découper un message en paquets pour l’envoi ainsi que la façon de reconstituer le message à la réception, dans le cadre de la création d’un protocole ordonné non fiable.

Commentez

- Titre : Cours programmation réseau en C++

- Auteur : Bousk

- Parution : 30 avril 2019

- Mise à jour : 15 juillet 2019

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Cyrille (Bousk) Bousquet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Datagramme

- I-A. Messages plus gros qu’un datagramme

- I-B. Messages plus petits qu’un datagramme

- I-B-1. Envoyer plusieurs messages par datagramme

- II. Paquets

- II-A. En-tête d’un paquet

- II-B. Identifiant de paquet

- II-C. Type de paquet

- II-D. Taille du paquet

- II-D-1. Taille d’un message

- II-E. Structure de paquet

- III. Multiplexeur, Démultiplexeur

- III-A. Interface

- III-B. Fonctionnement du multiplexeur

- III-C. Fonctionnement du démultiplexeur

- IV. Implémentation du multiplexeur

- IV-A. Variables membres

- IV-B. void queue(std::vector<uint8_t>&& data);

- IV-C. size_t serialize(uint8_t* buffer, const size_t buffersize);

- V. Implémentation du démultiplexeur

- V-A. Variables membres

- V-B. void onDataReceived(const uint8_t* data, const size_t datasize);

- V-C. std::vector<std::vector<uint8_t>> process();

- VI. Tests

I - Datagramme

Nous avons vu dès le premier chapitre UDP qu’un datagramme envoyé devrait avoir une taille aux alentours de 1400 o.

Mais en pratique, il est plutôt rare, voire quasiment impossible, que chaque message envoyé fasse exactement 1400 o.

I-A - Messages plus gros qu’un datagramme

Si un message dépasse cette taille limite, il devra être découpé et envoyé via plusieurs datagrammes qu’il faudra réunir à la réception pour reformer le message complet.

Ici plusieurs problématiques apparaissent déjà :

- Comment identifier de manière unique chaque fragment d’un tel message ?

- Comment rattacher chaque fragment à ce message (puisque les envois peuvent être dupliqués et désordonnés) ?

- Faut-il définir une taille maximale, ou un nombre de fragments maximal, pour un tel message ?

I-B - Messages plus petits qu’un datagramme

Si un message a une taille inférieure à cette limite, il ne posera pas problème et pourra être envoyé via un datagramme aisément comme nous le faisions au chapitre précédent. Mais ne peut-on pas faire mieux ?

I-B-1 - Envoyer plusieurs messages par datagramme

Prenons un cas un peu extrême, et pourtant assez commun, d’un message de 15 o. Si notre datagramme peut accepter 1400 o, ça nous laisserait 1385 o libres, voire gâchés puisque notre unité d’échange est le datagramme.

Plutôt que d’envoyer un unique message par datagramme, faisons en sorte d’y empaqueter autant de messages que possible. Ainsi un datagramme pourrait contenir X messages, tous de tailles différentes, dans la limite de sa capacité maximale que nous avons définie à 1400 o.

Toutefois, cette solution vient avec son nouveau lot de problèmes :

- Comment identifier chaque message au sein d’un datagramme ?

- Comment délimiter chaque message au sein d’un datagramme ?

II - Paquets

Il apparaît donc que les notions de message, paquet et datagramme doivent être orthogonales. Un paquet ne peut plus se contenter d’être le simple container d’un message tel que dans les chapitres précédents.

Le datagramme sera l’objet de plus haut niveau qui sera envoyé avec ses propres informations, son propre en-tête, et contiendra un ou plusieurs paquets qui auront également chacun leurs propres informations et en-tête.

Le paquet sera l’objet de plus bas niveau qui servira au transfert d’un message. Un message pourra être représenté par un paquet unique s’il a une taille suffisamment petite. Dans le cas d’un message plus large qu’un paquet, il sera transféré via plusieurs paquets qui sont aussi appelés fragments ou paquet-fragments.

Un fragment est un paquet particulier mais un paquet avant tout. Aussi il est simple de mélanger les deux concepts et c’est souvent le cas par abus de langage.

Dans la suite du cours, sera appelé paquet l’unité envoyée via un datagramme, indépendamment qu’il s’agisse d’un paquet ou d’un fragment.

Lorsque je ferai référence à un fragment spécifiquement, le terme de paquet-fragment sera utilisé.

Si je fais référence à un paquet non fragment, il s’agira d’un paquet-message.

II-A - En-tête d’un paquet

Chaque paquet sera précédé d’un en-tête afin de l’identifier.

Un tel en-tête possédera :

- l’identifiant du paquet : un entier non signé incrémenté à chaque paquet envoyé ;

- l’information sur le type de paquet : s’il s’agit d’un message entier ou d’un fragment ;

- la taille du paquet, afin de délimiter le tampon à traiter.

II-B - Identifiant de paquet

Tout comme nous l’avons déjà réalisé pour le datagramme, il s’agira d’ajouter un identifiant, un entier non signé qui s’incrémente, à chaque paquet envoyé. Et pour des raisons similaires au datagramme, un uint16_t sera choisi ici aussi.

II-C - Type de paquet

Le type de paquet sera caractérisé par une énumération possédant 4 valeurs :

- Packet pour un message complet ;

- FirstFragment pour la première partie d’un message découpé ;

- LastFragment pour la dernière partie d’un message découpé ;

- Fragment pour les parties centrales d’un message découpé, s’il y en a.

Plusieurs raisons pour soutenir ce choix :

- avec seulement quatre valeurs, ce champ pourra être optimisé pour tenir sur seulement 2 bits par la suite ;

- en distinguant les trois types de fragments, il est plus simple de détecter les parts d’un message. En particulier si le dernier fragment du message précédent est perdu alors que nous avons reçu le premier fragment d’un nouveau message, il n’y a pas de confusion possible.

II-D - Taille du paquet

Puisque la limite d’un datagramme est d’environ 1400 o, un paquet ne pourra jamais dépasser cette taille. Aussi un uint16_t fera ici aussi l’affaire.

II-D-1 - Taille d’un message

Vu que la taille d’un paquet est limitée, il convient également de limiter la taille d’un message : de délimiter combien de fragments peuvent définir un unique message.

Cette limite est arbitraire et définira en particulier la quantité de mémoire utilisée par votre moteur. Posons une limite de 32 fragments pour l’article et le cours, ce qui permet de transférer des messages de taille maximale ~43 Ko. Ceci devrait permettre de couvrir tous les cas y compris un peu extrêmes, mais gardez à l’esprit qu’une plus grande fragmentation augmente les chances de perte, comme le rappelle le tableau ci-dessous. Il ne faut donc pas en abuser - et favoriser les messages plus petits.

Et si par hasard vous avez besoin d’envoyer des données plus larges, il est tout à fait possible de les découper au préalable dans l’application plutôt que de laisser ce travail au moteur réseau.

Pertes

# fragments

Chances de livraison

Pertes

# fragments

Chances de livraison

1.00%

1

99.00%

5.00%

1

95.00%

2

98.01%

2

90.25%

3

97.03%

3

85.74%

4

96.06%

4

81.45%

5

95.10%

5

77.38%

6

94.15%

6

73.51%

7

93.21%

7

69.83%

8

92.27%

8

66.34%

9

91.35%

9

63.02%

10

90.44%

10

59.87%

15

86.01%

15

46.33%

20

81.79%

20

35.85%

25

77.78%

25

27.74%

30

73.97%

30

21.46%

32

72.50%

32

19.37%

5% est la valeur généralement utilisée pour implémenter et déboguer un moteur réseau afin d’être résistant et pouvoir faire face aux pertes. Sur une connexion sans problème particulier, via internet, le taux de perte devrait être < 1% voire quasi-nul. Mais même le plus petit taux de perte est vite démultiplié si le message est très fragmenté.

II-E - Structure de paquet

Avec les informations des paragraphes précédents, nous arrivons donc sur une première structure de paquet comme ceci :

Structure de paquet

1. struct Packet

2. {

3. enum class Type : uint8_t

4. {

5. Packet,

6. FirstFragment,

7. Fragment,

8. LastFragment,

9. };

10. struct Header

11. {

12. uint16_t id;

13. uint16_t size;

14. Type type;

15. };

16. static constexpr uint16_t PacketMaxSize = Datagram::DataMaxSize;

17. static constexpr uint16_t HeaderSize = sizeof(Header);

18. static constexpr uint16_t DataMaxSize = PacketMaxSize - HeaderSize;

19. static constexpr size_t MaxPacketsPerMessage = 32;

20. static constexpr size_t MaxMessageSize = MaxPacketsPerMessage * DataMaxSize;

22. Header header;

23. std::array<uint8_t, DataMaxSize> data;

24. };

Très certainement perfectible, ceci sera néanmoins notre base de travail.

III - Multiplexeur, Démultiplexeur

L’idée est donc d’avoir un traitement supplémentaire sur nos messages et paquets. Quand un message trop gros est mis en file d’envoi, il doit être découpé en plusieurs paquet-fragments de tailles correctes qu’il faudra recoller à la réception. Quand la file contient des petits paquets, il faut être capable d’en empaqueter plusieurs et de les redécouper à l’arrivée.

III-A - Interface

Nous aurons donc deux objets : un multiplexeur qui contiendra la file d’envoi, et un démultiplexeur qui effectuera les traitements à la réception.

Interface du multiplexeur et démultiplexeur

1. class Multiplexer

2. {

3. public:

4. Multiplexer() = default;

5. ~Multiplexer() = default;

7. void queue(std::vector<uint8_t>&& messageData);

8. size_t serialize(uint8_t* buffer, const size_t buffersize);

9. };

10. class Demultiplexer

11. {

12. public:

13. Demultiplexer() = default;

14. ~Demultiplexer() = default;

16. void onDataReceived(const uint8_t* data, const size_t datasize);

17. std::vector<std::vector<uint8_t>> process();

18. };

III-B - Fonctionnement du multiplexeur

Le multiplexeur ne devrait pas être un challenge d’implémentation. Il aura pour but de contenir la liste d’envoi des paquets, et de les sérialiser dans le tampon fourni pour envoyer les données.

Lors de la mise en file d’envoi des données, il devra vérifier la taille des données du message à envoyer, afin de les découper en paquets de taille correcte si nécessaire. Si les données sont déjà suffisamment petites, on se contentera de les mettre en queue.

III-C - Fonctionnement du démultiplexeur

UDP pouvant entraîner des pertes de données, la réception pourra avoir des trous (c’est-à-dire : réception du paquet #1, perte du paquet #2, réception du paquet #3…).

Le démultiplexeur agira donc ainsi :

- tous les paquets valides reçus seront stockés ;

- après chaque réception, l’appel à process retournera les messages complets s’il y en a ;

- après la réception du message terminé par le paquet N, les paquets précédents, s’il y en a, seront défaussés.

Ceci sera une implémentation dite ordonnée non fiable : en supposant des paquets-messages, la réception des paquets #1, #2, #4, #3 dans cet ordre retournera les paquets #1, #2, #4 dans cet ordre et le paquet #3 sera ignoré puisque reçu après #4.

IV - Implémentation du multiplexeur

Commençons par le plus simple : le multiplexeur.

IV-A - Variables membres

Le multiplexeur aura besoin de deux variables membres : la liste des paquets en file d’envoi et un compteur pour savoir quel est l’identifiant à assigner au paquet suivant.

1. class Multiplexer

2. {

3. …

4. private:

5. std::vector<Packet> mQueue;

6. Packet::Id mNextId{ 0 };

7. };

IV-B - void queue(std::vector<uint8_t>&& data);

La fonction queue permettra de mettre les données d’un message en file d’envoi, et de les découper au passage en plusieurs paquet-fragments si la quantité envoyée est plus grosse que la taille maximale autorisée pour un paquet.

Multiplexer::queue

1. void Multiplexer::queue(std::vector<uint8_t>&& msgData)

2. {

3. //<! S’assurer que notre message ne dépasse pas la limite auto-imposée

4. assert(msgData.size() <= Packet::MaxMessageSize);

5. if (msgData.size() > Packet::DataMaxSize)

6. {

7. size_t queuedSize = 0;

8. while (queuedSize < msgData.size())

9. {

10. const auto fragmentSize = std::min(Packet::DataMaxSize,  static_cast<uint16_t>( msgData.size()- queuedSize));

11. Packet packet;

12. packet.header.id = mNextId++;

13. packet.header.type = ((queuedSize == 0) ? Packet::Type::FirstFragment : Packet::Type::Fragment);

14. packet.header.size = fragmentSize;

15. memcpy(packet.data(), msgData.data() + queuedSize, fragmentSize);

16. mQueue.push_back(packet);

17. queuedSize += fragmentSize;

18. }

19. mQueue.back().header.type = Packet::Type::LastFragment;

20. assert(queuedSize == msgData.size());

21. }

22. else

23. {

24. //!< Message n’ayant pas besoin d’être découpé

25. Packet packet;

26. packet.header.id = mNextId++;

27. packet.header.type = Packet::Type::Packet;

28. packet.header.size = static_cast<uint16_t>( msgData.size());

29. memcpy(packet.data(),msgData.data(),msgData.size());

30. mQueue.push_back(packet);

31. }

32. }

IV-C - size_t serialize(uint8_t* buffer, const size_t buffersize);

La fonction serialize sera appelée lorsque l’on souhaite envoyer un datagramme afin d’y inclure nos paquets. Puisque notre implémentation est ordonnée non fiable, il suffira d’itérer sur la file et inclure les X premiers éléments jusqu’à remplir le tampon fourni. Une fois transmis, ils pourront être supprimés de la liste afin d’envoyer les suivants par la suite.

Multiplexer::serialize

1. size_t Multiplexer::serialize(uint8_t* buffer, const size_t buffersize)

2. {

3. size_t serializedSize = 0;

4. for (auto packetit = mQueue.cbegin(); packetit != mQueue.cend();)

5. {

6. const auto& packet = *packetit;

7. if (serializedSize + packet.size() > buffersize)

8. break; //<! Pas assez d’espace libre dans le tampon pour inclure ce paquet

10. memcpy(buffer, packet.buffer(), packet.size());

11. serializedSize += packet.size();

12. buffer += packet.size();

14. //<! Une fois qu’un paquet a été sérialisé, on le retire de la file d’envoi

15. packetit = mQueue.erase(packetit);

16. }

17. return serializedSize;

18. }

V - Implémentation du démultiplexeur

Le démultiplexeur aura pour responsabilité d’extraire les paquets reçus du datagramme, réassembler les messages fragmentés et défausser ce qui doit l’être afin de fournir à l’application ce qui a été défini plus haut : des messages ordonnés non fiables.

V-A - Variables membres

Pour remplir ces tâches, nous aurons besoin d’une liste des paquets reçus avant potentiel réassemblage ainsi que l’identifiant du paquet traité le plus récent afin de défausser les plus anciens et répondre à notre besoin d’implémentation ordonnée non fiable.

L’astuce consiste à maintenir notre liste de paquets triée de façon à simplifier son traitement par la suite.

V-B - void onDataReceived(const uint8_t* data, const size_t datasize);

Cette fonction sera le point d’entrée des données reçues. Il faudra ici séparer les paquets s’il y en a plusieurs dans notre tampon, puis traiter chaque paquet ainsi extrait pour s’assurer qu’il ne s’agit pas d’un doublon avant de l’insérer à la bonne place dans notre liste de paquets à traiter.

Le traitement est miroir à ce qui a été fait pour Multiplexer::serialize.

Demultiplexer::onDataReceived

1. void Demultiplexer::onDataReceived(const uint8_t* data, const size_t datasize)

2. {

3. //<! Extraire les paquets du tampon

4. size_t processedDataSize = 0;

5. while (processedDataSize < datasize)

6. {

7. const Packet* pckt = reinterpret_cast<const Packet*>(data);

8. if (processedDataSize + pckt->size() > datasize || pckt->datasize() > Packet::DataMaxSize)

9. {

10. //<! Paquet ou tampon mal formé, on interrompt

11. return;

12. }

13. onPacketReceived(pckt);

14. processedDataSize += pckt->size();

15. data += pckt->size();

16. }

17. }

Afin que le code soit plus clair, le traitement du paquet a été extrait du traitement du tampon dans sa propre fonction onPacketReceived.

Demultiplexer::onPacketReceived

1. void Demultiplexer::onPacketReceived(const Packet* pckt)

2. {

3. if (!Utils::IsSequenceNewer(pckt->id(), mLastProcessed))

4. return; //<! Paquet trop vieux

6. //<! Trouver l’emplacement dans la liste pour ce paquet, notre liste doit rester ordonnée

7. if (mPendingQueue.empty() || Utils::IsSequenceNewer (pckt->id(), mPendingQueue.back().id()))

8. {

9. mPendingQueue.push_back(*pckt);

10. }

11. else

12. {

13. //<! Trouver le premier itérateur avec un identifiant égal à ou plus récent que notre paquet, nous devons placer le paquet avant celui-ci

14. auto insertLocation = std::find_if(mPendingQueue.cbegin(), mPendingQueue.cend(), [&pckt](const Packet& p) { return p.id() == pckt->id() || Utils::IsSequenceNewer(p.id(), pckt->id()); });

15. //<! Assurons-nous de ne pas insérer un paquet en doublon

16. if (insertLocation->id() != pckt->id())

17. {

18. mPendingQueue.insert(insertLocation, *pckt);

19. }

20. }

21. }

V-C - std::vector<std::vector<uint8_t>> process();

Cette fonction retourne les messages complets récupérés depuis le dernier appel.

Il s’agit de traiter notre liste de paquets reçus, afin d’en extraire les éventuels messages complets pour les retourner à l’appelant. Puisque notre liste est ordonnée, il suffit d’itérer sur celle-ci afin de les extraire dans l’ordre, sans oublier de mettre à jour le paquet le plus récent reçu afin de ne pas accepter de paquets plus anciens lors des réceptions ultérieures.

Commençons par la partie simple : le cas où le message n’est pas découpé.

Demultiplexer::process

1. std::vector<std::vector<uint8_t>> Demultiplexer::process()

2. {

3. std::vector<std::vector<uint8_t>> messagesReady;

5. auto itPacket = mPendingQueue.cbegin();

6. auto itEnd = mPendingQueue.cend();

7. std::vector<Packet>::const_iterator newestProcessedPacket;

8. while(itPacket != itEnd)

9. {

10. if (itPacket->type() == Packet::Type::Packet)

11. {

12. //<! Paquet-message, on recopie toutes ses données

13. std::vector<uint8_t> msg(itPacket->data(), itPacket->data() + itPacket->datasize());

14. messagesReady.push_back(std::move(msg));

15. newestProcessedPacket = itPacket;

16. //<! On N’oublie pas d’incrémenter l’itérateur pour passer au paquet suivant

17. ++itPacket;

18. }

19. else if (itPacket->type() == Packet::Type::FirstFragment)

20. //<! Cas d’un message fragmenté, voir plus bas

21. else

22. {

23. //<! Ce paquet est un fragment que l’on ne peut pas traiter, on le saute

24. ++itPacket;

25. }

26. }

29. //<! Si des messages ont été extraits, on doit mettre à jour notre état interne

30. if (!messagesReady.empty())

31. {

32. //<! Mise à jour du dernier paquet traité, afin de refuser les plus anciens par la suite (protocole ordonné non-fiable)

33. mLastProcessed = newestProcessedPacket->id();

34. //<! Retirer tous les paquets traités de la file, y compris le dernier traité

35. mPendingQueue.erase(mPendingQueue.cbegin(), std::next(newestProcessedPacket));

36. }

38. return messagesReady;

39. }

La partie non triviale maintenant : si le message est fragmenté en plusieurs paquets. Pas beaucoup plus compliqué, puisque notre file est ordonnée, il suffit de passer en revue les paquets suivants, jusqu’à trouver un paquet de type LastFragment et en s’assurant que leurs identifiants sont tous consécutifs.

Réassemblage de message fragmenté

1. else if (itPacket->type() == Packet::Type::FirstFragment)

2. {

3. //<! Il faut vérifier que le message est prêt, s’il est entièrement reçu

4. std::vector<uint8_t> msg = [&]()

5. {

6. std::vector<uint8_t> msg(itPacket->data(), itPacket->data() + itPacket->datasize());

7. //<! Ceci est l’identifiant de paquet à suivre, il s’incrémentera avec le parcours de la liste

8. auto expectedPacketId = itPacket->id();

9. //<! Le premier paquet est le premier fragment, on passe directement au suivant

10. ++itPacket;

11. ++expectedPacketId;

12. //<! Tant que nous avons des paquets dans la liste et qu’ils ont l’identifiant voulu, on traite le paquet

13. while (itPacket != itEnd && itPacket->id() == expectedPacketId)

14. {

15. if (itPacket->type() == Packet::Type::LastFragment)

16. {

17. //<! Dernier fragment atteint, le message est complet

18. msg.insert(msg.cend(), itPacket->data(), itPacket->data() + itPacket->datasize());

19. return msg;

20. }

21. else if (itPacket->type() != Packet::Type::Fragment)

22. {

23. //<! Si on rentre ici, nous avons reçu un paquet mal formé ou mal intentionné

24. msg.clear();

25. return msg;

26. }

27. //<! Il s’agit d’un fragment valide à extraire et insérer dans le message final

28. msg.insert(msg.cend(), itPacket->data(), itPacket->data() + itPacket->datasize());

29. ++itPacket;

30. ++expectedPacketId;

31. }

32. //<! Si on arrive ici, le message est incomplet ou a une partie manquante

33. msg.clear();

34. return msg;

35. }();

36. if (!msg.empty())

37. {

38. //<! Nous avons un message !

39. messagesReady.push_back(std::move(msg));

40. newestProcessedPacket = itPacket;

41. //<! On déplace l’itérateur de parcours de la liste après le dernier paquet traité, qui est le dernier paquet du message

42. ++itPacket;

43. }

44. }

J’ai choisi de faire ce travail en utilisant une fonction lambda qui modifie l’itérateur des paquets et retourne le tampon du message complété. Il est tout à fait possible de le faire via une autre fonction membre privée, ceci est au goût de chacun.

Après l’exécution de cette fonction, l’itérateur de parcours de la liste des paquets pointe vers le dernier paquet traité qui est le dernier fragment du message si le message est complété.

Si le message n’a pas été complété, il peut s’agir d’un fragment du message dont un paquet est manquant, d’un paquet non lié au message ou de la fin de la liste. Dans ce cas, on laisse la boucle continuer et gérer son cas lors de sa prochaine itération.

VI - Tests

Comme d’habitude, quelques tests ont été écrits pour vérifier le bon fonctionnement.

PacketHandling_Test.hpp

1. #pragma once

3. #include "Tester.hpp"

4. #include "UDP/PacketHandling.hpp"

6. #include <limits>

7. #include <chrono>

8. #include <random>

10. class Multiplexer_Test

11. {

12. public:

13. static void Test();

14. };

16. void Multiplexer_Test::Test()

17. {

18. Bousk::Network::UDP::Multiplexer mux;

19. CHECK(mux.mQueue.size() == 0);

20. CHECK(mux.mNextId == 0);

21. {

22. const std::array<uint8_t, 5> arr{ 'T', 'o', 't', 'o', '\0' };

23. std::vector<uint8_t> data(arr.cbegin(), arr.cend());

24. mux.queue(std::move(data));

25. CHECK(mux.mQueue.size() == 1);

26. CHECK(mux.mNextId == 1);

28. std::array<uint8_t, Bousk::Network::UDP::Packet::PacketMaxSize> buffer;

29. const size_t serializedData = mux.serialize(buffer.data(), buffer.size());

30. CHECK(serializedData == Bousk::Network::UDP::Packet::HeaderSize + arr.size());

31. const Bousk::Network::UDP::Packet* packet = reinterpret_cast<const Bousk::Network::UDP::Packet*>(buffer.data());

32. CHECK(packet->datasize() == arr.size());

33. CHECK(memcmp(packet->data(), arr.data(), packet->datasize()) == 0);

34. }

35. {

36. //!< Envoi d’un message fragmenté : 3 fragments

37. std::vector<uint8_t> data(Bousk::Network::UDP::Packet::DataMaxSize * 3, 0);

38. const unsigned int seed = static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count());

39. std::default_random_engine generator(seed);

40. std::uniform_int_distribution<unsigned int> distribution(0, 100);

41. for (uint8_t& d : data)

42. d = distribution(generator);

44. const auto datacopy = data;

45. mux.queue(std::move(data));

46. CHECK(mux.mQueue.size() == 3);

47. CHECK(mux.mNextId == 4);

48. size_t totalDataSize = 0;

49. for (; !mux.mQueue.empty(); )

50. {

51. std::array<uint8_t, Bousk::Network::UDP::Packet::PacketMaxSize> buffer;

52. const size_t serializedData = mux.serialize(buffer.data(), buffer.size());

53. const Bousk::Network::UDP::Packet* packet = reinterpret_cast<const Bousk::Network::UDP::Packet*>(buffer.data());

54. CHECK(memcmp(packet->data(), datacopy.data() + totalDataSize, packet->datasize()) == 0);

55. totalDataSize += serializedData - Bousk::Network::UDP::Packet::HeaderSize;

56. }

57. CHECK(totalDataSize == datacopy.size());

58. }

59. }

61. class Demultiplexer_Test

62. {

63. public:

64. static void Test();

65. };

67. void Demultiplexer_Test::Test()

68. {

69. //!< Utilisation du multiplexer pour facilement mettre en file et découper les données

70. //!< Il est testé au préalable donc son utilisation est fiable

71. Bousk::Network::UDP::Multiplexer mux;

72. Bousk::Network::UDP::Demultiplexer demux;

73. CHECK(demux.mPendingQueue.empty());

74. CHECK(demux.mLastProcessed == std::numeric_limits<Bousk::Network::UDP::Packet::Id>::max());

75. {

76. const std::array<uint8_t, 5> arr0{ 'T', 'o', 't', 'o', '\0' };

77. std::vector<uint8_t> data0(arr0.cbegin(), arr0.cend());

78. //!< Réception des paquets 0 & 1

79. {

80. mux.queue(std::move(data0));

81. Bousk::Network::UDP::Packet packet;

82. mux.serialize(reinterpret_cast<uint8_t*>(&packet), Bousk::Network::UDP::Packet::PacketMaxSize);

83. CHECK(packet.id() == 0);

84. CHECK(packet.type() == Bousk::Network::UDP::Packet::Type::Packet);

85. CHECK(packet.datasize() == static_cast<uint16_t>(arr0.size()));

86. CHECK(memcmp(packet.data(), arr0.data(), arr0.size()) == 0);

87. demux.onDataReceived(packet.buffer(), packet.size());

88. }

89. const std::array<uint8_t, 5> arr1{ 'T', 'a', 't', 'a', '\0' };

90. std::vector<uint8_t> data1(arr1.cbegin(), arr1.cend());

91. {

92. mux.queue(std::move(data1));

93. Bousk::Network::UDP::Packet packet;

94. mux.serialize(reinterpret_cast<uint8_t*>(&packet), Bousk::Network::UDP::Packet::PacketMaxSize);

95. CHECK(packet.id() == 1);

96. CHECK(packet.type() == Bousk::Network::UDP::Packet::Type::Packet);

97. CHECK(packet.datasize() == static_cast<uint16_t>(arr1.size()));

98. CHECK(memcmp(packet.data(), arr1.data(), arr1.size()) == 0);

99. demux.onDataReceived(packet.buffer(), packet.size());

100. }

101. CHECK(demux.mPendingQueue.size() == 2);

102. const std::vector<std::vector<uint8_t>> packets = demux.process();

103. CHECK(packets.size() == 2);

104. CHECK(demux.mLastProcessed == 1);

105. CHECK(packets[0].size() == data0.size());

106. CHECK(packets[0] == data0);

107. CHECK(packets[1].size() == data1.size());

108. CHECK(packets[1] == data1);

109. }

110. {

111. //!< Réception d’un message fragmenté : 3 fragments

112. std::vector<uint8_t> data(Bousk::Network::UDP::Packet::DataMaxSize * 3, 0);

113. const unsigned int seed = static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count());

114. std::default_random_engine generator(seed);

115. std::uniform_int_distribution<unsigned int> distribution(0, 100);

116. for (uint8_t& d : data)

117. d = distribution(generator);

119. const auto datacopy = data;

120. mux.queue(std::move(data));

121. for (;;)

122. {

123. Bousk::Network::UDP::Packet packet;

124. const size_t serializedData = mux.serialize(reinterpret_cast<uint8_t*>(&packet), Bousk::Network::UDP::Packet::PacketMaxSize);

125. if (serializedData == 0)

126. break;

127. demux.onDataReceived(packet.buffer(), packet.size());

128. }

129. CHECK(demux.mPendingQueue.size() == 3);

130. const std::vector<std::vector<uint8_t>> packets = demux.process();

131. CHECK(packets.size() == 1);

132. CHECK(demux.mLastProcessed == 4);

133. CHECK(packets[0].size() == datacopy.size());

134. CHECK(packets[0] == datacopy);

135. }

136. }

138. class PacketHandling_Test

139. {

140. public:

141. static void Test()

142. {

143. Multiplexer_Test::Test();

144. Demultiplexer_Test::Test();

145. }

146. };

Récupérez le code source sous le tag V1.0.5 dans le dépôt Mercurial à l’adresse suivante.

Article précédent

Article suivant

<< UDP – Créer son propre protocole par-dessus UDP

UDP – Envoi de paquets ordonné fiable >>