Cours programmation réseau en C++

UDP – Déboguer une application réseau

Bousk

Maintenant que nous avons une première utilisation de notre moteur, nous nous heurtons à un problème : comment déboguer des applications en réseau, alors même qu’utiliser le débogueur ou un point d’arrêt déclenche une déconnexion ?

Commentez

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

- Auteur : Bousk

- Parution : 24 août 2021

- 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 ® 2021 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.

Voir également :

- UDP – Gérer des connexions entre machines

- Un premier jeu : Morpion / Tic-Tac-Toe

Sommaire

- I. Problèmes pour déboguer des applications réseau

- I-A. Comment y remédier?

- II. Politique d’interruption

- II-A. Interruption directe

- II-B. Interruption indirecte

- II-C. Une interruption unidirectionnelle

- II-D. Reprise de la connexion

- II-E. Se débarrasser d’un client

- III. Implémentation

- III-A. Une fonctionnalité désactivable

- III-B. Messages

- III-C. Mise à jour du code

- III-C-1. UDP Client

- III-C-1-a. Interface

- III-C-1-b. Implémentations

- III-C-2. Client distant

- III-C-2-a. Interface

- III-C-2-b. Implémentations

- III-C-2-b-i. Interruption directe

- III-C-2-b-ii. Interruption indirecte

- III-C-2-b-iii. Reprise de la connexion

- III-D. Kick

- IV. Tests

- IV-A. Interruption et reprise

- IV-B. Interruption et kick

I - Problèmes pour déboguer des applications réseau

Déboguer une application est primordial pour assurer sa stabilité et son bon fonctionnement.

Il y a cependant un problème majeur lorsque l’application a une composante réseau, en particulier si elle utilise un système de connexion via UDP : quand une application rencontre un point d’arrêt et que le débogueur s’arrête, les mécanismes de perte de connexion sont rapidement déclenchés. Le TCP est moins contraignant puisque le protocole prendra généralement plus de temps à détecter ceci comme une déconnexion.

Ceci peut complexifier énormément la tâche, puisqu’une déconnexion entraîne souvent la perte de la session et donc de l’état du programme.

I-A - Comment y remédier?

Afin de permettre un travail plus efficace, il est intéressant de modifier le mécanisme de déconnexion pour ne pas déclencher une déconnexion, mais seulement générer une interruption de connexion. À l’inverse d’une déconnexion, une connexion juste interrompue pourrait reprendre.

Ce mécanisme ne sera généralement activé que dans un environnement de développement et non dans l’application publique finale où l’on préférera laisser les déconnexions en cas d’interruption du flux de données, quelle que soit la cause réelle, afin de limiter les frictions envers les joueurs restants.

L’application sera ensuite libre de gérer cette interruption de connexion comme elle le souhaite. Dans la version de développement, il s’agira de mettre en pause la mise à jour des éléments du jeu permettant ainsi de vérifier ce que l’on veut et comprendre pourquoi un bug survient ou pour quelle raison le jeu ou une partie de celui-ci est tombé dans un état particulier.

II - Politique d’interruption

L’interruption de la connexion sera matérialisée par un message généré par le moteur réseau pour l’application.

À la réception de ce message, l’application peut choisir comment réagir et déclencher les mécanismes d’interruption qu’elle souhaite. Dans un jeu, on pourra décider de déclencher une pause et arrêter de mettre à jour les unités et éléments gameplay afin de garder l’état actuel en mémoire et pouvoir effectuer les vérifications de son choix. Ou tout simplement pour ne pas continuer de faire évoluer la partie alors qu’une machine de la session ne répond plus, par exemple parce que celle-ci vient de rencontrer un point d’arrêt pour étudier un bug. Une fois tout en ordre, et si cela est possible, alors la machine interrompue reprend son exécution, cette reprise est détectée par l’ensemble des machines et la session peut continuer son cours.

Deux types d’interruption sont possibles : une interruption directement constatée avec un client ou une interruption reportée par un client avec un de ses propres clients.

