TP Noté 2018 : serveur pour jeu Trivial Poursuite

1. Contexte et consignes

1.1 Introduction

Dans ce projet, nous réalisons un serveur qui gère plusieurs clients pour un jeu de questions du style « Trivial Poursuite ».

Ce serveur :

Ce serveur doit être capable de gérer le départ et l'arrivée de nouveaux clients à tout moment.

Sauf mention contraire explicite dans le sujet, tous les échanges réseaux et usages de fichiers doivent être mis en œuvre avec la bibliothèque JAVA NIO.

La qualité de votre code sera aussi évaluée. Il doit être lisible, indenté, commenté, et conforme aux exigences du fichier Checkstyle fourni pour ce module.

1.2 Consignes

Pour installer le sujet :

Le barème est donné à titre d'indication.

En fin de TP :

  1. si ce n'est pas déjà fait, ajouter au fichier readme.txt les informations à destination des correcteurs ;
  2. verifiez la conformité de vos fichiers aux exigences de Checkstyle :
  3. exportez votre projet Eclipse sous la forme d'une archive :
  4. déposez le fichier sur moodle comme devoir ;
  5. vérifiez avec l'enseignant présent dans la salle que le fichier téléchargé est correct.

2. Présentation du sujet

2.1 Introduction

Objectif : écrire la classe TrivialServer qui gére le jeu et fait le relais entre les clients et le service Web qui génère les informations pour créer les objets de la classe TrivialQuestion.

2.2 Les messages échangés

Les échanges réalisés par le serveur sont :

2.3 Les principales structures de données

Les échanges de données entre le serveur et les clients utilisent deux classes :

  1. TrivialQuestion : cette classe décrit la question du jeu,
  2. AnswersList : cette classe gère les réponses des joueurs.

La classe TrivialQuestion est donnée dans le package json. Ses principaux attributs sont :

La classe AnswersList est donnée dans le package trivial. Ses principaux attributs sont :

Une réponse est stockée dans la classe Answer qui possède deux attributs :

les deux classes échangées entre le client et le serveur
Fig 1 : les deux classes des objets échangés entre le client et le serveur.

2.4 Les états du serveur

Le serveur est structuré autour d'une boucle qui permet de faire passer le serveur dans trois états :


En résumé, le serveur suit le cycle d'états résumé dans la figure suivante :

Les changements d'état du serveur
Fig 2 : diagramme de machine à états.

3. Les 9 questions du sujet

3.1 Introduction

Il y a 9 questions. Les 8 premières qui consistent à écrire 8 méthodes :

La dernière question demande d'ajouter certains services, et donc d'aller modifier quelques unes des méthodes déjà écrites.

Des tests sont fournis pour chaque question. Dans l'archive qui vous est donnée, ils sont désactivés par l'anotation @Ignore. Lorsque vous avez terminé d'écrire une méthode, pour la tester il faut :

La totalité des tests réactivés seront lancés. Ces tests ont été écrits avec deux niveaux de Log :

Les logs des tests sont très verbeux, si vous voulez alléger leur sortie, il suffit de régler le niveau du Logger au début de chaque méthode de test.

Les tests ne fonctionnent que si vous faites les questions dans l'ordre.

3.2 Connexion au serveur HTTPS et lecture des données (4pt)

3.2.1 Connexion au serveur HTTPS (1pt)

Écrivez la méthode connectTo() de la classe https.HttpsProtocol.

Cette méthode doit connecter le canal httpsChan au serveur HTTPS dont l'URL est passé en paramètre. Pour faire la connexion au serveur et la vérification SSL, utilisez la classe java.net.URL.URL.


Voici un exemple d'usage de cette classe :

Inspirez-vous de l'exercice vu au premier TP pour relier un canal à l'InputStream ainsi obtenu.

Pour tester votre code, activez le test de la méthode testConnect() de la classe de test https.TestConnect.

3.2.2 Lecture des données (3pt)

Écrivez la méthode recvData() de la classe https.HttpsProtocol.

Cette méthode doit lire toutes les données (textuelles) envoyées par le serveur HTTPS pour former une chaîne de caractères qui est à retourner en résultat.

Vous devez considérez que le ByteBuffer est possiblement trop petit pour recevoir toutes les données en une seule lecture.

Pour tester votre code, activez deux tests dans la classe de test https.TestRecv :

3.3 Écriture du serveur multiclient (16pt)

3.3.0 Remarques

À partir de cette question, toutes les modifications à faire sont dans la classe TrivialServer. Pour pourvoir tester cette classe, quelques attributs et méthodes ont été ajoutés en fin de classe. Ne les utilisez pas, ne les modifiez pas.

À ce stade du sujet, vous devez avoir fini les questions 3.2.1 et 3.2.2. Si ce n'est pas le cas, et que vous voulez quand même avancer, vous pouvez demander leur solution au surveillant. Mais, dans ce cas, tous leurs points seront perdus.

3.3.1 Création du ServerSocketChannel (2pt)

Écrivez la méthode openListenChannel().

Cette méthode crée un ServerSocketChannel qui est lié au port contenu dans l'attribut port initialisé par le constructeur. Rendez ce canal asynchrone.

La méthode retourne le canal créé.

Pour tester votre code, activez le test de la méthode testOpenListenChannel() de la classe de test trivial.TestOpenListenChannel.

3.3.2 Accepter les nouveaux clients (2pt)

Écrivez la métode acceptNewPlayer().

Cette méthode est appelée quand un nouveau client se connecte sur le ServerSocketChannel listenChannel passé en argument. Vous devez accepter ce client.

