Dans ce projet, nous réalisons un serveur et client TCP pour la gestion d'un jeu de « Picross » collaboratif.
Un Picross est un jeu où il faut trouver une image pixel par pixel grâce à des indices fournis sur chaque colonne et chaque ligne de l'image. Il n'est pas utile de comprendre les règles de ce jeu pour réaliser ce TP. Vous devez juste savoir que la fenêtre du jeu se divise en trois zones :
Habituellement, ce jeu se joue en solitaire. L'architecture d'un programme avec interface graphique doit alors contenir trois parties :
Le but de ce TP est de rendre ce jeu collaboratif en réunissant sur le réseau plusieurs joueurs sur la même grille. L'architecture du nouveau programme ressemble donc à la figure suivante :
Vous devez écrire les modules client et serveur de ce modèle. Les autres modules sont déjà écrits et fournis.
Tous les échanges réseaux doivent être mis en œuvre avec la bibliothèque JAVA NIO.
La qualité de votre code fait aussi partie de l'évaluation : il doit être lisible, indenté, commenté, et conforme aux exigences SpotBugs (niveau 15) et Checkstyle (fichier fourni pour ce module).
Vous disposer de 3h pour réaliser ce TP. Vous avez le droit aux documents suivants :
JAVA ≥ 8
.Pour installer le sujet :
Le barème est donné à titre d'indication.
En fin de TP :
L'objectif est d'écrire les classes qui implémentent le protocole entre le serveur et les clients TCP. Pour cela, vous devez compléter les quatre classes suivantes :
ConnexionAsynchrone
: elle contient des
méthodes pour gérer une lecture en mode asynchrone (mode du
serveur) ;AppliSwing
et ThreadClient
: elles contiennent les méthodes
du client chargées de la réception et du traitement des
messages venant du serveur ;ServeurPicross
: elle contient les
méthodes pour un serveur asynchrone multiclient réalisant le
protocole pour les échanges nécessaires au jeu.
Une dernière question indépendante consiste à modifier
l'énumération Protocole
.
Tous les messages échangés entre le serveur et les clients sont
des trames de taille fixe
(MessageProtocole.BUFFERSIZE
octets) qui contiennent
exactement quatre entiers. Le premier entier correspond au type du
message, et les trois autres à des paramètres. Il y a 7 types de
messages possibles entre le serveur et les clients. Ils sont
listés dans l'énumération Protocole
:
TAILLE_GRILLE
:
TAILLE_INDICES
:
INDICE_COLONNE
:
INDICE_LIGNE
:
VALEUR_GRILLE_SERVEUR
:
GAGNE
:
VALEUR_GRILLE_CLIENT
:
Les messages du protocole sont construits par la
classe MessageProtocole
. Elle contient deux
constructeurs :
getBuffer()
;getType()
, getVal1()
, getVal2()
,
et getVal3()
.Voici un exemple d'usage pour obtenir un buffer prêt à être envoyé sur le réseau :
Voici un exemple d'usage pour obtenir les informations du message à partir d'un buffer reçu du réseau :
L'action à réaliser à la réception d'un message du protocole est
fixée par l'énumération Protocole
. Son constructeur
associe à chaque énumérateur une instance d'une classe qui
implémente l'interface ProtocoleAction
. Cette
interface demande l'implémentation de la méthode void
realiser(PicrossApi picrossApi, int val1, int val2, int
val3)
. Cette méthode doit contenir l'action à faire lors de
la réception du message de ce type.
Les classes qui doivent recevoir et traiter les messages ont
toutes un attribut (déjà initialisé par le code qui vous est
donné) picrossApi
contenant la référence à passer en
premier paramètre de la méthode realiser()
. Le
traitement d'un message du protocole se fait donc avec l'appel de
la méthode realiser()
avec un code qui doit
ressembler à ce qui suit :
Il y a 8 questions :
ConnexionAsynchrone
(3 pt) ;ThreadClient
(7 pt) ;ServeurPicross
(8 pt) ;Protocole
à modifier (2 pt).Le barème est donné à titre d'indication.
Les 7 premières questions doivent être faites dans l'ordre, mais la dernière question est indépendante et peut être faite à tout moment.
Des tests sont fournis pour les six premières questions. Dans
l'archive qui vous est donnée, ils sont désactivés par
l'annotation @Ignore
. Après l'écriture d'une méthode,
pour la tester il faut :
@Ignore
du test ;mvn clean
install
» ; la totalité des tests réactivés
sont exécutés.
Les tests sont é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.
ConnexionAsynchrone
(3pt)
La classe ConnexionAsynchrone
gère la réception des
messages en mode asynchrone. Pour cela, elle associe
un buffer (l'attribut buffer
de la classe) au
canal de la connexion, et fournit des méthodes pour gérer ce
buffer.
La lecture d'un message asynchrone peut nécessiter plusieurs
appels à la méthode avancerLecture()
avant la
réception complète du message. Lorsque le message est complètement
reçu, la méthode avancerLecture()
passe le
statut messageComplet
à true
, et
ensuite, la méthode getMessage()
peut fournir ce
message. La méthode getMessage()
doit pouvoir être
appelée plusieurs fois pour le même message.
La méthode clear()
de la classe doit être appelée
avant de commencer la lecture d'un nouveau message.
Écrivez le constructeur de la
classe ConnexionAsynchrone
. Le
paramètre rwChan
est le canal déjà connecté. Le
constructeur doit rendre la connexion asynchrone et initialiser
deux attributs de la classe :
rwChan
: il doit être initialisé avec le canal déjà connecté passé en paramètre du constructeur ;buffer
: il doit être exactement dimensionné pour les échanges des messages du protocole.
Pour tester votre code, activez le test de la
méthode testConnexionAsynchrone()
de la classe de
test tp4509.tcp.TestConnexionAsynchrone.
Écrivez la méthode avancerLecture()
qui doit faire
progresser la lecture d'un message du protocole, et ainsi le
recevoir complètement appel après appel. Elle signale que le
message est complètement reçu en passant le statut
de messageComplet
à true
.
Pour tester votre code, activez le test de la
méthode testAvancerLecture()
de la classe de
test tp4509.tcp.TestConnexionAsynchrone.
Écrivez la méthode getMessage()
qui fournit un
message du protocole s'il est complètement arrivé,
et null
sinon. La méthode doit pouvoir fournir
plusieurs fois le même message.
Pour tester votre code, activez le test de la
méthode testGetMessage()
de la classe de
test tp4509.tcp.TestConnexionAsynchrone.
Attention: le test ci dessus contient une boucle. Si votre méthode est mal écrite cette boucle peut devenir une boucle infinie et le test ne se terminera jamais.
Les autres méthodes de la classe sont déjà écrites ; vous n'avez donc pas à les programmer. Mais, vous devez en utiliser certaines pour la suite du sujet. Donc, prenez le temps de lire et de comprendre les méthodes suivantes :
clear()
: elle prépare la classe pour la réception d'un nouveau message. Le message précédemment reçu est perdu et la méthode
getMessage()
ne peut délivrer un nouveau message qu'après sa réception complète ;messageComplet()
: cette méthode indique si le message en cours réception est complet ou non. Il faut tester cette
méthode avant tout appel à getMessage()
;close()
: elle ferme le canal de connexion. Elle doit être appelée toutes les fois que vous voulez fermer la connexion ou que vous détectez que le distant a fermé
la connexion ;envoyer()
: elle envoie un message du protocole. Même si le canal est asynchrone, on considère qu'un seul appel à cette
méthode suffit pour envoyer le message complètement.ThreadClient
(7pt)
Pour éviter de gérer une connexion asynchrone, l'application
cliente (AppliSwing
) lance deux threads :
VALEUR_GRILLE_CLIENT
). Il est lancé à la fin de la méthode main()
par une méthode de classe de SwingUtilities
: SwingUtilities.invokeLater(tacheSwing)
;
La classe ThreadClient
contient la
méthode run()
de ce thread. Elle possède deux
attributs initialisés par le constructeur qui vous est donné :
rwChan
: un canal sur une connexion TCP déjà ouverte avec le serveur ;picrossApi
: la référence vers l'API du picross. Pour ce client, vous avez besoin de cette API pour trois usages :
realiser()
qu'il faut appeler à la réception de chaque
message du protocole ;PicrossApi::estGagne()
;PicrossApi::setConnexionPerdue(boolean)
avec la valeur true
.recevoirMessage()
(2pt)
Le rôle principal du thread du client est de recevoir des
messages et de réaliser l'action associée. La
méthode recevoirMessage()
réalise la réception d'un
message en mode synchrone.
Écrivez la méthode recevoirMessage()
qui retourne le
message une fois qu'il est totalement reçu, ou la
valeur null
si une exception se produit ou si la
connexion est fermée.
Pour tester votre code, activez le test de la
méthode testRecevoirMessage()
de la classe de
test tp4509.tcp.TestThreadClient.
run()
(5pt)Commencez par écrire les instructions pour lancer le thread:
La classe runnable ThreadClient
est déjà
partiellement écrite, mais les instructions qui lancent le thread avec
cette classe restent à écrire. Ajoutez à la fin de la méthode main()
de la classe tp4509.graphique.AppliSwing
les intructions pour lancer un
nouveau thread qui exécute la méthode run()
de la classe ThreadClient
.
Ensuite écrivez la méthode run()
:
La signature de la méthode run()
nous est imposée par
l'interface Runnable
et ne permet donc pas la
transmission des exceptions. Il faut traiter dans la méthode
toutes les exceptions qui peuvent se produire. Le traitement d'une
exception se fera par l'affichage du message de l'exception. Donc,
pour les exceptions que votre code lève, veuillez fournir un
message clair qui explicite la raison de l'exception.
En mode réseau, l'application graphique ne peut pas s'initialiser
tant que les informations sur la taille de la grille et des
indices ne sont pas reçues. Le thread de l'application
graphique est bloqué par un wait()
en attente d'une
notification signalant la disponibilité de ces
informations. Lorsqu'elles sont reçues et traitées, il faut
appeler la méthode notifierFinInitialisation()
pour
redémarrer le thread de l'application graphique.
La méthode run()
déroule le protocole d'une partie de
Picross. Elle doit donc ressembler à cela :
TAILLE_GRILLE
;TAILLE_INDICES
;notifierFinInitialisation()
;TAILLE_GRILLE
et TAILLE_INDICES
.
Si l'ordre des messages du protocole n'est pas cohérent, il faut
lever l'exception ProtocoleException
avec un message
explicite. L'erreur est considérée comme fatale (fin
du thread) s'il s'agit d'un deux premiers messages, mais
sinon il faut afficher le message, et continuer le programme.
Écrivez la méthode run()
. Si la façon de traiter un
message du protocole ne vous semble pas claire, relisez la section
2.2 du sujet.
Pour tester votre code pour les situations normales, activez le
test de la méthode testRun()
de la classe de
test tp4509.tcp.TestThreadClient.
Pour tester votre code pour les situations anormales, activez le
test de la méthode testRun2()
de la classe de
test tp4509.tcp.TestThreadClient.
Même si votre code ne passe pas la seconde série de tests, vous pouvez continuer la suite du sujet : votre programme fonctionnera dans les situations normales ; cependant il n'aura pas le comportement attendu en cas d'anomalies dans le protocole.
ServeurPicross
(8pt)
La classe ServeurPicross
contient un serveur TCP
multiclient en mode asynchrone. Vous devez écrire deux
méthodes :
createServerSocket()
qui crée un serveur TCP asynchrone ;serverLoop()
qui contient la boucle d'attente des nouveaux clients et des messages des clients déjà connectés.
La méthode envoyerPicross()
vous est donnée. Elle
contient le code qui envoie toutes les informations utiles à un
client qui se connecte.
createServerSocket()
(2pt)Écrivez le code qui crée un serveur TCP asynchrone en attente sur le port passé en paramètre.
serverLoop()
(6pt)
Écrivez la boucle d'un serveur asynchrone en utilisant la
classe Selector
de JAVA NIO
.
Chaque client connecté est
représenté par une instance de la classe ConnexionAsynchrone
écrites
à la question 3.2 de ce sujet.
Cette boucle doit gérer trois types d’événements :
ServeurPicross::envoyerPicross()
pour envoyer l'état de la grille au nouveau joueur ;VALEUR_GRILLE_CLIENT
de la part d'un client, il faut :
VALEUR_GRILLE_SERVEUR
avec les trois mêmes paramètres que le message reçu.
Cette boucle s'arrête quand la partie est gagnée
(picrossApi.estGagne()
retourne true). Il
faut alors envoyer le message GAGNE
à tous les
clients.
Écrivez la méthode serverLoop()
.
Une fois cette méthode écrite, le programme est terminé et il est possible de le lancer. Pour le tester, voici la procédure :
pom.xml
) ;mvn exec:java@server
». Cette commande lance le serveur sur le port 4545 et sur le fichier de jeu ./data/sablier.txt
;mvn exec:java@client
». Cette commande lance l'application graphique qui lancera le thread client (connexion sur l'adresse localhost:4545
). Cliquez sur deux ou trois cases de la grille pour les noircir ;mvn exec:java@client
». Cela lance la même application graphique, mais la fenêtre de jeu doit déjà avoir en noir les cases que vous avez noircies dans la première fenêtre. Cliquez sur des cases dans les trois fenêtres, et l'action doit à chaque fois être visible partout ;Faites la question 3.5 avant de tenter les tests ci dessous.
Pour tester la robustesse de votre application, vérifiez que l'utilisateur ne subit pas une pile d'exception, mais peut soit continuer la partie normalement, soit être averti par un message clair de la raison de la fin du programme pour les cas suivants :
Protocole
(2pt)
Dans l'énumération Protocole
, l'association entre la
méthode à appeler et la valeur de l'énumérateur est faite avec le
constructeur qui reçoit en paramètre une instance d'un objet qui
implémente l'interface fonctionnelle
ProtocoleAction
. Dans le code actuel, toutes les
constructions se font grâce à des classes d'implémentation
définies de façon explicite dans le
paquetage tp4509.tcp.protocole_action
.
Modifiez la construction de
l'énumérateur TAILLE_GRILLE
en utilisant une classe
anonyme à la place de la classe TailleGrilleAction
.
Modifiez la construction de
l'énumérateur TAILLE_INDICES
en utilisant une
expression lambda à la place de la
classe TailleIndicesAction
.
Supprimez les classes TailleGrilleAction
et TailleIndicesAction
(et leur ligne
d'import) dans le
paquetage tp4509.tcp.protocole_action
et vérifiez que
le programme fonctionne encore. Si vous avez besoin de rétablir
ces classes, il y a une version de sauvegarde dans le
dossier save
du projet.