II-A - Interruption directe

L’interruption directe sera celle constatée directement vis-à-vis du client perdu : plus aucune donnée n’est reçue de ce client depuis suffisamment longtemps pour le considérer comme interrompu.

II-B - Interruption indirecte

L’interruption indirecte sera une interruption reçue d’un client non interrompu, mais dont l’un de ses clients est interrompu directement. C’est une interruption qui est transmise à ses clients et qui est donc reçue et non constatée :

Ou indirectement selon la complexité du réseau :

II-C - Une interruption unidirectionnelle

Notez que l’interruption n’est pas bidirectionnelle : le client qui constate l’interruption continue d’envoyer des données vers le client interrompu.

Ceci afin que la connexion puisse reprendre dès que possible et pour éviter un deadlock le cas échéant :

- A arrête ses envois vers B ;

- B déclare A interrompu ;

- si B arrête ses envois vers A, alors quand A reprend, il déclare B interrompu.

De la même façon, B ne doit pas non plus transmettre une interruption à A si seul A est interrompu.

II-D - Reprise de la connexion

Si l’interruption est constatée par un arrêt de réception des données ou par une interruption transmise par un client, alors la reprise sera effective quand des données recommenceront à être reçues ou quand une reprise de connexion sera indirectement reçue.

Si plusieurs interruptions sont constatées, alors la reprise complète du moteur réseau survient quand toutes les connexions ont repris.

II-E - Se débarrasser d’un client

Il n’est parfois pas possible qu’un client puisse rétablir sa connexion. Un client peut avoir totalement crashé, ou l’application a été fermée et n’existe plus.

On peut aussi vouloir continuer sans attendre le client et simplement vouloir terminer notre connexion avec lui.

Dans ce cas, il est nécessaire de se débarrasser du client de force avec un système de « kick ».

III - Implémentation

Il s’agit essentiellement de modifier le comportement des déconnexions et du maintien de connexion. Nous nous appuierons donc sur les implémentations du chapitre 8 : gestion de connexions.

III-A - Une fonctionnalité désactivable

Cette fonctionnalité sera désactivable grâce au préprocesseur afin de pouvoir la retirer complètement de l’exécutable final.

En effet, bien que très utile pendant la phase de développement, elle pourrait rapidement devenir ennuyeuse pour les utilisateurs finaux.

Ainsi, tout le code spécifique à cette fonctionnalité sera protégé par des instructions préprocesseurs.

La syntaxe utilisée reprendra celle existante dans le fichier Settings.hpp et elle sera désactivée par défaut :

Settings.hpp

#ifndef BOUSKNET_ALLOW_NETWORK_INTERRUPTION
#define BOUSKNET_ALLOW_NETWORK_INTERRUPTION BOUSKNET_SETTINGS_DISABLED
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION

III-B - Messages

Commençons par ajouter les nouveaux messages pour supporter ce mécanisme.

Messages.hpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
class ConnectionInterrupted : public Base
{
DECLARE_MESSAGE(ConnectionInterrupted);
public:
ConnectionInterrupted(const Address& emitter, uint64 emitterid, bool isDirect)
: Base(Type::ConnectionInterrupted, emitter, emitterid)
, isDirectInterruption(isDirect)
{}
// True si l’émetteur est en interruption directe. False si l’émetteur transmet l’interruption.
bool isDirectInterruption;
};
class ConnectionResumed : public Base
{
DECLARE_MESSAGE(ConnectionResumed);
public:
ConnectionResumed(const Address& emitter, uint64 emitterid, bool networkResume)
: Base(Type::ConnectionResumed, emitter, emitterid)
, isNetworkResumed(networkResume)
{}
// True si le réseau n’est plus interrompu. False si le réseau est encore interrompu après la reprise de cet émetteur.
bool isNetworkResumed;
};
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

III-C - Mise à jour du code

III-C-1 - UDP Client

