Projet ISTY3 <subtitle>Le serveur TitBet <author>Sébastien Moreau (<htmlurl url="mailto:sdroopy@mail.dotcom.fr" name="sdroopy@mail.dotcom.fr">) <newline> Luc Stepniewski (<htmlurl url="mailto:lstep@mail.dotcom.fr" name="lstep@mail.dotcom.fr">) <newline> Pierre Vassellerie (<htmlurl url="mailto:Pierre.Vassellerie@obspm.fr" name="Pierre.Vassellerie@obspm.fr">)<newline> <date>v1.6, Dimanche 3 Mai 1998 <abstract> Ce document présente le projet TitBet qui avait pour but de réaliser une application Client-Serveur implémentant un jeu de morpion. D'un coté, le serveur, réalisé en C, devait pouvoir gérer un nombre arbitraire de joueur ainsi que de parties, de l'autre, les joueurs devaient se connecter à travers une application réalisée en Tcl/Tk implémentant certaines fonctions en C. </abstract> <toc> <chapt>Point de vue général du projet <p> Nous n'avons pas pris le projet comme un projet comme les autres. Nous souhaitions pouvoir augmenter nos comptétences en matière de conception, d'analyse et de programmation de projets. Nous avons donc réalisé ce projet avec comme but sa diffusion, son utilisation par un grand nombre de personnes, sa possible extension/réutilisation et non juste une ou deux personnes testant un petit programme. <p> Ceci a bien sûr impliqué des méthodes d'analyses plus poussées et une très grande importance dans l'organisation du projet. Le projet étant assez volumineux, et le nombre de participants à ce projet s'élevant à trois, nous avons fait attention à ne pas partir dans un dédale de complexité dans le code, qui le rendrait totalement inmaintenable. De nombreux projets arrivent jusqu'à une version de <em/beta/ mais ne peuvent continuer parce que le code est devenu tellement complexe, qu'il n'est plus possible de faire quoi que ce soit. Les dépendances entre fonctions sont trop grandes, de nombreuses variables sont globales, rendant la recherche et la modification du code quasiment impossible. <p> <quote/Nous ne souhaitions pas cela pour ce projet./ <p> Après avoir étudié quelques projets importants tels que le serveur web Apache, ou la librairie Gtk et lu des articles sur le sujet <footnote>NCWorld (http://www.ncworldmag.com/ncworld/ncw-04-1998/ncw-04-modvucon.html)</>, nous avons pu dégager des méthodes et des règles à suivre telles que la programmation objet (la philosophie, pas le C++), l'organisation des sources, la sécurité, l'importance de l'optimisation ou le travail en équipe. <chapt>Méthode de développement <p> <sect>Conception objet (MVC) <p> <sect1>Séparation en modules <p> La répartition en modules s'est d'abord faite en niveaux logiques. Le fait d'avoir préparé et bien mis à plat la structure du serveur nous a permis de séparer les fonctionnalités du serveur en couches logiques, chacune dépendant de celle du dessous, tout comme le modèle OSI : <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/couche.eps} \caption{Repartition des couches} \end{center} \end{figure} > ]]> <p> Nous avons pu dégager quatre modules indépendants de leur couche inférieure : <descrip> <tag/Couche Bas niveau Sockets/ Cette couche regroupe les fonctions de gestion des connexions au niveau des sockets. Cela inclue la gestion des nouvelles connexions, ainsi que celles déjà existantes. Toutes les fonctions de cette couche sont identifiables par le préfixe <bf/socket_/. <tag/Couche Sécurité/ Cette couche, qui est totalement indépendante est appelée par la couche <em/Protocole de communication/ lorsqu'elle récupère des informations venant de l'extérieur. Par exemple, cette couche se charge de vérifier et, si c'est le cas, de retirer tous les caratères non autorisés, pouvant poser problème. Toutes les fonctions de cette couche sont identifiables par le préfixe <bf/secur_/. <tag/Couche Gestion Morpion/ Cette couche contient les fonctions permettant d'accéder aux données du serveur et permettant de les manipuler. On y trouvera les fonctions de gestion des votes, de test des coups reçus, ou de gestion des parties. Toutes les fonctions de cette couche sont identifiables par le préfixe <bf/morpion_/ <tag/Couche Protocole de communication/ Cette couche contient les fonctions relatives à toutes les actions possibles de la part du serveur, en réponse à des demandes du client. Ces fonctions sont toutes indépendantes et représentent chacune un des mots-clef possibles envoyés par un client. Cette couche s'appuye sur la couche <em/Gestion Morpion/ pour communiquer avec les clients. Toutes les fonctions de cette couche sont identifiables par le préfixe <bf/proto_/. </descrip> <p> Cette organisation en modules a été une expérience intéressante et s'est montrée être d'une très grande utilité lors de la programmation du projet. Le source est facile à modifier et l'ajout de correctifs se fait très rapidement. Le code est robuste car les fonctions sont isolées les unes des autres et sont découpées en fonctions élémentaires. Le code est aussi facilement réutilisable du fait de sont indépendance. Par exemple, on pourra noter la fonction <tt/morpion_noticeToAllPlayers()/ qui est très couramment utilisée dans la plupart des fonctions du protocole. Par exemple, le fait que chaque module puisse être interchangé ou modifié sans que cela ne se répercute sur toutes les fonctions nous a permis de gagner beaucoup de temps. <sect>Organisation des sources<label id="source-orga"> <p> Le fait d'avoir bien séparé les différentes parties du projet nous a permis de faire de même pour les fichiers sources. Nous les avons séparé par modules indépendants. <sect1>Répertoires <p> <tscreen> <verb> . |-- client |-- doc |-- etc |-- images |-- include |-- scripts |-- server `-- tests </verb> </tscreen> Tous les fichiers relatif au serveur sont dans le répertoire <tt/server/, ceux relatif au client sont dans le répertoire <tt/client/. Les fichier include de déclaration des fonctions sont dans <tt/include/. Cela inclus les fichiers include du client et du serveur. Le répertoire <tt/scripts/ contient différents scripts servant par exemple au lancement ou à l'arrêt du serveur, à la regénération du fichier contenant les mots-clef (par le programme Gperf). <tt/tests/ contient un programme qui est un mini-client, accompagné d'un script qui permettent de tester automatiquement le serveur avec un nombre important d'utilisateurs fictifs. <sect1>Règles de style<label id="regle-style"> <p> Chaque module a ses fonctions identifiées par un mot-clef qui est unique pour un fichier C. Par exemple, toutes les fonctions de gestion bas niveau des communications sont préfixées du mot 'socket': <tscreen> <code> int socket_print(int sock, char *msg); </code> </tscreen> Cette méthode de nommage a de nombreux avantages, elle permet de : <itemize> <item>Savoir immédiatement quelle est la fonctionnalité principale de la fonction ; <item>Pouvoir retrouver très rapidement dans quelle partie des sources elle se trouve ; <item>éviter que plusieurs des développeurs utilisent les mêmes noms de fonctions ; <item>Homogénéiser les noms de fonctions, et donc permet une lecture plus facile des sources. </itemize> <sect>Logiciels utilisés<label id="logi-util"> <p> <sect1>Autoconf <p> Autoconf est un programme permettant de tester le système selon certains critères comme la disponibilité de fonctions, la disponibilités de fichiers d'entêtes ou des tests sur la configuration de la machine utilisée pour la compilation. Le but d'autoconf est de pouvoir créer du code portable entre les différents Unix. Ces détections se font par l'intermédiaire de constantes qui sont testées lors de la compilation du programme. Par exemple, la fonction select() n'a pas la même déclaration sur tous les Unix. Celle de Linux est : <tscreen><code> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); </code></tscreen> Alors que celle d'une machine sous HP-UX est : <tscreen><code> int select (size_t nfds, int *readfds, int *writefds, int *exceptfds, const struct timeval *timeout); </code></tscreen> On peut voir que le tableau de descripteur readfds, que nous utilisons n'a pas la même déclaration sur chacun des systèmes. Ce qui génère un <em/warning/ supplémentaire à la compilation. Grace à <tt/autoconf/, il nous suffit de tester le système sur lequel on se trouve par l'intermédiaire de <tt/#ifdef/. <sect1>Gperf <p> Gperf permet de générer une table de hachage ainsi que sa fonction de hachage associée pour que la recherche soit minimale. C'est ce que l'on appelle <em/une table de hachage parfaite/. Il n'y a aucune collision. Bien sûr, il n'est pas possible d'ajouter dynamiquement pendant l'exécution du programme des informations. Il faut connaitre à l'avance les mots à placer dans la table. Ce prérequis convient parfaitement au traitement des mots-clef du serveur. Reportez-vous à la section <ref id="fonc-recep" name="Hachage parfait"> pour plus d'informations. <chapt>Description du protocole <p> Nous décrivons ici le protocole utilisé pour les échanges d'informations entre le serveur et ses différents clients. <newline> <newline> Nous avons choisi de faire communiquer le serveur avec les clients par l'intermédiaire d'un protocole de type <sq>clear text</sq>, c'est à dire constitué de messages facilement compréhensibles sous la forme de chaînes de caractères lisibles (comme le font de nombreux autres protocoles SMTP, FTP, IRC, etc.). Le fait de faire communiquer les clients/serveurs par l'intermédiaire de messages lisibles par un être humain permet de comprendre très facilement ce qui se passe, de tester et d'envoyer des messages entrés à la main, et de rester compatible au niveau des blocs de données quel que soit le type de machine. Le seul inconvénient est qu'il y a plus d'informations à envoyer par rapport à des envois en binaire. Mais pour ce projet, la vitesse et la quantité d'informations à transférer était négligeable.<newline> <newline> <sect>Généralités <p> <sect1>Conventions d'ecriture <p> <descrip> <tag>le_mot</tag> signifie que l'argument n'est en fait qu'un seul mot et ne doit donc contenir aucun espace <tag>la chaine</tag> signifie que l'argument est une chaine pouvant contenir des espaces <tag>{informations}</tag> signifie que l'argument est un ensemble d'informations structurées sous forme de listes imbriquées </descrip> <sect1>Description d'un message <p> Un message est constitué de deux parties: <itemize> <item>La partie clef <item>La partie options dependant de la clef de message. Sur certains messages cette partie est inexistante </itemize> Un message est une chaîne sur une seule ligne délimitée par un retour chariot. Par ailleurs, chacun des messages échangés ne constitue qu'une seule ligne. Le délimiteur de message est donc le retour chariot qui permet de différencier les lignes de messages échangées. <sect1>Différents types d'échanges utilisés <p> Le protocole est basé sur 2 types d'échanges de messages : <enum> <item> l'échange <sq>émission d'un message</sq> <item> l'échange <sq>émission d'un message/réponse à ce message</sq> </enum> Ces différents échanges ont toujours lieu entre un des clients et le serveur. L'échange d'information entre les différents clients est effectué par le serveur lui-même, selon la forme : <sq>émission d'un message/diffusion d'un autre message aux autres clients/réponse à cet autre message/réponse au message initial</sq> <sect1> L'acceptation et le refus <p> Dans le cas du deuxième type d'échange, la réponse attendue en retour est souvent une acceptation ou un refus de la donnée envoyée précédemment. Ceci est effectué par un message en retour : <descrip> <tag>acceptation :</tag> <bf>OK chaîne de caractères ou données renvoyées</bf> <newline> où la chaîne est un message informatif qui n'est normalement pas utilisé par la partie réceptrice qui se satisfait uniquement du <bf>OK</bf>. <tag>refus :</tag> <bf>BAD chaîne de caractères</bf><newline> où la chaîne de caractères sert à passer à la partie réceptrice la raison de ce refus, et pourra donc être utilisée par la partie réceptrice. </descrip> <sect>Les différents messages et échanges associés <p> Voyons maintenant les détails du protocole en analysant les différentes phases du jeu. <sect1>Début de partie <p> <descrip> <tag>MSG le message</tag> <itemize> <item><bf>Sens :</bf> serveur vers client. <item><bf>Réponse :</bf> <sl>aucune</sl> <item><bf>Fonction :</bf> permet, par exemple, au serveur de passer au client le <sq>message du jour</sq> juste après la connexion, ou encore des messages informatifs en cours de partie. </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/msg.eps} \caption {\it Message echange lors de l'envoi d'un message informatif par le serveur} \end{center} \end{figure} > ]]> <sect1>S'identifier auprès du serveur <p> <descrip> <tag>NICK le_surnom</tag> <itemize> <item><bf>Sens :</bf> client vers serveur. <item><bf>Réponse :</bf> <descrip> <tag>OK</tag> si le surnom est accepté par le serveur <tag>BAD le message</tag> si le surnom est refusé par le serveur (par exemple si il est déjà utilisé par un autre client) </descrip> <item><bf>Fonction :</bf> permet au client de communiquer au serveur le surnom utilisé par le joueur. <item><bf>Effet :</bf> Le serveur enverra ensuite un message <bf>GAMES</bf> (voir ci-dessous) afin de fournir au client la liste des informations sur les parties en cours. </itemize> </descrip> <descrip> <tag>GAMES {informations sur les parties et les joueurs}</tag> <itemize> <item><bf>Sens :</bf> serveur vers client <item><bf>Réponse :</bf>aucune <item><bf>Fonction :</bf> permet au serveur de fournir au client les informations sur l'ensemble des parties et des joueurs. </itemize> </descrip> Les informations passées au client sont : <itemize> <item>l'ID de la partie : celui-ci est un entier fixé par le serveur à la création de la partie <item>le nom de la partie : celui-ci est fourni par le client à la création de la partie et est un seul mot <item>le type de jeu de la partie (morpion ou puissance 4) <item>la largeur de la grille de jeu <item>la hauteur de la grille de jeu <item>la liste des informations sur les différents joueurs sur la partie: <itemize> <item>l'ID du joueur : celui-ci est un entier fixé par le serveur après la saisie du surnom <item>le nom (ou surnom) du joueur comme saisi par ce dernier au debut du jeu <item>la nom de la machine depuis laquelle est connecté le joueur </itemize> </itemize> Les informations sont passées sous la forme de liste imbriquées : <tscreen><verb> { {id_partie_1 nom_partie_1 type_partie_1 largeur hauteur { {id_joueur_1 nom_joueur_1 machine_1} {id_joueur_2 nom_joueur_2 machine_2} }} {id_partie_2 nom_partie_2 type_partie_2 largeur hauteur { {id_joueur_3 nom_joueur_3 machine_3} {id_joueur_4 nom_joueur_4 machine_4} {id_joueur_5 nom_joueur_5 machine_5} }} } </verb></tscreen> <bf>Attention :</bf> Nous représentons ici les informations sur plusieurs lignes pour des raisons de lisibilité, mais dans la pratique l'ensemble du message est sur une seule ligne. <newline> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/nick.eps} \caption{\it Messages echanges lors de l'identification du joueur auprès du serveur} \end{center} \end{figure} > ]]> <sect1>Créer une nouvelle partie <p> <descrip> <tag>NEW nom_partie largeur hauteur type_jeu</tag> <itemize> <item><bf>Sens :</bf> client vers serveur. <item><bf>Réponse :</bf> <descrip> <tag>OK id_partie</tag> si la création de la partie est acceptée par le serveur <tag>BAD le message</tag> si la création est refusée par le serveur (par exemple si le nom est déjà utilisé pour une autre partie ou si la taille de la grille est hors limites) </descrip> <item><bf>Fonction :</bf> permet au client de créer une nouvelle partie <item><bf>Effet :</bf> Ceci provoque l'envoi à tous les clients d'un message <bf>GAMES</bf> (voir ci-dessus), puis au joueur d'un message <bf>PLAY</bf> (voir ci-dessous) afin de lui indiquer que c'est son tour de jouer. </itemize> </descrip> <descrip> <tag>PLAY id_partie id_joueur</tag> <itemize> <item><bf>Sens :</bf> serveur vers client. <item><bf>Réponse :</bf> Aucune <item><bf>Fonction :</bf> permet au serveur de signifier au client à qui est le tour de jouer sur la partie </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/new.eps} \caption{\it Messages echangees lors de la creation d'une nouvelle partie} \end{center} \end{figure} > ]]> <sect2>Integrer une partie exsitante <p> <descrip> <tag>JOIN id_partie</tag> <itemize> <item><bf>Sens :</bf> client vers serveur. <item><bf>Réponse :</bf> <descrip> <tag>OK</tag> si le client est accepté par les autres clients sur la partie spécifiée <tag>BAD le message</tag> si le client est refusé par au moins un des autres clients </descrip> <item><bf>Fonction :</bf> permet au client de joindre une partie existante <item><bf>Effet :</bf> Si le joueur est accepté dans la partie, ceci provoque l'envoi à tous les autres clients présents sur la partie spécifiée d'un message <bf>GAMES</bf>, et ensuite au joueur un message <bf>GRID</bf> et un message <bf>PLAY</bf>. </itemize> Le joueur venant d'intégerer la partie jouera en dernier, c'est à dire juste avant le joueur dont c'est le tour au moment du <bf>JOIN</bf>. </descrip> <descrip> <tag>JOIN id_partie nom_joueur</tag> <itemize> <item><bf>Sens :</bf> serveur vers client <item><bf>Réponse :</bf> <descrip> <tag>YES id_partie nom_joueur</tag> si l'accès à cette partie est accepté par le client <tag>NO id_partie nom_joueur</tag> si l'accès est refusé </descrip> </itemize> </descrip> <descrip> <tag>GRID id_partie {les informations sur les coups déjà joues}</tag> les informations sont passées sous la forme de liste imbriquées : <tscreen><verb> { {id_joueur_1 {x1 y2} {x2 y2} ... {xn yn}} {id_joueur_2 {x1 y2} {x2 y2} ... {xn yn}} } </verb></tscreen> <bf>Attention :</bf> Nous représentons ici les informations sur plusieurs lignes pour des raisons de lisibilité, mais dans la pratique l'ensemble du message est sur une seule ligne. <itemize> <item><bf>Sens :</bf> serveur vers client. <item><bf>Réponse :</bf> Aucune <item><bf>Fonction :</bf> permet au serveur d'envoyer au client les coups déjà joués sur une grille </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/join.eps} \caption{\it Messages echanges lors d'une demande d'integration de partie} \end{center} \end{figure} > ]]> <sect2>Jouer <p> <descrip> <tag>HIT id_partie x y</tag> <itemize> <item><bf>Sens :</bf> client vers serveur. <item><bf>Réponse :</bf> <descrip> <tag>HIT id_partie x y id_joueur</tag> si le coup est accepté par le serveur. Ce message est envoye a tous les joueurs de la partie. <tag>BAD le message</tag> si le coup est refusé par le serveur </descrip> <item><bf>Fonction :</bf> permet au client d'envoyer au serveur le coup qu'il vient de jouer <item><bf>Effet :</bf> <descrip> <tag>Si personne ne gagne et que la grille n'est pas pleine :</tag> ceci provoque l'envoi à tous les autres clients présents sur la partie spécifiée d'un message <bf>PLAY</bf> indiquant à qui est maintenant le tour de jouer. <tag>Si un joueur gagne :</tag> ceci provoque l'envoi à tous les autres clients présents sur la partie spécifiée d'un message <bf>WIN</bf> indiquant le joueur ayant gagné la partie, puis d'un message <bf>GAMES</bf> ne contenant plus cette partie puisqu'elle est desormais terminée. <tag>Si la grille est pleine et que personne ne gagne :</tag> ceci provoque l'envoi à tous les autres clients présents sur la partie spécifiée d'un message <bf>END</bf> indiquant le match nul, puis d'un message <bf>GAMES</bf> ne contenant plus cette partie puisqu'elle est désormais terminée. </descrip> </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/hit_ok.eps} \caption{\it Messages echanges lors d'un coup valide} \end{center} \end{figure} > ]]> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/hit_bad.eps} \caption{\it Messages echanges lors d'un coup refuse} \end{center} \end{figure} > ]]> <descrip> <tag>WIN id_partie id_joueur</tag> <itemize> <item><bf>Sens :</bf> client vers serveur. <item><bf>Réponse :</bf> serveur vers client <item><bf>Fonction :</bf> est envoyé à tous les clients de la partie quand un joueur gagne </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/hit_win.eps} \caption{\it Messages echanges lors d'une victoire par un joueur} \end{center} \end{figure} > ]]> <descrip> <tag>END id_partie</tag> <itemize> <item><bf>Sens :</bf> serveur vers client. <item><bf>Réponse :</bf> serveur vers client <item><bf>Fonction :</bf> est envoyé à tous les client de la partie quand la grille est pleine et que personne n'a gagné </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/hit_end.eps} \caption{\it Messages echanges lors d'un match nul} \end{center} \end{figure} > ]]> <sect2> Quitter la partie en cours <p> <descrip> <tag>QUIT id_partie</tag> <itemize> <item><bf>Sens :</bf> client vers serveur. <item><bf>Réponse :</bf>aucune <item><bf>Fonction :</bf> permet au client de signifier au serveur qu'il quitte la partie spécifiée. <item><bf>Effet :</bf> Ceci provoque l'envoi à tous les autres clients d'un message <bf>QUIT</bf> (dans le sens serveur vers clients, voir ci-dessous). </itemize> </descrip> <descrip> <tag>QUIT id_partie id_joueur</tag> <itemize> <item><bf>Sens :</bf> serveur vers client <item><bf>Réponse :</bf>aucune <item><bf>Fonction :</bf> permet au serveur de signifier aux autres clients que le joueur spécifié quitte la partie spécifiée. <item><bf>Effet :</bf> Ceci provoque l'envoi à tous les clients restant d'un message <bf>GRID</bf> (afin qu'ils puissent réafficher une grille ou les coups du joueur ayant quitté la partie seront représentés par une autre icone), puis d'un message <bf>PLAY</bf> indiquant à qui est le tour de jouer (au cas ou on attendait avant le <bf>QUIT</bf> le coup du joueur qui a decidé de quitter la partie). </itemize> </descrip> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/quit.eps} \caption{\it Messages echanges lors du depart d'un joueur d'une partie} \end{center} \end{figure} > ]]> <!-- PARTIE SERVEUR --> <chapt>Le Serveur <p> <sect>Présentation <p> Dans un soucis d'evolution de TitBet, nous avons décidé d'implémenter dans le serveur le plus de fonctionnalité possible, ce même si le client actuel ne les supporte pas. Seul une modification future du client sera necessaire pour les prendre en compte. De plus, le serveur est totalement independant du client. Ainsi, n'importe qui peut s'il le desire développer son propre client dans le langage qu'il desire et utiliser toutes les possibilités du serveur. <p> Le serveur implémenté est donc un serveur multi-parties / multi-joueurs, cela signifie que le serveur peu : <itemize> <item> Gérer plusieurs parties simultanément ; <item> Gérer des parties de différents types (Morpion, Puissance4) ; <item> Gérer plusieurs joueurs ; <item> Permettre à un joueur de créer plusieurs parties dans la meme session ; <item> Permettre à un joueur de jouer dans plusieurs parties en même temps ; <item> Permettre à un joueur de joindre un jeu en cours de partie ; <item> Gestion des déconnexions des joueurs en pleine partie. </itemize> <p> Le client, dans sa version actuelle, ne gère qu'une partie par joueur. Il est donc impossbile pour une même personne de figurer dans plusieurs parties à la fois à moins de lancer plusieurs clients. <sect>Architecture <p> Pour gérer une multitude de parties et de joueurs, nous avons implémenté notre gestion du morpion par des liste chainée, n'ayant pas de limites fixes dans le nombre maximum de parties et de joueurs (cf Figure Structure des parties). En effet, les limites auxquelles nous avons été confrontées furent celles de la mémoire (pour l'allocation de nouvelle partie ou de joueur) et du nombre de descripteurs ouverts simultanément <P> Pour la gestion d'une partie, la seule limite se trouvait dans le nombre de joueur y jouant. Ce dernier est limité au nombre d'icones présentent lors de la compilation. Nous aurions pû utiliser un tableau pour gérer la liste de joueurs dans la partie mais il est peu probable que le nombre maximum de joueur soit atteind. Cela serait injouable ! La solution de garder les listes chainées a pour avantage d'économiser de la mémoire. <p> <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/structure.eps} \caption{Structure des parties} \end{center} \end{figure} > ]]> <p> L'architecture de la gestion des parties est constituée autour de la structure Games. Elle est à la base de tout, elle contient la liste des votes en cours <tt/(ListVote*)/ pour chaque partie, la liste de toutes les parties <tt/(ListGameItem*)/ et le tableau recensant tous les joueurs <tt/(PlayerSock)/ qu'ils jouent ou non. Chaque élément de la liste des parties possède une référence vers la partie elle-même <tt/(Game)/ et sa liste de joueur <tt/(ListPlayerSockItem*)/. Chaque élément de la liste de joueurs contient l'indice de ce joueur dans le tableau de joueur défini dans la structure Games, afin de ne pas dupliquer les informations. <p> Chaque partie possède une grille représentant le jeu et la liste des joueurs y participant. Ces joueurs sont identifiés par un numéro propre à la partie, c'est ce même numéro qui est stocké dans la grille. <sect>Spécificité du jeu <p> <sect1>Système de vote <p> Pour permettre à un joueur de joindre une partie, un système de vote a été mis en place. Le joueur ne sera accepté que si <bf/tous/ les joueurs ont répondu oui. Un seul vote négatif suffit pour le rejeter. <sect1>Déconnexion des clients d'une partie <p> La gestion des déconnexions des clients en cours de partie est un aspect important du jeu. Trois solutions étaient envisageable : <itemize> <item> Supprimer les coups du joueur de la grille. Cette solution n'est pas idéale car les autres joueurs ont placé leur coups en fonction de ceux de ce joueur. Supprimer les coups fausserait complètement la partie ; <item> Garder les coups et l'affecter au premier joueur qui déciderait de joindre la partie. Cette solution est elle aussi a oublier ; <item> Garder les coups et les affecter à un joueur virtuel, ce joueur récupererait tous les coups des joueurs ayant quitté la partie. Ces coups ne sont pas pris en compte pour déterminer le vainqueur, le cours de la partie n'est donc en aucun cas faussé. </itemize> <p> Nous avons utilisé la dernière méthode, à savoir affecter les coups à un joueur virtuel. Ils sont représenté par un icone représentant un cafard. <sect1>Détermination du vainqueur <p> Pour déterminer si un joueur gagne ou non une partie, nous ne parcourons pas toute la grille, mais seulement les 4 directions possibles à partir du coup du joueur (cf Figure Recherche du gagnant). On comptabilise les coups successifs de ce joueur et dès qu'un des coups scanné ne lui appartient pas, on arrête la recherche sur cette direction. Si le nombre de coups alignés d'une direction est supérieur ou égal à cinq pour le morpion ou quatre pour le puissance 4, le joueur gagne la partie. Le temps de recherche est ainsi fortement optimisé. <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/hit.eps} \caption{Recherche du gagnant} \end{center} \end{figure} > ]]> <p> <sect1>Facilité d'ajout de nouveaux jeux <p> La définition de structures et l'implémentation en couches du serveur permet d'ajouter assez facilement et rapidement de nouveaux types de jeu utilisant une grille. <p> Pour exemple, initialement le serveur ne devait gérer que le jeu de type Morpion. L'ajout du jeu Puissance 4 fut assez simple, il fut nécessaire de définir le type Puissance4 dans la variable globale (partie) de la librairie morpion. <tscreen><verb> char* partie[] = {"Morpion", "Puissance4"}; </verb></tscreen> Puis, au niveau de la mémorisation du coup (fonction <tt/morpion_hit/), il suffit de le traiter selon le type de partie et d'appeler la fonction qui vérifie son état (gagne ou non). Il est évident que cette fonction est propre à chaque type de jeu, dans notre cas le developppement de cette fonction pour le puissance 4 fut inutile car l'algorithme utilisé est le même que celui du morpion (on teste un alignement de pièces sur un échiquier). <p> L'ajout d'un nouveau type de jeu peut donc se faire sans revoir toute l'architecture du serveur. <sect>Fonctionnement du serveur<label id="fonct-serv"> <p> <sect1>Détachement du serveur de l'environnement <p> Le serveur a la possibilité de se détacher du terminal sur lequel il a été lancé. Par défaut, il se comporte comme un serveur, c'est à dire qu'il se détache de tout l'environnement auquel il était attaché. <p> L'environnement est nettoyé (variables sensibles pour la sécurité retirées, ensemble des descripteurs fermés, changement du répertoire par défaut à la racine. La plupart des signaux sont détournés vers une fonction qui permet de quitter le serveur proprement. A partir de cet instant, le serveur ne peut plus communiquer que par l'intermédiaire de <tt/SYSLOG/. <p> La fonction principale qui permet de détacher le processus, <tt/setsid()/ n'est pas disponible sur toutes les machines que nous avons pu rencontrer. Il nous a donc fallu trouver d'autres méthodes pour le détacher : <tscreen> <code> #ifdef HAVE_SETSID if (setsid() == -1){ perror("Pas pu exécuter setsid()"); exit(EXIT_FAILURE); } #else if (setpgrp(0, getpid()) == -1){ perror("Pas pu exécuter setpgrp()"); exit(EXIT_FAILURE); } fd = open("/dev/tty", O_RDWR); /* On lache le terminal */ if (fd != -1){ ioctl(fd, TIOCNOTTY, 0); close(fd); } #endif </code> </tscreen> Comme on peut le voir dans cet extrait du source, le test de l'existence de la fonction <tt/setsid()/ se fait par l'intermédiaire du programme <tt/autoconf/ qui a déclaré une constante du nom de <tt/HAVE_SETSID/. <sect1>Le coeur du serveur : select <p> Les méthodes standard d'écoute de sockets et d'attente d'événements ne permettent pas de gérer plusieurs connexions simultanées. Ceci est dû au fait que la fonction <tt/accept()/ est bloquante. Le projet doit pouvoir écouter un certain nombre de clients <bf/en même temps/, et donc doit écouter plusieurs sockets. <p> Nous avions deux solutions (une troisième, extension de la deuxième que nous avons choisie a été envisagée pour une version future, voir le chapitre <ref id="futur" name="Futur du projet"> pour plus de détails) : <itemize> <item>Dès qu'une nouvelle connexion arrive, le serveur se dédouble avec la fonction <tt/fork()/, permettant au processus principal de continuer à écouter les nouvelles connexions. Le très gros désavantage de cette méthode, en dehors de sa lenteur due au fait qu'à chaque changement, tout le contexte est remplacé par un nouveau, toutes les variables et données sont dupliquées et une nouvelle version de ces données apparait dans chaque nouveau processus créé. La solution à ce problème est de mettre toutes les informations en mémoire partagée et de communiquer avec le processus père à travers des IPC. <item>La deuxième solution, que nous avons choisi d'utiliser est plus simple, et a de nombreux avantages. Elle consiste à utiliser la fonction <tt/select()/ qui permet d'attendre un événement en écoutant sur un grand nombre de descripteurs <bf/en même temps/. </itemize> L'utilisation de la fonction <tt/select()/ nécéssite l'utilisation d'une zone de stockage supplémentaire pour garder la trace des descripteurs ouverts, ainsi que ceux à écouter. Cette zone est en fait un ensemble de bits, chacun représentant un descripteur actif, s'il est à 1. C'est la taille de ce tableau de bits qui limite le nombre de personnes maximum pouvant se connecter en même temps pour un processus donné. Ce nombre varie en fonction du système d'exploitation utilisé. Pour connaitre ce nombre, nous avons ajouté l'option <tt/-V/ qui, lorsque utilisée avec le serveur, donne le nombre maximum de descripteurs ouvrables. Voici quelques exemples selon les machines testées : <itemize> <item>Linux 2.1.99 : 1024 <item>HP-UX 10.01 : 60 <item>DEC OSF 4.0 : 61 </itemize> <sect1>Fonctionnement de la réception des messages<label id="fonc-recep"> <p> Le serveur doit pouvoir traiter les messages qui lui arrivent le plus rapidement possible. Pour cela, la durée d'identification (ou d'aiguillage) d'un message doit être la plus courte possible. La fonction d'attente de message est organisée à la manière d'un objet. Lorsqu'elle reçoit un message, la fonction génère un objet qui est renvoyé. Cet objet, de type <tt/SockEvent/, contient les informations suivantes : <itemize> <item>Le message reçu ; <item>Longueur du message reçu ; <item>Numéro du descripteur correspondant au numéro du joueur ayant déclenché l'événement ; <item>Le type d'événement qui a déclenché la génération du message <descrip> <tag/EVENT_INTERRUPT, EVENT_BADDESCRI, EVENT_ERROR/ Ces événements reflètent différents cas d'erreur possible lors de la réception d'un message. <tag/EVENT_LOSTCONNEX/ Cas spécial lorsque le serveur a détecté la déconnexion d'un joueur de <sq/manière violente/. <tag/EVENT_REPLY/ Cas typique d'envoi d'un message d'un client étant déjà connecté et enregistré sur le serveur. <tag/EVENT_NEWCONNEX/ Evénement déclenché lorsqu'un nouveau joueur se connecte. </descrip> </itemize> <sect2>Cheminement d'un message à travers les modules <p> En fonction de son type, l'objet contenant le message est routé sur différentes parties du serveur. L'objet ayant le type de message le plus courant est envoyé à la couche <em/Protocole de communication/. Le message, découpé en deux (mot-clef et arguments), est immédiatement transféré à la fonction spécialisée qui doit la traiter grace à la fonction de hachage. <p> Cette fonction de hachage renvoie un index dans une structure qui associe le mot clef avec une variable de type fonction : <tscreen><code> struct commmands { char *name; void (*func)(SockEvent *ev, char *args); }; </code></tscreen> La fonction spécialisée vérifie les arguments, et effectue les traitements qui doivent être faits pour ce message. <sect>Sécurité et assurance de stabilité <p> La sécurité est une part très importante dans la réalisation d'un projet. Plus particulièrement lorsque le projet à développer est un serveur de données sur lequel n'importe qui peut se connecter. <sect1>L'accès aux fichiers externes en écriture/lecture <p> Nous avons volontairement limité le nombre de fichiers accédés, et fait très attention à leur utilisation. Le seul fichier externe utilisé par le serveur est celui qui indique le numéro du processus pour pouvoir l'arrêter plus facilement. Ce fichier, stocké dans /tmp est en accès à tout le monde. Une personne pourrait causer des dégats si, entre l'instant où le fichier est créé et l'instant où l'on écrit, ce fichier était remplacé par un lien vers un fichier non accessible normalement. Pour éviter cela, nous avons testé si le type du fichier était un lien ou non. Tout problème est rapporté par SYSLOG. <sect1>Vérification de la stabilité du serveur <p> Nous avons souhaité connaitre la stabilité du serveur. Pour cela, il nous fallait simuler un ensemble d'utilisateurs se connectant au serveur de différente manière, et effectuant différentes opérations. Le programme de test se trouve dans le répertoire <em>tests/</em>, dans lequel un mini-client a été implémenté. <p> Nous avons lancé un nombre croissant de clients se connectant/déconnectant en meme temps. Nous nous sommes arretés à 236 clients (le nombre de déconnexions étant plus plus grand que le nombre de connexions, nous n'avons pas dépassé ce chiffre) accédant au serveur simultanément sans aucun problèmes. Le serveur peut supporter une charge variable d'utilisateurs (taille de <tt/fd_set/ varie selon le système). Pour vous en assurer, il est possible de lancer le serveur avec l'option <bf>-V</bf>, qui donnera le nombre maximal de clients possibles pour un processus. <sect1>Vérification des débordements de structures et tableaux <p> Chaque déplacement/assignation de données se fait toujours avec des tailles fixes et prédéfinies. On utilise, par exemple, des fonctions telles que <tt/strncpy/, et non <tt/strcpy/. Pour tester les erreurs et oublis éventuels, nous avons réalisé un mini-programme qui envoie des données générées au hasard et de taille très variable. De cette façon, nous avons pu corriger et rajouter des tests de débordements possibles pour éviter les coredumps, ou écritures dans des zones du programme non autorisées. <p> Par la suite, nous avons utilisé Purify ainsi que Electric Fence, deux applications vérifiant qu'il n'y a aucuns débordement dans le programme. <chapt>Le Client <sect>Analyse <p> <sect1>Les différentes phases du jeu. <p> Dans le jeu du morpion, les différentes phases du jeu sont : <enum> <item> <itemize> <item>On décide d'une taille de grille si on crée une nouvelle partie. <item>Ou, on intègre une partie existante. </itemize> <item>On choisit un symbole afin de s'identifier sur la grille de jeu. <item>On attend que ce soit à nous de jouer. <item>On joue un coup valide, c'est à dire que l'on met notre symbole dans une case libre de la grille. <item> <itemize> <item>Si on a aligné 5 symboles, alors on gagne la partie. <item>Ou, si la grille est pleine, alors il y a match nul. </itemize> <item>On continue en reprenant à la phase 3 si la partie continue ou sinon on reprend à la phase 1. </enum> Le client doit donc être en mesure de proposer aux joueurs d'évoluer dans ces différentes phases de jeu, selon l'ordre établi ci-dessus. <sect1>Échanges à effectuer avec les autres joueurs. <p> Le but du projet est de développer une application de jeu du morpion entre utilisateurs de systèmes Unix en réseau. Nous devons donc effectuer différents échanges entre les différents joueurs, afin que les grilles de jeu de chacun des joueurs soient tenues en permanence à jour. Par ailleurs, l'application doit permettre d'obtenir la liste des parties déjà en cours (ce qui sous-entend qu'il peut exister plusieurs parties), de créer une nouvelle partie (et donc la rendre accessible aux autres joueurs du réseau), de joindre une des parties existantes selon l'accord des autres joueurs (il va donc falloir créer un système de <sq>vote</sq> à travers le réseau). De plus, l'application doit permettre de signifier aux joueurs d'une même partie s'il y a match nul (la grille est pleine et personne ne gagne) ou si l'un des joueurs a gagné (en alignant 5 signes horizontalement, verticalement ou en diagonale). Partant de cette étude nous avons donc pu établir le protocole décrit précédemment. <sect>Implémentation <p> Dans notre implémentation du jeu, nous avons décidé de confier <sq>l'intelligence</sq> du jeu à un serveur gérant l'ensemble des joueurs, des parties et des coups joués dans ces parties. Ceci nous permet donc d'avoir des clients simples, ne gérant que : <itemize> <item>l'interface homme/machine <item>les messages en provenance du serveur conformes au protocole défini <item>les messages à destination du serveur. </itemize> Le client est construit en différents modules articules selon les différentes phases de jeu décrites ci-dessus. On peut donc représenter le fonctionnement du client par le schema suivant : <![%fmttex; [ <? \begin{figure}[!hbtp] \begin{center} \epsffile{figures/phases_client.eps} \caption{\it Differentes phases de fonctionnement du client} \end{center} \end{figure} > ]]> <sect1>Description des différentes phases <p> <descrip> <tag>Nick</tag> est la phase durant laquelle le joueur doit saisir son surnom afin de s'identifier auprès du serveur. <tag>ListeParties</tag> est la phase durant laquelle le joueur peut visualiser la liste des parties existantes, et choisir de créer une nouvelle partie ou joindre une partie existante. <tag>NewPartie</tag> durant cette phase le joueur saisi les paramètres de creation de la nouvelle partie, les envoie au serveur et attend l'accord ou le refus de cette creation par le serveur. <tag>JoinPartie</tag> est la phase surant laquelle le joueur demande à joindre une partie existante et attend confirmation ou refus de son intégration par le serveur. <tag>JouePartie</tag> durant cette phase le joueur visualise la grille de jeu et les informations sur les joueurs présents dans la partie. Il est en attente de son tour de jouer ou d'une modification de la grille. <tag>Play</tag> durant cette phase le joueur peut jouer un coup et attend que ce coup soit accepté ou refusé par le serveur. <tag>Grid</tag> est la phase durant laquelle une mise à jour complète de la grille est effectuée. <tag>Win</tag> est la phase indiquant au joueur que la partie a été gagnée. <tag>End</tag> est la phase indiquant au joueur qu'il y a eu match nul. </descrip> <sect>Ajout à Tcl de commandes permettant les échanges de messages avec le serveur <p> Les échanges entre les clients et le serveur de messages conformes au protocole doit se faire à l'aide des <it>sockets</it>. Le serveur : <itemize> <item>attend sur un port TCP des demandes de connexions en provenance des clients et établit ces connexions, <item>reçoit des messages en provenance des clients et les traite, <item>envoie des messages vers chacun des clients qui doivent les traiter, <item>ferme une connexion sitôt qu'un client se déconnecte. </itemize> De son coté le client doit : <itemize> <item>effectuer une demande de connexion au serveur et établir cette connexion, <item>envoyer des messages conformes au protocole vers le serveur, <item>recevoir les messages en provenance du serveur et les traiter dès leur arrivée (ou au moins le plus rapidement possible) car un message en provenance du serveur peut influer sur le déroulement de la partie, <item>fermer la connexion avec le serveur si ce dernier coupe la connexion. </itemize> Afin d'être le plus indépendant possible de l'architecture, le client est codé en majeure partie en langage script Tcl/Tk. Afin de permettre ces échanges d'informations avec le serveur, nous avons dû ajouter de nouvelles commandes à ce langage script, effectuant les opérations décrites ci-dessus. Ces nouvelles commandes sont : <itemize> <item><tt>ouvre_connexion</tt> qui effectue la demande de connexion au serveur et établit cette connexion <item><tt>ferme_connexion</tt> qui ferme la connexion si le serveur <sq>tombe</sq>. <item><tt>ecrit_socket</tt> qui permet d'envoyer un message vers le serveur <item><tt>lit_socket</tt> qui permet de lire les messages en provenance du serveur. </itemize> Les 3 premières de ses fonctions ne présentent aucun problème à l'implémentation car l'instant auquel elles sont utilisées est parfaitement déterminé dans le code du client. Par contre, il n'en est pas de même avec la fonction de lecture des messages en provenance du serveur. En effet les messages envoyés par le serveur peuvent arriver à tout moment sur la connexion du coté client. De plus, comme nous l'avons dit plus haut, ces messages doivent être traités le plus tôt possible de leur arrivée. Il a donc fallu trouver un moyen permettant à l'interprérteur Tcl d'aller chercher les messages sur la connexion dès que ceux-ci sont disponibles. Pour ce faire nous effectuons l'appel de l'instruction à chaque fois que l'interpréteur Tcl ne fait plus rien (ce qui est quand même très souvent puisque le joueur attend la majeur partie du temps son tour de jouer), grâce à l'instruction : <tscreen><verb> after idle { TraiteProtocole [lit_socket] } </verb></tscreen> qui signifie <sq><it>quand l'interpréteur ne fait plus rien, alors il va lire un message sur la connexion, puis traite ce message grâce à la procédure <tt>TraiteProtocole</tt> (écrite en Tcl)</it></sq>. Cette <sq>banale</sq> phrase fait apparaître un autre problème : la procédure <tt>TraiteProtocole</tt> ne gère qu'un seul message à la fois. Or en étudiant rapidement le protocole défini, nous nous appercevons que plusieurs messages peuvent arriver à la suite. De plus, les sockets TCP ne garantissent pas du tout que lors d'une lecture, nous obtiendrons l'intégralité d'un message. Ce problème a été résolu de la manière suivante: nous avons défini un message comme étant une chaîne de caractères terminée par un retour chariot. Il suffit donc de mettre dans un tampon les données lues sur la socket à la suite des données déjà présentes dans ce tampon (qui sont normalement le début du message que nous sommes en train de finir de lire), puis de rechercher le premier retour chariot. Nous connaissons alors le premier message reçu, et en intégralité il ne reste plus qu'à le retourner afin qu'il soit traité par <tt>TraiteProtocole</tt>. Étant donné que nous devions utiliser la librarie Tcl pour l'ajout de ces nouvelles commandes, nous avons profité des fonctions de gestion d'une chaîne dynamique offertes par la librairie Tcl (fonctions <tt>Tcl_DString*</tt>), ce qui nous a évité d'avoir à écrire du code, de ce fait, inutile et peu intéressant. Évidemment, si l'interpréteur va chercher à lire des données à chaque fois qu'il ne fait plus rien (c'est à dire quand il n'y a plus de commande Tcl à interpréter, plus d'évènement X à gérer, etc.), il va donc appeler très souvent la commande <tt>lit_socket</tt> et donc se bloquer en attente de l'arrivée de données sur la socket. Pour remédier à ce problème, nous avons donc introduit l'appel à une instruction nous indiquant si des données sont disponibles sur la socket (à l'aide d'un timeout sur un select). Ainsi si aucune donnée n'est disponible à la lecture sur la socket et que le tampon des messages ne contient pas de message complet, alors nous redonnons immédiatement la main à l'interpréteur Tcl qui pourra alors se consacrer à gérer les évènements X ou tout autre chose. L'avantage de cette implémentation est de garder l'indépendance à l'architecture, de laisser le shell maître de ses opérations, et surtout d'éviter d'avoir un processus client passant l'intégrale de son temps à lire sur la socket, au détriment de la gestion des évènements X sur l'interface homme/machine, et consommant une grande partie des ressources de la machine à ne rien faire. <!-- CONCLUSION --> <chapt>Limitations actuelles et futur du projet<label id="futur"> <p> <sect>Limitations <p> Le projet a été conçu dans l'optique de pouvoir être réutilisé. Que ce soit les fonctions, les modules, ou le programme entier. C'est pour cette raison que nous avons essayé de prévoir la plupart des cas possibles d'utilisation et de fonctionnement. <p> Actuellement le serveur peut gérer un grand nombre de personnes en même temps. Pour pouvoir gérer plus de personnes, il nous faudra dédoubler les processus actuels. Ceci ne peut etre fait qu'à certaines conditions, telles que le fait qu'il ne doit pas y avoir de variables globales, ce qui aurait entrainé des dépendances supplémentaires inrésolvables. <p> De ce fait, certaines parties du serveur ne sont pas optimisées au maximum. Les premières versions du serveur avaient une structure commune renvoyée à chaque fois qu'un événement arrivait. Cette structure était unique. A priori cela ne posait aucun problèmes. Or, après quelques versions, nous nous sommes aperçus que si nous voulions transformer le serveur en un serveur plus grand, supportant un nombre de connexions encore plus grand, il nous fallait pouvoir gérer les connexions par paquets de connexions en parallèle. La présence de cette structure unique ne permettait l'utilisation et la génération simultanée d'événements. C'est pour cette raison qu'une nouvelle instance de cette structure est maintenant renvoyée à chaque nouvel événement. <sect>Futur <p> La principale limitation actuelle est la limitation du système concernant le nombre maximum de descripteurs ouvrables dans un processus. Cette variable limite le nombre maximum de joueurs sur un serveur. <p> Pour pallier à ce problème, il nous faudrait réaliser un système plus élaboré qui utilise plusieurs processus, à la manière des serveurs web actuels. Il y aurait un processus dit <sq/contrôleur/ qui lance plusieurs autres processus. Ces processus ne seront pas créés à partir de fork() mais seront des threads, ce qui nous obligera à utiliser des sémaphores. Chacune des threads écoutera un ensemble de connexions, tout comme le fait le serveur actuel, grace à la fonction <tt/select()/. Si une des threads arrive à un nombre limite de clients, il enverra l'information au processus contrôleur qui créera une nouvelle thread pour accepter le flot des clients. De cette manière, nous n'avons plus la limite du nombre maximal de descripteurs. <p> Une autre amélioration possible du serveur actuel serait d'allouer pour chacune des connexions un poids indiquant sa priorité. En effet, dans l'implémentation actuelle du programme, lorsque plusieurs événements arrivent en même temps, c'est le premier du tableau de bits (de la structure fd_set) qui est traité. Il faudrait étudier si le fait d'ajouter des poids à ces bits n'alourdissent pas la gestion des connexions au point d'être plus lent que la version précédente. Le traitement étant tellement rapide, qu'il ne nous a pas semblé utile d'implémenter cette fonctionnalité. </book>