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

Cours programmation réseau en C++

UDP – Déboguer une application réseau

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 ?

Commentez1

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

Image non disponible

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 :

Image non disponible

Ou indirectement selon la complexité du réseau :

Image non disponible

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
Cacher/Afficher le codeSélectionnez
#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
Cacher/Afficher le codeSélectionnez

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
Cacher/Afficher le codeSélectionnez
#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
Cacher/Afficher le codeSélectionnez

Puis quelques fonctions pour contrôler cette fonctionnalité :

UDPClient.hpp
Cacher/Afficher le codeSélectionnez
III-C-1-b. Implémentations

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

UDPClient.cpp
Cacher/Afficher le codeSélectionnez

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
Cacher/Afficher le codeSélectionnez
// 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
Cacher/Afficher le codeSélectionnez

Ainsi que des accesseurs pour ces variables :

DistantClient.hpp
Cacher/Afficher le codeSélectionnez
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
Cacher/Afficher le codeSélectionnez
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
Cacher/Afficher le codeSélectionnez

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

DistantClient.cpp
Cacher/Afficher le codeSélectionnez

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

DistantClient.cpp
Cacher/Afficher le codeSélectionnez
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
Cacher/Afficher le codeSélectionnez

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
Cacher/Afficher le codeSélectionnez

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

DistantClient.cpp
Cacher/Afficher le codeSélectionnez

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
Cacher/Afficher le codeSélectionnez

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
Cacher/Afficher le codeSélectionnez

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

Article précédent

 

<< Un premier jeu : Morpion

 

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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 ni 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.