Commençons par mettre à jour le client UDP. Ce dernier devra suivre les interruptions en cours et permettre d’informer si la connexion est actuellement interrompue ou non, et, en cas d’interruption, de dire si le client est le seul coupable ou non de l’interruption.

III-C-1-a - Interface

Voici l’interface qui permettra de modifier l’état de l’interruption et de retrouver les informations qui nous intéressent :

UDPClient.hpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
// Appelé quand un client devient interrompu.
void onClientInterrupted(const DistantClient* client);
// Appelé quand un client récupère sa connectivité.
void onClientResumed(const DistantClient* client);
// Retourne true si le client en paramètre est le seul responsable de l’interruption. False sinon.
bool isInterruptionCulprit(const DistantClient* client) const;
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

Quelques variables supplémentaires sont nécessaires pour y parvenir :

UDPClient.hpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
// Permet d’activer ou non les interruptions.
bool mNetworkInterruptionAllowed{ true };
// Liste des clients actuellement interrompus.
std::unordered_set<const DistantClient*> mInterruptedClients;
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

Puis quelques fonctions pour contrôler cette fonctionnalité :

UDPClient.hpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
inline void enableNetworkInterruption() { setNetworkInterruptionEnabled(true); }
inline void disableNetworkInterruption() { setNetworkInterruptionEnabled(false); }
inline void setNetworkInterruptionEnabled(bool enabled) { mNetworkInterruptionAllowed = enabled; }
inline bool isNetworkInterruptionAllowed() const { return mNetworkInterruptionAllowed; }
inline bool isNetworkInterrupted() const { return !mInterruptedClients.empty(); }
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

III-C-1-b - Implémentations

Les implémentations de ces fonctions sont assez simples et directes :

UDPClient.cpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
void Client::onClientInterrupted(const DistantClient* client)
{
mInterruptedClients.insert(client);
}
void Client::onClientResumed(const DistantClient* client)
{
mInterruptedClients.erase(client);
}
bool Client::isInterruptionCulprit(const DistantClient* client) const
{
return mInterruptedClients.size() == 1 && *(mInterruptedClients.begin()) == client;
}
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

III-C-2 - Client distant

III-C-2-a - Interface

Puis mettons à jour la détection de la déconnexion vers une détection d’interruption dans le client distant.

Définissons d’abord l’interface que nous voulons utiliser :

DistantClient.hpp

// Appelé quand le client est directement interrompu.
void onConnectionInterrupted();
// Appelé quand ce client transmet une interruption.
void onConnectionInterruptedForwarded();
void onConnectionResumed();

Et des variables membres pour supporter cette fonctionnalité :

DistantClient.hpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
bool mInterrupted{ false }; // Si la connectivité avec ce client est interrompue (ce client a cessé de nous envoyer des données)
bool mDistantInterrupted{ false }; // Si ce client a sa connectivité interrompue avec d’autres clients (et nous a transmis cette interruption)
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

Ainsi que des accesseurs pour ces variables :

DistantClient.hpp

#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
inline bool isInterrupted() const { return mInterrupted; }
inline bool hasInterruptedClients() const { return mDistantInterrupted; }
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

III-C-2-b - Implémentations

III-C-2-b-i - Interruption directe

Puisque l’objectif est de modifier une perte de connexion en connexion interrompue, il est assez aisé de trouver où se brancher dans le code pour cela.

Trouvons où onConnectionLost() est appelé pour le modifier par onConnectionInterrupted(). Cette fonction devra déclencher l’interruption directe si cette fonctionnalité est activée, sinon une déconnexion :

DistantClient.cpp

void DistantClient::onConnectionInterrupted()
{
#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
if (mClient.isNetworkInterruptionAllowed())
{
if (!mInterrupted)
{
mInterrupted = true;
mClient.onClientInterrupted(this);
onMessageReady(std::make_unique<Messages::ConnectionInterrupted>(mAddress, mClientId, true));
}
}
else
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
{
if (isConnected())
{
onConnectionLost();
}
}
}