Pour communiquer avec ce client, nous utilisons la classe common.FullDuplexMsgWorker écrite en cours. Créez un nouveau worker pour ce client.

La communication avec ce client est faite en mode asynchrone. Elle est orchestrée par le selector de JAVA NIO qui se trouve dans les attributs de la classe. N'intanciez pas ce selector maintenant, ce sera fait dans une autre méthode. Par contre, écrivez toutes les instructions qu'il faut pour que le canal de ce client puisse être géré par l'appel select() de ce selector.

Enfin, utilisez l'attribut private Map<SelectionKey, FullDuplexMsgWorker> theClients pour pouvoir retrouver plus tard le worker du client quand il faudra faire une lecture.

Pour tester votre code, activez le test de la méthode testAcceptNewPlayer() de la classe de test trivial.TestAcceptNewPlayer.

3.3.3 Envoyer une question aux clients (2pt)

Écrivez la métode sendNewQuestion().

Cette méthode est appelée à chaque fois que le serveur passe dans l'état WAITING. Elle utilise les méthodes des questions 2.1 et 2.2 pour interroger le service Web et obtenir une nouvelle question. La réponse est une chaîne de caractères au format JSON qu'il faut analyser pour construire un objet de la classe TrivialQuesion. L'analyse et la construction sont déjà écrites dans des classes du package json. Voici le code pour l'utiliser :

Ce code utilise l'attribut trivialQuestion du serveur.

Envoyez cette question à tous les clients.

Envoyer une nouvelle question, c'est aussi devoir se préparer à recevoir de nouvelles réponses. Créez une nouvelle instance de la answersList du serveur. Initialisez immédiatement sa réponse correcte. Regardez les méthodes getCorrectAnswerNum() de TrivialQuestion et setCorrectAnswer() de AnswersList.

Pour tester votre code, activez le test de la méthode testSendNewQuestion() de la classe de test trivial.TestSendNewQuestion.

3.3.4 Recevoir une réponse d'un client et renvoyer les réponses aux clients (3pt)

Écrivez la métode recieveAnswerFromPlayer() puis la méthode sendCorrectAnswer().

La méthode recieveAnswerFromPlayer() est appélée à chaque fois que le selector détecte que le client a envoyé un message ou s'est déconnecté. Vous devez gérer ces deux évènements.

Son paramètre en entrée est la clef (SelectionKey) du selector associée au SocketChannel relié à ce client.

Si la méthode est appelée pour la reception d'une AnswersList, vous devez extraire le seul élément de cette liste pour l'ajouter à la answersList du serveur. Regardez les méthodes add(), getNickname() et getAnswerNum() de la classe AnswersList.

Pour tester votre code, activez le test de la méthode testReceiveAnswerFromPlayer() de la classe de test trivial.TestReceiveOrSendAnswer.

La méthode sendCorrectAnswer() est appelée à chaque fois que le serveur passe dans l'état PENDING. Elle envoie le contenu de la answersList à tous les clients. Ne détruisez pas cette liste dans cette méthode, car des réponses des clients arrivées en retard peuvent encore l'utiliser. Vous avez dû écrire les instructions qui créent une nouvelle liste (et donc abandonnent celle-ci) dans la méthode sendNewQuestion().

Pour tester votre code, activez le test de la méthode testSendCorrectAnswer() de la classe de test trivial.TestReceiveOrSendAnswer.

3.3.5 Écrire l'automate du serveur (2pt)

Écrivez la méthode changeStateAndSendData().

Cette méthode est appelée à chaque début de tour de la boucle du serveur. Elle gére l'automate du serveur. Autrement dit :

Elle retourne comme résultat la durée du timer du prochain select().

Pour tester votre code, activez le test de la méthode testChangeState() de la classe de test trivial.TestChangeState.

3.3.6 Écrire la boucle du serveur (2pt)

À ce state du développement de votre serveur, tous les tests doivent passer. Donc la commande « mvn clean install »  doit indiquer à la fin des tests le résumé suivant:
« Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 » 

Si c'est le cas, vous pouvez passer à la dernière étape qui consiste à écrire la méthode play().

Cette méthode est appelée par le constructeur du serveur. Elle contient la boucle du serveur.

Elle doit commencer par initialiser toutes les variables qui ont besoin de l'être. Elle continue avec la boucle qui, à chaque tour, doit gérer les évènements attendus avec le selector et changez d'état conformément à la figure 2.

Le serveur est maintenant totalement écrit. Le code du client vous a été donné dans l'archive. Pour tester, il faut lancer le jeu :

Si vous voulez lancer plus de clients, vous pouvez démarrer plusieurs fois le même (plusieurs joueurs auront le même pseudonyme), ou bien ajouter des entrées dans la section du greffon Maven d'exécution dans le fichier pom.xml .

3.3.7 (défi) Rendre le jeu plus fun (3pt)

(1pt) Dans la version actuelle du jeu, le joueur peut attendre longtemps entre sa connexion et la première question qu'il reçoit. Modifier votre code pour qu'il reçoive immédiatement la question en cours. Si le jeu se trouve dans l'état Pending la dernière question n'est plus pertinente, et la nouvelle n'est pas encore connue. Vous pouvez envoyer la question « voidTrivialQuestion ». Cette question contient juste un message demandant au joueur de patienter.

(2pt) Dans la version actuelle du jeu, même si tous les joueurs connectés ont déjà envoyé leur réponse, le serveur attend l'expiration du timer WAITINGTIMER avant d'envoyer la bonne réponse. Modifier votre code pour qu'il passe dans l'état Pending dès qu'il a reçu une réponse de tous les joueurs.