I. Prémices de protocole▲
Les plus attentifs auront sans doute remarqué que notre datagramme s’est vu doter d’un en-tête comportant bien plus qu’un seul identifiant lors du deuxième chapitre.
En effet, notre datagramme possède un champ pour informer la machine distante, lors de la réception du datagramme en question, de l’identifiant le plus récent reçu, ainsi que de l’état, reçu ou non, des 64 précédents datagrammes.
Ces deux champs sont la pierre angulaire de la création de notre protocole : en ayant cette information partagée entre les parties, il devient possible de savoir quelles informations ont été reçues ou non et d’agir en conséquence !
Ce chapitre va vous montrer comment cet en-tête doit être utilisé.
II. Fonctionnement du moteur▲
Le moteur réseau que l’on créera dans le cadre de ce cours traitera des données pures, des tampons (buffers) bruts. C’est-à-dire que la phase de sérialisation devra être réalisée en amont par l’application, et c’est le tampon résultant de cette sérialisation qui sera passé au moteur pour être envoyé. Si vous suivez également les traductions de la série « Construire un protocole de jeu en réseau », vous verrez que dans cet article, Glenn prend une tout autre direction avec une structure de paquet de base dont doit hériter chaque paquet à transmettre, et c’est le moteur qui sérialise le paquet avant chaque envoi dans le tampon qui sera transmis.
Je favorise cette approche afin que la sérialisation, qui est un processus lent, ne soit exécutée qu’une seule fois. Ceci permet aussi un découpage net entre le moteur réseau et le reste de l’application, puisqu’aucune hiérarchie ni interface n’est imposée par le moteur réseau à l’application pour le format des structures de données. L’application est libre de définir et implémenter les structures de données à échanger comme bon lui semble.
Le tampon fourni par l’application sera appelé message sérialisé. La structure de données ayant servi à créer ce tampon sera le message. C’est ce tampon qui sera transféré à l’autre machine. À la réception, le moteur fournira les données sous forme de Message UserData tel qu’introduit dans ce chapitre TCP. Puis celles-ci devront être désérialisées par l’application afin d’obtenir les données d’origine.
Donc pour résumer :
- l’application traite les structures de données de son choix, puis fournit un tampon que l’on appellera message sérialisé au moteur réseau ;
- lors du passage au moteur réseau, le message sérialisé est transformé en paquet ;
- le paquet est ensuite emballé dans un datagramme et c’est ce dernier qui sera envoyé via sendto.
Emetteur | OS, routeurs, câbles… | récepteur | ||||||||||
application | moteur réseau | sendto | … | recvfrom | moteur réseau | application | ||||||
message | > | paquet | > | datagramme | > | … | > | datagramme | > | paquet | > | message |
À la réception, c’est l’opération inverse qui est effectuée, comme l’indique le graphique ci-dessus :
- Le datagramme est reçu via recvfrom.
- Le paquet est extrait du datagramme.
- Le message sérialisé est mis à disposition de l’application.
Cette approche a l’inconvénient de gâcher quelques bits si le paquet n’a pas une taille alignée sur un octet, au bénéfice de consommer moins de ressources puisque la sérialisation n’a pas à être effectuée plusieurs fois en cas de renvoi.
Il s’agit donc du premier endroit où vous pourriez dévier de l’implémentation proposée dans le cadre de ce cours. Mais pas de panique, celui-ci reste valide et applicable quel que soit votre choix. Réfléchissez-donc bien à l’implémentation que vous souhaitez en pesant les avantages et inconvénients de chaque option.
Tout particulièrement, avec l’approche de Glenn, il est possible d’avoir une callback pour informer qu’un message n’a pas été remis, afin que l’application puisse réagir : modifier le message, le renvoyer tel quel, choisir de l’abandonner…
En pratique, bien que cette fonctionnalité soit presque toujours mentionnée lors d’un développement de moteur réseau, il s’avère qu’elle a un intérêt moindre :
- tout ceci étant généralement exécuté sur différents threads, avoir une telle callback obligerait quasi-obligatoirement à ajouter des points de synchronisation (mutex, lock) et aurait un impact sur les performances, mettant en pause la frame de jeu ou réseau, ce qui retarderait l’affichage ou l’envoi de données ;
- le traitement est rarement une opération pouvant être exécutée à tout moment mais est liée au gameplay ;
- il est finalement plus simple et robuste de prendre en compte qu’un message peut être perdu et de bâtir l’application en conséquence.
Il est toujours possible de passer d’une implémentation à l’autre, mais un tel changement se compliquera avec l’avancée du projet. En particulier parce que l’application doit s’adapter et être modifiée en conséquence.
III. Client local▲
Le client local sera l’objet qui servira de point d’entrée pour tous nos échanges utilisant UDP pour un port donné. Toutes les données à envoyer via ce port passeront par celui-ci et il réceptionnera tout ce qui arrive sur son port. Ceci sera donc l’interface principale entre l’application et un socket ouvert sur le port local souhaité.
III-A. Quelle peut être l’interface d’un tel objet ?▲
Nous ne nous intéresserons pour le moment qu’aux fonctions d’échanges de données :
- sendTo pour envoyer des données à une adresse ;
- receive pour réceptionner les données en attente.
Afin de garder une cohérence avec l’implémentation TCP et pour démarquer clairement la limite entre le code de la bibliothèque réseau et le code l’utilisant, le système de messages introduits dans cet article sera utilisé et une fonction poll servira à récupérer les messages reçus :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
class
Client
{
public
:
Client();
Client(const
Client&
) =
delete
;
Client(Client&&
) =
delete
;
Client&
operator
=
(const
Client&
) =
delete
;
Client&
operator
=
(Client&&
) =
delete
;
~
Client();
bool
init(uint16_t port);
void
release();
void
sendTo(destinataire, std::
vector<
uint8_t>&&
data);
void
receive();
std::
vector<
std::
unique_ptr<
Messages::
Base>>
poll();
}
;
III-A-1. Qu’est le destinataire ?▲
Le destinataire devra définir à qui les données sont envoyées. D’après la signature de sendto vue au premier chapitre, il devrait s’agir d’un const sockaddr*. Toutefois, on y préférera un sockaddr_storage afin d’avoir une structure utilisable en IPv4 et IPv6. Bien que le cours utilise uniquement IPv4 jusque-là, autant commencer à utiliser la structure la plus adaptée vu qu’il n’y a aucune différence ni difficulté réelle à avoir l’une ou l’autre ici.
III-A-2. Squelette d’un programme type cible▲
L’objectif au terme de ce chapitre est de pouvoir manipuler un tel objet avec un code proche de
Client client;
client.init(8888
) ;
client.sendTo(destinataire, …);
client.receive();
auto
messages =
client.poll();
for
(auto
&&
msg : messages)
{
// traitement du message reçu
}
En proposant une interface similaire, il sera relativement aisé de passer d’une implémentation utilisant TCP à UDP. Ceci est particulièrement intéressant si vous souhaitez développer le moteur en parallèle de votre projet, afin d’avoir une version rapidement fonctionnelle via TCP, pour que le reste de l’équipe n’ait pas à vous attendre, avant d’implémenter la version optimisée utilisant UDP. Cela représente également un intérêt si vous voulez passer de TCP à UDP selon les projets au sein d’un même moteur.
IV. Client distant▲
La structure de client distant sera la représentation locale d’un client distant.
Pour faire une analogie avec le cours TCP : le client local est le socket serveur, et le client distant est le socket client sur ce serveur.
IV-A. Utilité du client distant▲
Puisque l’utilisation d’UDP se fait via un unique socket utilisé pour envoyer à tous les destinataires, absolument rien ne nous lie à ces destinataires. Il faudra donc avoir une liste de ces clients, afin d’avoir un suivi des identifiants de datagrammes reçus et à envoyer pour chacun d’eux, pour pouvoir défausser les duplicatas.
IV-B. Interface du client distant▲
Sans surprise, l’interface est assez similaire à celle du client local, puisqu’il s’agit plus ou moins d’une simple redirection de traitement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
class
DistantClient
{
public
:
DistantClient();
DistantClient(const
DistantClient&
) =
delete
;
DistantClient(DistantClient&&
) =
delete
;
DistantClient&
operator
=
(const
DistantClient&
) =
delete
;
DistantClient&
operator
=
(DistantClient&&
) =
delete
;
~
DistantClient();
void
send(std::
vector<
uint8_t>&&
data);
void
onDatagramReceived(Datagram&&
datagram);
private
:
Datagram::
ID mNextDatagramIdToSend{
0
}
; // Identifiant du prochain datagramme à envoyer
}
;
IV-B-1. Fonctionnement du client distant▲
L’idée est de faire passer toutes les données reçues et à envoyer à ce client via cette classe afin qu’elle gère le suivi des échanges. En particulier, elle sera responsable du suivi de l’identifiant à envoyer, mais aussi du suivi des acquittements afin d’interrompre les traitements quand un datagramme est reçu en doublon.
Et si les données sont valides, elle les transmettra au client local afin que l’application puisse les récupérer.
IV-B-2. Comment y parvenir ?▲
Pour accomplir ces tâches, nous réutiliserons le gestionnaire d’acquittements créé lors du deuxième chapitre. Celui-ci possède une interface adaptée à notre besoin présent, et d’après les tests mis en place lors du chapitre précédent, il est tout à fait fonctionnel. Nous pouvons donc bâtir sur sa base à moindre risque.
IV-B-2-a. Envoi de données▲
L’envoi ne devrait pas poser de problème particulier. Il s’agira d’envoyer un datagramme en remplissant les champs de la structure introduite dans le second chapitre.
2.
3.
4.
5.
6.
7.
8.
9.
void
DistantClient::
send(std::
vector<
uint8_t>&&
data)
{
Datagram datagram;
datagram.header.id =
htons(mNextDatagramIdToSend);
++
mNextDatagramIdToSend;
memcpy(datagram.data.data(), data.data(), data.size());
sendto(mClient.mSocket, reinterpret_cast
<
const
char
*>
(&
datagram), static_cast
<
int
>
(Datagram::
HeaderSize +
data.size()), 0
, reinterpret_cast
<
const
sockaddr*>
(&
mAddress), sizeof
(mAddress));
}
IV-B-3. Traitement des données reçues▲
C’est ici que ça devient plus intéressant. Le traitement des données reçues couvre plusieurs points :
- vérifier que le datagramme n’est pas un doublon ;
- vérifier que le datagramme n’est pas trop vieux et désordonné le rendant obsolète ;
- vérifier la mise à jour des acquittements reçus ;
- vérifier la mise à disposition des données à l’application.
Pour couvrir les deux premiers points, nous utiliserons à nouveau un gestionnaire d’acquittements. Celui-ci nous fournira un suivi glissant des datagrammes reçus, permettant de détecter les doublons et les datagrammes trop anciens afin de les rejeter.
Ce gestionnaire servira également au troisième point.
Avec un découpage net permettant de traiter les données puis le message, le code devrait être plus facile à (re)lire :
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
DistantClient
{
…
private
:
void
onDataReceived(std::
vector<
uint8_t>&&
data);
void
onMessageReady(std::
unique_ptr<
Messages::
Base>&&
msg);
private
:
AckHandler mReceivedAcks; //!
< Pour détecter les duplications
}
;
Nous pouvons maintenant implémenter la réception en soi :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
void
DistantClient::
onDatagramReceived(Datagram&&
datagram)
{
const
auto
datagramid =
ntohs(datagram.header.id);
//!
< Mise à jour des datagrammes reçus
mReceivedAcks.update(datagramid, datagram.header.previousAcks, true
);
//!
< Ignorer les duplications
if
(!
mReceivedAcks.isNewlyAcked(datagramid))
return
;
//!
< Gérer les données reçues
onDataReceived(std::
vector<
uint8_t>
(datagram.data.data(), datagram.data.data() +
datagram.datasize));
}
Ce qui mène à l’implémentation de onDataReceived :
2.
3.
4.
void
DistantClient::
onDataReceived(std::
vector<
uint8_t>&&
data)
{
onMessageReady(std::
make_unique<
Messages::
UserData>
(std::
move(data)));
}
Puis onMessageReady implémentée comme ceci :
2.
3.
4.
5.
void
DistantClient::
onMessageReady(std::
unique_ptr<
Messages::
Base>&&
msg)
{
memcpy(&
(msg->
from), &
mAddress, sizeof
(mAddress));
mClient.onMessageReceived(std::
move(msg));
}
Ces fonctions permettent de factoriser les traitements des données et des messages, comme la mise à jour des données communes de tout type de message, à commencer par l’adresse de l’émetteur.
Il s’agit aussi de l’emplacement idéal pour générer quelques statistiques à diverses étapes sur notre connexion avec cette machine. Un chapitre ultérieur traitera ceci plus précisément.
Finalement, il est temps d’implémenter cette nouvelle fonction Client::
onMessageReady :
2.
3.
4.
void
Client::
onMessageReady(std::
unique_ptr<
Messages::
Base>&&
msg)
{
mMessages.push_back(std::
move(msg));
}
L’idée est là aussi de factoriser les traitements des messages reçus, au niveau du client local cette fois. Par exemple, toujours pour parler de statistiques, du nombre de messages reçus sur ce port en particulier, de l’ensemble des clients connus.
V. Implémentation du Client▲
Avant toute chose, il convient d’étoffer cette classe de variables membres comme :
- le socket à utiliser ;
- la liste des destinataires connus pour faire le suivi des données reçues et envoyées (voir plus haut : le Client distant) ;
- les messages reçus prêts à être extraits par l’application.
V-A. Variables membres▲
Ajoutons donc toutes les variables citées plus haut et déclarons DistantClient amie afin qu’elle ait une relation privilégiée sans avoir à afficher le nécessaire totalement public.
2.
3.
4.
5.
6.
7.
8.
9.
class
Client
{
friend
class
DistantClient;
…
private
:
SOCKET mSocket{
INVALID_SOCKET }
;
std::
vector<
std::
unique_ptr<
DistantClient>>
mClients;
std::
vector<
std::
unique_ptr<
Messages::
Base>>
mMessages;
}
;
L’amitié est une relation extrêmement forte qu’il convient de maîtriser et d’utiliser avec parcimonie.
Ici DistantClient est déclarée amie afin d’avoir un accès privilégié à Client, sans avoir à offrir ces accès dans la portée publique qui les rendraient alors utilisables depuis n’importe qui et l’application.
Cette amitié est maîtrisée et aura notamment pour restriction de ne jamais accéder à des variables membres. Il s'agira d'utiliser uniquement les fonctions privées dédiées à cette utilisation.
Malheureusement, rien ne permet cela dans le langage, il s’agira donc d’une restriction implicite et d’une règle autoimposée à suivre.
V-B. Initialisation▲
Cette fonction initialisera un socket IPv4 sur le port choisi et le passera en mode non-bloquant, puisque c’est ce dernier qui sera utilisé pour l’échange de données. Il suffira de copier la création de socket du premier chapitre et l’initialisation du mode non bloquant du cinquième chapitre sur TCP.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
bool
Client::
init(uint16_t port)
{
release();
mSocket =
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if
(mSocket ==
INVALID_SOCKET)
return
false
;
sockaddr_in addr;
addr.sin_addr.s_addr =
INADDR_ANY;
addr.sin_port =
htons(port);
addr.sin_family =
AF_INET;
int
res =
bind(mSocket, reinterpret_cast
<
sockaddr*>
(&
addr), sizeof
(addr));
if
(res !=
0
)
return
false
;
if
(!
SetNonBlocking(mSocket))
return
false
;
return
true
;
}
V-C. Envoi de données▲
sendTo devra transmettre au client distant les données afin d’y appliquer le traitement adéquat et les transformer en un datagramme valide pour notre protocole (ajout de l’en-tête).
Il faudra donc récupérer le client distant correspondant, ou en créer un s’il n’existe pas, afin de lui transmettre les données pour qu’il effectue l’envoi sur le réseau.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
DistantClient&
Client::
getClient(const
sockaddr_storage&
clientAddr)
{
auto
itClient =
std::
find_if(mClients.begin(), mClients.end(), [&
](const
std::
unique_ptr<
DistantClient>&
client) {
return
memcmp(&
(client->
address()), &
clientAddr, sizeof
(sockaddr_storage)) ==
0
; }
);
if
(itClient !=
mClients.end())
return
*
(itClient->
get());
mClients.emplace_back(std::
make_unique<
DistantClient>
(*
this
, clientAddr));
return
*
(mClients.back());
}
void
Client::
sendTo(const
sockaddr_storage&
target, std::
vector<
uint8_t>&&
data)
{
auto
&
client =
getClient(target);
client.send(std::
move(data));
}
memcmp retourne un entier qui vaut 0 si les deux tampons sont égaux. Pour vérifier que les adresses sont identiques il faut donc vérifier memcmp(…) ==
0
.
V-D. Réception de données▲
receive se chargera de recevoir tous les datagrammes en attente et de les transférer aux clients correspondants afin que ceux-ci fassent le suivi des identifiants reçus avant de mettre le nouveau message correspondant en file de réception pour être récupéré par l’application via poll, s’il est accepté.
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.
34.
35.
36.
void
Client::
receive()
{
for
(;;)
{
Datagram datagram;
sockaddr_in from{
0
}
;
socklen_t fromlen =
sizeof
(from);
int
ret =
recvfrom(mSocket, reinterpret_cast
<
char
*>
(&
datagram), Datagram::
BufferMaxSize, 0
, reinterpret_cast
<
sockaddr*>
(&
from), &
fromlen);
if
(ret >
0
)
{
if
(ret >
Datagram::
HeaderSize)
{
datagram.datasize =
ret -
Datagram::
HeaderSize;
auto
&
client =
getClient(reinterpret_cast
<
sockaddr_storage&>
(from));
client.onDatagramReceived(std::
move(datagram));
}
else
{
//!
< Datagramme innatendu
}
}
else
{
if
(ret <
0
)
{
//!
< Gestion des erreurs
const
auto
err =
Errors::
Get();
if
(err !=
Errors::
WOULDBLOCK)
{
//!
< Une erreur s’est produite
}
}
return
;
}
}
}
Notez au passage l’ajout du membre size_t datasize{
0
}
; dans Datagram afin de contenir la taille effective des données du datagramme. Cette variable sera utilisée localement uniquement et jamais transférée.
VI. En-tête du datagramme▲
Voyons maintenant comment utiliser cet en-tête afin d’établir notre protocole.
VI-A. Lors de la réception d’un datagramme▲
Lors de la réception du datagramme, nous pouvons extraire les datagrammes acquittés par l’autre machine depuis l’en-tête.
Une fois le gestionnaire d’acquittements mis à jour avec cette donnée, il devient possible de savoir quels datagrammes ont été récemment reçus ou si certains sont définitivement perdus. Cette information est la fondation du protocole.
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.
void
DistantClient::
onDatagramReceived(Datagram&&
datagram)
{
…
//!
< Mise à jour du suivi des datagrammes envoyés et acquittés par l’autre partie
mSentAcks.update(ntohs(datagram.header.ack), datagram.header.previousAcks, true
);
…
//!
< Traiter les pertes à la réception
const
auto
lostDatagrams =
mReceivedAcks.loss();
for
(const
auto
receivedLostDatagram : lostDatagrams)
{
onDatagramReceivedLost(receivedLostDatagram);
}
//!
< Gérer les données envoyées et non reçues
const
auto
datagramsSentLost =
mSentAcks.loss();
for
(const
auto
sendLoss : datagramsSentLost)
{
onDatagramSentLost(sendLoss);
}
//!
< Traiter les données envoyées et acquittées
const
auto
datagramsSentAcked =
mSentAcks.getNewAcks();
for
(const
auto
sendAcked : datagramsSentAcked)
{
onDatagramSentAcked(sendAcked);
}
…
}
Les implémentations de onDatagramReceivedLost, onDatagramSentLost et onDatagramSentAcked seront laissées vides pour le moment.
VI-B. Lors de l’envoi d’un datagramme▲
L’envoi sera bien plus simple : il suffit de remplir l’en-tête du datagramme avec les données locales pour informer la machine distante de ce que nous avons reçu.
2.
3.
4.
5.
6.
7.
void
DistantClient::
send(std::
vector<
uint8_t>&&
data)
{
…
datagram.header.ack =
htons(mReceivedAcks.lastAck());
datagram.header.previousAcks =
mReceivedAcks.previousAcksMask();
…
}
N’oubliez pas de convertir l’acquittement le plus récent en endianness réseau. Le masque ne doit quant à lui pas être converti.
VII. Tests▲
Comme pour le gestionnaire d’acquittements et chaque élément que nous rajouterons par la suite, le client distant n’a de valeur et ne peut être décrété fonctionnel qu’après avoir passé avec succès quelques tests.
Écrivons donc ces tests.
Le test du client distant est similaire à celui du gestionnaire d’acquittements : un datagramme identique ou trop ancien doit être rejeté. Il s’agira donc d’écrire quelques scénarios afin de vérifier tout ça.
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.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
#pragma once
#include
"Tester.hpp"
#include
"UDP/DistantClient.hpp"
#include
"UDP/UDPClient.hpp"
#include
"Messages.hpp"
class
DistantClient_Test
{
public
:
static
void
Test();
}
;
void
DistantClient_Test::
Test()
{
static
constexpr
uint64_t MASK_COMPLETE =
std::
numeric_limits<
uint64_t>
::
max();
static
constexpr
uint64_t MASK_FIRST_ACKED =
Bousk::Utils::
Bit<
uint64_t>
::
Right;
static
constexpr
uint64_t MASK_FIRST_AND_SECOND_ACKED =
(MASK_FIRST_ACKED <<
1
) |
Bousk::Utils::
Bit<
uint64_t>
::
Right;
static
constexpr
uint64_t MASK_FIRST_MISSING =
~
MASK_FIRST_ACKED;
static
constexpr
uint64_t MASK_LAST_ACKED =
(MASK_FIRST_ACKED <<
63
);
Bousk::Network::UDP::
Client client; //!
< Un client est nécessaire pour créer un DistantClient
sockaddr_in localAddress;
localAddress.sin_addr.s_addr =
htonl(INADDR_LOOPBACK); //!
< Disons que notre client distant a une adresse de loopback (n’importe quelle adresse ferait l’affaire ici)
localAddress.sin_family =
AF_INET;
localAddress.sin_port =
htons(8888
);
Bousk::Network::UDP::
DistantClient distantClient(client, reinterpret_cast
<
const
sockaddr_storage&>
(localAddress));
CHECK(distantClient.mNextDatagramIdToSend ==
0
);
CHECK(distantClient.mReceivedAcks.lastAck() ==
std::
numeric_limits<
uint16_t>
::
max());
constexpr
char
*
TestString =
"Test data"
;
constexpr
size_t TestStringLength =
sizeof
(TestString);
distantClient.send(std::
vector<
uint8_t>
(TestString, TestString +
TestStringLength));
CHECK(distantClient.mNextDatagramIdToSend ==
1
);
//!
< Créons un datagramme pour vérifier sa réception
Bousk::Network::UDP::
Datagram datagram;
datagram.header.id =
0
;
memcpy(datagram.data.data(), TestString, TestStringLength);
datagram.datasize =
TestStringLength;
//!
< Si un datagramme est accepté, il créera un Message UserData chez le Client
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
0
);
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_COMPLETE);
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
1
);
const
auto
&
msg =
polledMessages[0
];
CHECK(msg->
is<
Bousk::Network::Messages::
UserData>
());
const
auto
dataMsg =
msg->
as<
Bousk::Network::Messages::
UserData>
();
CHECK(dataMsg->
data.size() ==
TestStringLength);
CHECK(memcmp(TestString, dataMsg->
data.data(), TestStringLength) ==
0
);
}
//!
< Le datagramme #0 est reçu en double : il doit être ignoré
datagram.header.id =
0
;
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
0
);
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_COMPLETE);
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
0
);
}
//!
< Envoi du datagramme #2, le #1 est maintenant manquant
datagram.header.id =
htons(2
);
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
2
);
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_FIRST_MISSING);
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
1
);
const
auto
&
msg =
polledMessages[0
];
CHECK(msg->
is<
Bousk::Network::Messages::
UserData>
());
const
auto
dataMsg =
msg->
as<
Bousk::Network::Messages::
UserData>
();
CHECK(dataMsg->
data.size() ==
TestStringLength);
CHECK(memcmp(TestString, dataMsg->
data.data(), TestStringLength) ==
0
);
}
//!
< Réception du datagramme #1 désordonné
datagram.header.id =
htons(1
);
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
2
);
CHECK(distantClient.mReceivedAcks.isNewlyAcked(1
));
CHECK(!
distantClient.mReceivedAcks.isNewlyAcked(2
));
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_COMPLETE);
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
1
);
const
auto
&
msg =
polledMessages[0
];
CHECK(msg->
is<
Bousk::Network::Messages::
UserData>
());
const
auto
dataMsg =
msg->
as<
Bousk::Network::Messages::
UserData>
();
CHECK(dataMsg->
data.size() ==
TestStringLength);
CHECK(memcmp(TestString, dataMsg->
data.data(), TestStringLength) ==
0
);
}
//!
< Saut de 64 datagrammes, tous les intermédiaires sont manquants
datagram.header.id =
htons(66
);
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
66
);
CHECK(distantClient.mReceivedAcks.isNewlyAcked(66
));
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_LAST_ACKED);
CHECK(distantClient.mReceivedAcks.loss().empty());
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
1
);
const
auto
&
msg =
polledMessages[0
];
CHECK(msg->
is<
Bousk::Network::Messages::
UserData>
());
const
auto
dataMsg =
msg->
as<
Bousk::Network::Messages::
UserData>
();
CHECK(dataMsg->
data.size() ==
TestStringLength);
CHECK(memcmp(TestString, dataMsg->
data.data(), TestStringLength) ==
0
);
}
//!
< Réception du datagramme suivant
datagram.header.id =
htons(67
);
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
67
);
CHECK(distantClient.mReceivedAcks.isNewlyAcked(67
));
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_FIRST_ACKED);
CHECK(distantClient.mReceivedAcks.loss().empty());
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
1
);
const
auto
&
msg =
polledMessages[0
];
CHECK(msg->
is<
Bousk::Network::Messages::
UserData>
());
const
auto
dataMsg =
msg->
as<
Bousk::Network::Messages::
UserData>
();
CHECK(dataMsg->
data.size() ==
TestStringLength);
CHECK(memcmp(TestString, dataMsg->
data.data(), TestStringLength) ==
0
);
}
//!
< Réception du suivant, le datagramme #3 est maintenant perdu
datagram.header.id =
htons(68
);
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
68
);
CHECK(distantClient.mReceivedAcks.isNewlyAcked(68
));
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_FIRST_AND_SECOND_ACKED);
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
1
);
const
auto
&
msg =
polledMessages[0
];
CHECK(msg->
is<
Bousk::Network::Messages::
UserData>
());
const
auto
dataMsg =
msg->
as<
Bousk::Network::Messages::
UserData>
();
CHECK(dataMsg->
data.size() ==
TestStringLength);
CHECK(memcmp(TestString, dataMsg->
data.data(), TestStringLength) ==
0
);
}
//!
< Réception du datagramme #3 : trop ancien, ignoré
datagram.header.id =
htons(3
);
{
distantClient.onDatagramReceived(std::
move(datagram));
CHECK(distantClient.mReceivedAcks.lastAck() ==
68
);
CHECK(!
distantClient.mReceivedAcks.isNewlyAcked(68
));
CHECK(distantClient.mReceivedAcks.previousAcksMask() ==
MASK_FIRST_AND_SECOND_ACKED);
auto
polledMessages =
client.poll();
CHECK(polledMessages.size() ==
0
);
}
}
Encore une fois, ajoutez autant de tests que vous le souhaitez jusqu’à être convaincu que le client distant est fonctionnel.
Récupérez le code source sous le tag V1.0.4 dans le dépôt Mercurial à l’adresse suivante.