III-C-2-b-ii - Interruption indirecte

Maintenant, prenons en compte les interruptions transmises.

Si elles sont reçues, elles doivent l’être via un datagramme ou paquet.

Nous allons donc ajouter cette information dans le datagramme de keep-alive. Ce keep-alive nous informe généralement que la connexion est correcte. Ici, nous devons modifier son traitement pour tenir compte du fait qu’elle peut nous transmettre une interruption :

DistantClient.cpp

void DistantClient::maintainConnection(bool distantNetworkInterrupted /*= false*/)
{
mLastKeepAlive = Utils::Now();
if (distantNetworkInterrupted)
onConnectionInterruptedForwarded();
else
onConnectionResumed();
}

L’implémentation de onConnectionResumed sera détaillée juste après, et onConnectionInterruptedForwarded sera comme ceci :

DistantClient.cpp

void DistantClient::onConnectionInterruptedForwarded()
{
#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
if (mClient.isNetworkInterruptionAllowed())
{
if (!mDistantInterrupted)
{
// If we were interrupted, we must have received something to know the distant interruption
if (mInterrupted)
{
mInterrupted = false;
onMessageReady(std::make_unique<Messages::ConnectionResumed>(mAddress, mClientId, false));
}
mDistantInterrupted = true;
mClient.onClientInterrupted(this);
onMessageReady(std::make_unique<Messages::ConnectionInterrupted>(mAddress, mClientId, false));
}
}
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
}

Puis nous modifions la gestion du keep-alive pour prendre en compte ce nouveau mécanisme :

DistantClient.cpp

void DistantClient::handleKeepAlive(const uint8* data, const uint16 datasize)
{
...

bool isNetworkInterruptedOnTheOtherEnd = false;
#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
// Récupérer l’état de la connectivité du client distant
deserializer.read(isNetworkInterruptedOnTheOtherEnd);
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
// Si nous avons reçu un keep-alive, alors la connexion est maintenue. Mais elle peut être interrompue indirectement.
maintainConnection(isNetworkInterruptedOnTheOtherEnd);
}

III-C-2-b-iii - Reprise de la connexion

Maintenant que l’interruption est gérée, il faut gérer le mécanisme inverse : la reprise de la connexion.

Commençons par la fonction de plus haut niveau que nous avons déjà rencontrée dans la partie précédente et qui génère le message de reprise pour l’application :

DistantClient.cpp

void DistantClient::onConnectionResumed()
{
#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
if (mInterrupted || mDistantInterrupted)
{
mInterrupted = false;
mDistantInterrupted = false;
mClient.onClientResumed(this);
onMessageReady(std::make_unique<Messages::ConnectionResumed>(mAddress, mClientId, !mClient.isNetworkInterrupted()));
}
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
}

Afin de ne pas bloquer l’application, nous pouvons également considérer qu’une connexion sur le point de disparaître (un client qui va se déconnecter) reprend juste avant :

DistantClient.cpp

