Imprimer
Catégorie : Programmation avancée
Affichages : 398

1°/ Principe du jeu

Illustrations :

diamond-1 diamond-2 diamond-3
le plateau vide bleu perd : 12, rouge gagne : 11 bleu gagne : 5, rouge perd : 6

 

2°/ Le diamond en réseau

Ce TP permet d'utiliser les connaissances sur les mutex et attente d'événements, dans le cadre d'un serveur multi-threadé, mais non basé sur des requêtes. Dans ce contexte, la structure du serveur principal est relativement identique puisqu'il s'agit d'attendre des connexions et de créer des threads. En revanche, le code des threads est organisé de façon très différente, puisqu'il n'y a pas de notion de requête.

2.1°/ Fonctionnement général

 

Les principes de fonctionnement généraux du jeu sont les suivants :

Ces contraintes permettent de déduire les éléments de programmation suivants :

 

2.2°/ Protocole de communication

Comme l'application est un jeu à tour de rôle, le protocole de communication n'est pas orienté requête. C'est plutôt le thread côté serveur qui "prévient" les clients de ce qu'ils doivent faire :

 

2.3°/ Problématiques d'implémentation

L'état de la partie est partagé entre les threads et doit contenir toutes les méthodes permettant de manipuler cet état de façon "thread-safe", c'est-à-dire sans que des threads entrent en conflit. Pour cela, il suffit de créer une classe Party, avec toutes les informations nécessaires au déroulement de la partie (pseudo des joueurs, liste des pions à jouer pour chacun, le plateau de jeu, sémaphores, ...). La plupart des méthodes sont relativement simples à écrire, et dans un cas aussi simple que ce jeu, elles peuvent être simplement déclarées synchronized pour utiliser le mutex de l'objet Party. Il y a cependant une exception avec les méthodes qui manipulent les sémaphores, qui ne doivent surtout pas être synchronized (cf. explications dans les sources à télécharger).

Les problèmes principaux de ce genre de jeu sont plutôt dans l'écriture du code du serveur principal et des threads, notamment pour la gestion :

Pour le jeu "diamond", une partie se joue uniquement à 2 joueurs. Pour autant, il ne suffit pas d'attendre 2 connexions clientes et créer 2 threads pour commencer une partie, par exemple :

party = new Party();
...
while(true) {
  for(int i=0;i<2;i++) {
    sockComm = waitClient.accept();
    ThreadServer t = new ThreadServer(sockComm, party, ...);
    t.start();
  }
  party.waitParyEnd(); // attendre fin de partie et donc fin des 2 threads
} 

 

En effet, les joueurs doivent d'abord envoyer un pseudo valide avant d'accéder potentiellement à une partie. Cet envoi de pseudo ne peut évidemment pas être géré au niveau du serveur principal, sinon ce dernier serait bloqué dès qu'un joueur met du temps à choisir un pseudo. Il faut donc que ce soient les threads qui gèrent l'échange de pseudo.

Le problème avec l'exemple de code ci-dessus est qu'un client peut se déconnecter durant l'échange de pseudo. Le thread va détecter cette déconnexion et s'arrêter. Il faudrait donc que le serveur principal puisse accepter un nouveau client, ce qui n'est pas possible puisqu'il fait une boucle à 2 itérations !

Une solution simple et générique à ce problème est de toujours autoriser la connexion, mais à chaque étape principale de l'exécution, de prévenir le client s'il peut continuer, ou bien s'il doit s'arrêter parce qu'une partie est déjà en cours. Cependant, cela implique d'en tenir compte dans le protocole de communication entre le client et le serveur. C'est ce qui est fait dans celui de la section 2.2, puisque le premier envoi consiste à dire au client nouvellement connecté si une partie est déjà en cours, ainsi qu'après l'envoi d'un pseudo valide s'il peut effectivement être enregistré comme joueur. Cette solution simplifie également le serveur principal car celui-ci n'a jamais besoin d'attendre la fin de partie, comme dans le code bancal ci-dessus.

Remarque : une solution bien plus complexe consiste à mettre en place une file d'attente d'accès à une partie. C'est notamment utile lorsque l'on veut garder le client en attente de l'accès à une partie et le prévenir dès que c'est possible.

 

Gérer les déconnexions brutales est souvent encore plus complexe. En effet, il faut obligatoirement qu'une partie se termine proprement afin d'être réinitialisée. Cela implique de mettre une barrière de synchronisation juste à la fin de la méthode run() et de s'assurer que TOUS les threads passent par cette barrière. Le premier à passer la barrière indique que la partie doit se terminer, et le dernier réinitialise l'état de la partie. Le premier problème est donc de structurer le code pour qu'un thread puisse atteindre directement cette barrière en cas de déconnexion de son client. Ce n'est pas forcément évident d'obtenir une telle structuration, surtout quand il faut jongler avec les cas d'exception.

Malheureusement, ce n'est pas suffisant. En effet, il est fort possible qu'un thread atteigne cette barrière alors que d'autres sont bloqués dans une autre barrière, ce qui produit un interblocage. Voici un tel cas pour le "diamond" :

Une solution simple consiste à inclure dans la barrière de fin de partie des instructions qui permettent de débloquer des threads qui seraient en attente dans d'autres barrières. Ce n'est malheureusement pas totalement satisfaisant dans certains jeux complexes car les threads ainsi débloqués peuvent ensuite être en attente de données venant du client. Et si ce dernier n'envoie rien, alors il est impossible de "forcer" la fin de partie. Ce deuxième problème se résoud en ajoutant dans le code des vérifications de l'état de la partie avant chaque réception (NB : les envois n'étant pas bloquant, pas besoin d'une telle précaution). Si la partie doit se terminer, alors pas besoin de faire la réception et le thread saute à la barrière de fin de partie. C'est une solution "lourde" mais efficace.

En conclusion, on voit que cette gestion des déconnexion pose de fortes contraintes dans l'écriture du code des threads et même dans celle des objets partagés.

 

3°/ Exercice

L'objectif est simple : implémenter un client et un serveur afin de jouer au diamond, selon les spécifications données ci-dessus. 

Pour vous faciliter la tâche, vous avez accès à une archive comportant des sources à compléter pour le [ client ] et le [ serveur ]. Les parties à compléter sont indiquées par un commentaire /* A COMPLETER ...*/ dans lequel les étapes nécessaires sont indiquées. Bien entendu, vous devrez lire et comprendre les autres classes fournies pour compléter correctement les codes. 

Pour information, les classes à compléter sont :

 

Commentaires additionnels :

diamond-4