3 Organisation du travail
3.1 Généralités
Il est fortement conseillé de suivre les étapes proposées ci-après pour réaliser le travail.
3.2 Organisation modulaire
Le projet est constitué du programme pour le micro-contrôlleur, de 2 bibliothèques et
du programme tournant sous Unix. Chacune de ces entités est développée
dans un répertoire propre. On gère donc les quatre répertoires suivants :
-
Costume -
- pour les sources du programme chargé sur la plate-forme Arduino Lilypad ;
- Communication -
- pour les sources de la bibliothèque contenant les fonctions
de gestion réseau et série libcom ;
- Threads -
- pour les sources de la bibliothèque contenant les fonctions
de gestion des threads libthrd ;
- Jeu -
- pour l'application de gestion du jeu et de communication avec les accessoires.
Cette arborescence et quelques squelettes de fichiers sont disponibles sous forme
d'un fichier au format tar et compressé à l'URL
http://www.plil.net/~rex/Enseignement/Systeme/Tutorat.IMA4sc.Chaises/chaises.tgz.
Transférez ce fichier dans votre compte Polytech'Lille et décompressez-le avec
la commande tar xvzf chaises.tgz. Vous pouvez constater que très peu de code
est fourni concernant l'application de gestion des accessoires et du jeu :
-
quelques fonctions d'affichage dans la bibliothèque Communication pour impulser
l'affichage de déverminage ;
- le code des fonctions d'ouverture et de fermeture du port séries dans la même bibliothèque ;
- le squelette du code pour micro-contrôleur.
Cependant, le répertoire créé contient déjà un fichier Makefile global et des Makefile
annexes dans chaque sous-répertoire. Vous avez un exemple de Makefile permettant de générer
un exécutable et un exemple de Makefile permettant de construire une bibliothèque. Appuyez
vous sur ces deux exemples pour compléter les autres Makefile. Le projet doit pouvoir être
généré par la simple commande make exécutée dans le répertoire principal du projet. Il doit
être aussi possible de regénérer complètement le projet par la commande make clean all lancée
du même répertoire.
Vous constaterez aussi la présence d'un répertoire XBee contenant un utilitaire qui peut vous
aider pour configurer vos modules XBee. Le code de cet utilitaire est suffisament clair pour
que vous puissez en comprendre le fonctionnement en le lisant.
On prévoira de pouvoir compiler les différents sources avec un drapeau DEBUG (option
-DDEBUG de gcc), permettant un affichage conditionnel d'informations de déverminage
des programmes.
3.3 Sockets et serveur TCP
Il s'agit dans cette étape de réaliser un serveur TCP basique à l'aide de
l'interface de programmation des sockets.
Ecrivez dans le module libcom.c (répertoire Communication), les deux
fonctions suivantes :
-
int initialisationServeur(short int); Cette fonction prend en
paramètre le port sur lequel il faut écouter et retourne la socket de
lecture. Il vous est demandé d'activer l'option de réutilisation
d'adresse sur la socket d'écoute (fonction setsockopt).
- int boucleServeur(int, void (*)(int)); Cette fonction effectue
l'écoute sur la socket passée en premier argument et lors d'une
connexion, exécute la fonction passée en second argument. Cette
fonction passée en argument doit être une fonction qui prend une socket
en unique paramètre. Lors d'une connexion de client, la fonction
boucleServeur lance donc cette fonction avec la socket de dialogue
en paramètre.
Testez cette bibliothèque en écrivant un programme jeu.c (répertoire
Jeu) qui l'utilise et dont la fonction de traitement des connexions
(celle appelée par boucleServeur) effectue juste une écriture de message
dans la socket et clôt la connexion. Pour écrire, et plus tard lire, sur la
connexion utilisez les fonctions classiques comme fgets ou fprintf.
Pour y arriver vous devez transformer le descripteur de socket en une structure
de fichier par la primitive fdopen.
Ce programme peut prendre des arguments : -p <port> ou --port <port>
pour spécifier un numéro de port différent de celui par défaut (port
4000). Pour traiter les arguments, utilisez la fonction
getopt_long (voir la page de manuel correspondante). Si les arguments
sont incorrects, on doit afficher un message qui précise la syntaxe.
Pour plus de clarté, l'analyse des arguments et l'affichage de la syntaxe
seront écrits dans des fonctions séparées.
Modifiez la fonction de traitement des connexions afin qu'elle ne ferme la
connexion qu'après avoir lu une commande de l'utilisateur. Testez votre serveur
avec plusieurs commandes nc simultanées. Conclusion ?
3.4 Un serveur de jeu à base de processus légers
Pour que votre serveur de jeu puisse traiter plusieurs connexions simultanément,
vous allez lancer un processus léger (thread) par connexion. Pour cela, implémentez
la fonction publique de libthrd (répertoire Threads) :
int lanceThread(void (*)(int),int);
Cette fonction doit avoir comme action de lancer un thread dans le mode
détaché. Ce thread doit exécuter la fonction passée en paramètre. La dite
fonction prenant elle même comme paramètre le second paramètre de
lanceThread. Il vous est conseillé d'utiliser une fonction intermédiaire
récupérant un pointeur vers une structure comprenant les deux paramètres de
lanceThread.
Testez votre fonction lanceThread en créant une nouvelle fonction dans
jeu.c qui appelle votre fonction de traitement des connexions
via lanceThread. Utilisez la nouvelle fonction comme paramètre de
boucleServeur. Pour plus de clarté, ce serait une bonne idée de déplacer
les fonctions de traitement des ordres des utilisateurs du fichier jeu.c vers
le fichier gestionOrdres.c.
Vérifiez que votre serveur est maintenant capable de gérer plusieurs
connexions simultanément (toujours avec l'utilitaire nc).
La fonction lanceThread est utilisée dans plusieurs situations différentes.
Tout d'abord comme expliqué plus haut pour gérer les ordres des utilisateurs ; dans
ce cas l'entier passé à la fonction paramètre est un descripteur de socket de dialogue.
La fonction lanceThread est aussi utilisée pour démarrer le processus léger
de gestion des messages UDP et le processus léger de gestion du jeu.
3.5 Diffusion de messages par UDP
Dans la description de l'architecture générale il est indiqué que les processus Unix doivent
s'échanger des messages par UDP. Vous allez donc ajouter dans la bibliothèque Communication
les fonctions suivantes :
-
void serveurMessages(short int, void (*)(unsigned char *,int));
Cette fonction prend en paramètre le port sur lequel lancer un serveur UDP et la
fonction de traitement des messages reçus.
- int envoiMessage(int, unsigned char *, int);
Il s'agit de la fonction complémentaire, permettant d'envoyer un message UDP en
diffusion totale. Le premier argument est le port UDP ciblé, le second est le paquet
à envoyer et enfin le dernier la taille du paquet. N'oubliez pas d'activer l'option
permettant une diffusion totale (fonction setsockopt). Notez que le descripteur
de socket utilisé est celui créé, en variable globale, par la fonction précédente.
Testez vos fonctions en créant un fichier gestionMessages.c. Vous y écrirez une fonction
messages permettant de lancer un processus léger réalisant principalement un appel à la fonction
serveurMessages. La fonction de traitement associée se bornera à afficher le contenu
des messages reçus en considérant qu'il s'agit d'une chaîne de caractères. Dans le même
fichier écrivez une fonction qui va créer un message avec une chaîne quelconque et l'envoyer.
Dans l'embryon du fichier de gestion du jeu, que l'on va nommer algorithmeJeu.c,
écrivez une dernière fonction jeu permettant de lancer un processus léger
appelant régulièrement la fonction d'envoi de chaîne.
Faites en sorte de lancer, au niveau de la fonction principale, les fonctions messages
et jeu.
3.6 Structure de données du serveur de jeu
La structure de données du serveur de jeu doit principalement permettre de gérer
les joueurs qui se signalent.
Définissez cette structure de données dans le fichier entête jeu.h et
écrivez sa fonction d'initialisation.
Analysez les opérations nécessaires sur les structures de données afin de
déterminer celles qui nécessitent l'utilisation de sémaphores.
Implantez dans votre bibliothèque libthrd les deux fonctions publiques :
void P(int) ;
void V(int) ;
Ces fonctions cachent totalement le fait que vous utilisez des verrous
d'exclusion mutelle pour threads POSIX. En particulier, les verrous sont
représentés par une constante.
En implantant dans les divers fichiers, les fonctions nécessaires au
fonctionnement du processus Unix, prenez soin d'y ajouter les poses et levées
de verrous nécessaires.
3.7 Le serveur de jeu
Vous pouvez maintenant vous attaquer à l'écriture du code concernant le
serveur de jeu. Il vous est rappelé qu'il vous a été conseillé d'utiliser
les fichiers suivants :
-
jeu.h : fichier des structures de données ;
- jeu.c : fonction principale et analyse des arguments ;
- algorithmeJeu.c : fonctions implantant le jeu par communication série
avec le micro-contrôleur et d'envoi de messages UDP ;
- gestionMessages.c : fonctions de réception et d'envoi des messages UDP,
fonctions de construction et d'analyse des messages UDP eux-mêmes ;
- gestionOrdre.c : fonction d'analyse des ordres des utilisateurs, fonctions
d'exécution des différents ordres.
L'algorithme du jeu peut se décomposer comme suit.
-
Phase 1 :
- Boucle de récupération du type de joueur et de signalement du joueur par message.
Si le joueur s'avère être un animateur et que le tableau des joueurs montre la présence de plusieurs
animateurs en avertir le joueur. Cette phase ne peut pas durer moins d'une minute. Pour sortir de
cette phase un unique animateur doit être présent. Le processus de l'animateur demande alors à son
joueur de lancer la partie. A ce moment le processus de l'animateur passe en phase 2 et émet un
message de début de partie. Sur réception de ce message les compétiteurs passent aussi en phase 2.
- Phase 2/animateur :
- Le processus de l'animateur demande à son joueur de lancer la manche. Il émet un message
de début de manche, demande au joueur d'effectuer son geste et émet le message contenant le type du
geste. Le processus de l'animateur attend un message comportant le type du geste de tous les joueurs
encore en lice. Si l'attente est trop longue la manche est annulé et la partie continue. Si deux manches
de suite sont annulée, la partie est annulée. Le processus de l'animateur sort de cette manche quand il
s'aperçoit que le tableau des joueurs comporte moins de deux joueurs dans l'état "en lice". Il émet alors
le message de fin de partie et revient dans la première phase. Sinon la phase 2 est répétée.
- Phase 2/compétiteur :
- Le processus du compétiteur attend le début de manche et en averti son joueur.
Il se met en attente sur le geste de son joueur, le type du geste est envoyé. Si le type du geste n'est
pas le bon ou que tous les autres joueurs ont déjà envoyé leur geste le processus passe en phase 3. Dans
le cas contraire le processus vérifie si son joueur n'est pas le seul en lice. Auquel cas le joueur est
prévenu qu'il est le gagnant et le processus repasse dans la première phase. Sinon la phase 2 est répétée.
- Phase 3/compétiteur :
- Le joueur est averti de son échec. Les processus des compétiteurs éliminés attendent un message
de fin de partie. Ils repassent alors dans la première phase.
Il faut bien comprendre que la modification du tableau des joueurs est principalement effectuée par le
processus léger de réception des messages UDP. Par exemple quand un message de signalement est reçu ce
processus léger vérifie si le joueur est déjà dans le tableau et si ce n'est pas le cas l'y ajoute. Le
processus léger de gestion du jeu se cantonnera donc parfois à attendre que ce tableau soit à jour. Par
exemple dans la phase 2 concernant l'animateur, attendre que tous les compétiteurs aient envoyé leur type
de geste consiste juste à parcourir le tableau pour vérifier qu'un champ précis est bien affecté. Si ce
n'est pas le cas, le processus doit attendre un court instant et recommencer. Il est aussi possible d'éviter
la boucle en utilisant des sémaphores mais dans ce cas la gestion d'un temps limite est problématique.