void DistantClient::processSend(const uint8 maxDatagrams /*= 0*/)
{
...
if (isDisconnecting())
{
if (now > mLastKeepAlive + 2 * GetTimeout())
{
// Après 2 timeout, considérer ce client déconnecté
// Ceci laisse assez de temps pour que chaque machine remarque la déconnexion de son client distant
#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
// Reprendre la connexion juste avant sa déconnexion afin de générer un message de reprise pour l’application
if (isInterrupted())
onConnectionResumed();
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED

Et bien sûr, il faut modifier le keep-alive afin d’intégrer ce nouveau mécanisme dans son envoi également :

DistantClient.cpp

void DistantClient::fillKeepAlive(Datagram& dgram)
{
fillDatagramHeader(dgram, Datagram::Type::KeepAlive);
// Notifier la machine distante si nous devons être connectés ou en demande de connexion
Serialization::Serializer serializer;
serializer.write(mState == State::ConnectionSent || isConnected());
#if BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
// Si la connexion est interrompue, vérifier si ce client est l’unique responsable
// Si oui, alors ne pas envoyer d’interruption pour que la machine distante puisse reprendre
const bool isNetworkInterrupted = mClient.isNetworkInterrupted();
const bool isNetworkInterruptedByMe = mClient.isInterruptionCulprit(this);
serializer.write(isNetworkInterrupted && !isNetworkInterruptedByMe);
#endif // BOUSKNET_ALLOW_NETWORK_INTERRUPTION == BOUSKNET_SETTINGS_ENABLED
memcpy(dgram.data.data(), serializer.buffer(), serializer.bufferSize());
dgram.datasize = static_cast<uint16>(serializer.bufferSize());
}

III-D - Kick

Pour se débarrasser d’un client, nous n’avons pas besoin d’ajouter un mécanisme de kick particulier : nous pouvons simplement utiliser l’interface disconnect du client UDP.

IV - Tests

Enfin, des tests sont écrits pour s’assurer que tout ceci fonctionne comme attendu.

IV-A - Interruption et reprise

Le premier test vérifiera que les mécanismes d’interruption et de reprise sont fonctionnels.

Pour ceci, nous créons et connectons deux clients, puis l’un des clients cesse d’envoyer des données afin d’être détecté comme interrompu par l’autre client.

Enfin, l’envoi de données reprend, et la connexion doit être notifiée comme reprise :

ConnectionInterruption_Test.cpp

void ConnectionInterruption_Test::TestInterruptionAndResume()
{
const Bousk::Network::Address client1 = Bousk::Network::Address::Loopback(Bousk::Network::Address::Type::IPv4, 8888);
const Bousk::Network::Address client2 = Bousk::Network::Address::Loopback(Bousk::Network::Address::Type::IPv4, 9999);

const std::vector<std::string> messagesToSend = { "my", "first", "udp", "transfert" };
std::mutex coutMutex;
// Create a thread per client
std::thread t1([&]()
{
Bousk::Network::UDP::Client client;
client.registerChannel<Bousk::Network::UDP::Protocols::ReliableOrdered>();
client.enableNetworkInterruption();
if (!client.init(client1.port()))
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 initialisation error : " << Bousk::Network::Errors::Get();
return;
}
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 initialized on port " << client1.port() << std::endl;
}
// Connect client 1 to client 2
client.connect(client2);
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 connecting to " << client2.toString() << "..." << std::endl;
}
std::vector<std::string> receivedMessages;
for (bool exit = false; !exit;)
{
client.receive();
auto messages = client.poll();
for (auto&& message : messages)
{
if (message->is<Bousk::Network::Messages::Connection>())
{
if (message->emitter() != client2)
{
std::scoped_lock lock(coutMutex);
std::cout << "Unexpected connection from " << message->emitter().toString() << " (should be from " << client2.toString() << ")" << std::endl;
continue;
}
else
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 [" << client2.toString() << "] connected to client 1" << std::endl;
std::cout << "Sleeping 10s to trigger interruption..." << std::endl;
}
std::this_thread::sleep_for(std::chrono::seconds(10));
}
else if (message->is<Bousk::Network::Messages::UserData>())
{
const Bousk::Network::Messages::UserData* userdata = message->as<Bousk::Network::Messages::UserData>();
Bousk::Serialization::Deserializer deserializer(userdata->data.data(), userdata->data.size());
std::string msg;
if (!deserializer.read(msg))
{
std::cout << "Error deserializing msg !" << std::endl;
return;
}
receivedMessages.push_back(msg);
if (receivedMessages == messagesToSend)
{
std::scoped_lock lock(coutMutex);
std::cout << "Every messages received in order !" << std::endl;
std::cout << "Disconnecting client 2..." << std::endl;
client.disconnect(client2);
}
}
else if (message->is<Bousk::Network::Messages::Disconnection>())
{
assert(message->emitter() == client2);
std::cout << "Shutting down client 1..." << std::endl;
exit = true;
}
}
client.processSend();
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
std::cout << "[Client 1]Normal termination." << std::endl;
client.release();
});
std::thread t2([&]()
{
Bousk::Network::UDP::Client client;
client.registerChannel<Bousk::Network::UDP::Protocols::ReliableOrdered>();
client.enableNetworkInterruption();
if (!client.init(client2.port()))
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 initialisation error : " << Bousk::Network::Errors::Get();
return;
}
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 initialized on port " << client2.port() << std::endl;
}
for (bool connected = false, exit = false; !exit;)
{
client.receive();
auto messages = client.poll();
for (auto&& message : messages)
{
if (message->is<Bousk::Network::Messages::IncomingConnection>())
{
if (message->emitter() != client1)
{
std::scoped_lock lock(coutMutex);
std::cout << "Unexpected connection received from " << message->emitter().toString() << " (should be from " << client1.toString() << ")" << std::endl;
client.disconnect(message->emitter());
continue;
}
else
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 receiving incoming connection from [" << message->emitter().toString() << "] (client 1)... and accepting it" << std::endl;
}
client.connect(message->emitter());
}
else if (message->is<Bousk::Network::Messages::Connection>())
{
if (message->emitter() != client1)
{
std::scoped_lock lock(coutMutex);
std::cout << "Unexpected connection from " << message->emitter().toString() << " (should be from " << client1.toString() << ")" << std::endl;
continue;
}
else
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 [" << client1.toString() << "] connected to client 2" << std::endl;
}
// Send messages to client 1, 1 message per packet
for (const auto& msg : messagesToSend)
{
Bousk::Serialization::Serializer serializer;
if (!serializer.write(msg))
{
std::scoped_lock lock(coutMutex);
std::cout << "Error serializing msg \"" << msg << "\" !" << std::endl;
return;
}
std::vector<Bousk::uint8> buffer(serializer.buffer(), serializer.buffer() + serializer.bufferSize());
client.sendTo(client1, std::move(buffer), 0);
}
connected = true;
}
else if (connected)
{
// Wait for client 1 to disconnect
if (message->is<Bousk::Network::Messages::ConnectionInterrupted>())
{
const Bousk::Network::Messages::ConnectionInterrupted* msg = message->as<Bousk::Network::Messages::ConnectionInterrupted>();
std::cout << "Connection interrupted with client 1..." << std::endl;
}
else if (message->is<Bousk::Network::Messages::ConnectionResumed>())
{
const Bousk::Network::Messages::ConnectionResumed* msg = message->as<Bousk::Network::Messages::ConnectionResumed>();
std::cout << "Connection resumed " << (msg->isNetworkResumed ? "totally" : "partially") << " with client 1..." << std::endl;
}
else if (message->is<Bousk::Network::Messages::Disconnection>())
{
std::scoped_lock lock(coutMutex);
assert(message->emitter() == client1);
std::cout << "Disconnection from client 1... [" << message->as<Bousk::Network::Messages::Disconnection>()->reason << "]" << std::endl;
exit = true;
}
}
}
client.processSend();
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
std::cout << "[Client 2]Normal termination." << std::endl;
client.release();
});

t1.join();
t2.join();
}

IV-B - Interruption et kick

Un second test vérifiera que quand un client interrompu se fait « kick », alors la connexion reprend normalement pour le client restant :

ConnectionInterruption_Test.cpp

void ConnectionInterruption_Test::TestInterruptionAndKick()
{
const Bousk::Network::Address client1 = Bousk::Network::Address::Loopback(Bousk::Network::Address::Type::IPv4, 8888);
const Bousk::Network::Address client2 = Bousk::Network::Address::Loopback(Bousk::Network::Address::Type::IPv4, 9999);

std::mutex coutMutex;
// Create a thread per client
std::thread t1([&]()
{
Bousk::Network::UDP::Client client;
client.registerChannel<Bousk::Network::UDP::Protocols::ReliableOrdered>();
client.enableNetworkInterruption();
if (!client.init(client1.port()))
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 initialisation error : " << Bousk::Network::Errors::Get();
return;
}
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 initialized on port " << client1.port() << std::endl;
}
// Connect client 1 to client 2
client.connect(client2);
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 connecting to " << client2.toString() << "..." << std::endl;
}
std::vector<std::string> receivedMessages;
for (bool exit = false; !exit;)
{
client.receive();
auto messages = client.poll();
for (auto&& message : messages)
{
if (message->is<Bousk::Network::Messages::Connection>())
{
if (message->emitter() != client2)
{
std::scoped_lock lock(coutMutex);
std::cout << "Unexpected connection from " << message->emitter().toString() << " (should be from " << client2.toString() << ")" << std::endl;
continue;
}
else
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 [" << client2.toString() << "] connected to client 1" << std::endl;
std::cout << "Shutting down client 1!" << std::endl;
}
exit = true;
}
}
client.processSend();
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
std::cout << "[Client 1]Normal termination." << std::endl;
client.release();
});
std::thread t2([&]()
{
Bousk::Network::UDP::Client client;
client.registerChannel<Bousk::Network::UDP::Protocols::ReliableOrdered>();
client.enableNetworkInterruption();
if (!client.init(client2.port()))
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 initialisation error : " << Bousk::Network::Errors::Get();
return;
}
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 initialized on port " << client2.port() << std::endl;
}
for (bool connected = false, exit = false; !exit;)
{
client.receive();
auto messages = client.poll();
for (auto&& message : messages)
{
if (message->is<Bousk::Network::Messages::IncomingConnection>())
{
if (message->emitter() != client1)
{
std::scoped_lock lock(coutMutex);
std::cout << "Unexpected connection received from " << message->emitter().toString() << " (should be from " << client1.toString() << ")" << std::endl;
client.disconnect(message->emitter());
continue;
}
else
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 2 receiving incoming connection from [" << message->emitter().toString() << "] (client 1)... and accepting it" << std::endl;
}
client.connect(message->emitter());
}
else if (message->is<Bousk::Network::Messages::Connection>())
{
if (message->emitter() != client1)
{
std::scoped_lock lock(coutMutex);
std::cout << "Unexpected connection from " << message->emitter().toString() << " (should be from " << client1.toString() << ")" << std::endl;
continue;
}
else
{
std::scoped_lock lock(coutMutex);
std::cout << "Client 1 [" << client1.toString() << "] connected to client 2" << std::endl;
}
connected = true;
}
else if (connected)
{
// Wait for client 1 to disconnect
if (message->is<Bousk::Network::Messages::ConnectionInterrupted>())
{
const Bousk::Network::Messages::ConnectionInterrupted* msg = message->as<Bousk::Network::Messages::ConnectionInterrupted>();
std::cout << "Connection interrupted with client 1..." << std::endl;
std::cout << "Disconnecting/Kicking client 1..." << std::endl;
client.disconnect(client1);
}
else if (message->is<Bousk::Network::Messages::ConnectionResumed>())
{
const Bousk::Network::Messages::ConnectionResumed* msg = message->as<Bousk::Network::Messages::ConnectionResumed>();
std::cout << "Connection resumed " << (msg->isNetworkResumed ? "totally" : "partially") << " with client 1..." << std::endl;
}
else if (message->is<Bousk::Network::Messages::Disconnection>())
{
std::scoped_lock lock(coutMutex);
assert(message->emitter() == client1);
std::cout << "Disconnection from client 1... [" << message->as<Bousk::Network::Messages::Disconnection>()->reason << "]" << std::endl;
exit = true;
}
}
}
client.processSend();
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
std::cout << "[Client 2]Normal termination." << std::endl;
client.release();
});

t1.join();
t2.join();
}

Retrouvez le code source dans le dépôt GitHub.

Article précédent

<< Un premier jeu : Morpion