Page suivante Page précédente Table des matières

4. Le Serveur

4.1 Présentation

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.

Le serveur implémenté est donc un serveur multi-parties / multi-joueurs, cela signifie que le serveur peu :

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.

4.2 Architecture

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

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.

\begin{figure}[!hbtp] \begin{center} \epsffile{figures/structure.eps} \caption{Structure des parties} \end{center} \end{figure}

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 (ListVote*) pour chaque partie, la liste de toutes les parties (ListGameItem*) et le tableau recensant tous les joueurs (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 (Game) et sa liste de joueur (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.

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.

4.3 Spécificité du jeu

Système de vote

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 tous les joueurs ont répondu oui. Un seul vote négatif suffit pour le rejeter.

Déconnexion des clients d'une partie

La gestion des déconnexions des clients en cours de partie est un aspect important du jeu. Trois solutions étaient envisageable :

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.

Détermination du vainqueur

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é. \begin{figure}[!hbtp] \begin{center} \epsffile{figures/hit.eps} \caption{Recherche du gagnant} \end{center} \end{figure}

Facilité d'ajout de nouveaux jeux

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.

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.

  char* partie[] = {"Morpion", "Puissance4"};
Puis, au niveau de la mémorisation du coup (fonction 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).

L'ajout d'un nouveau type de jeu peut donc se faire sans revoir toute l'architecture du serveur.

4.4 Fonctionnement du serveur

Détachement du serveur de l'environnement

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é.

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 SYSLOG.

La fonction principale qui permet de détacher le processus, 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 :


#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

Comme on peut le voir dans cet extrait du source, le test de l'existence de la fonction setsid() se fait par l'intermédiaire du programme autoconf qui a déclaré une constante du nom de HAVE_SETSID.

Le coeur du serveur : select

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 accept() est bloquante. Le projet doit pouvoir écouter un certain nombre de clients en même temps, et donc doit écouter plusieurs sockets.

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 Futur du projet pour plus de détails) :

L'utilisation de la fonction 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 -V qui, lorsque utilisée avec le serveur, donne le nombre maximum de descripteurs ouvrables. Voici quelques exemples selon les machines testées :

Fonctionnement de la réception des messages

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 SockEvent, contient les informations suivantes :

Cheminement d'un message à travers les modules

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 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.

Cette fonction de hachage renvoie un index dans une structure qui associe le mot clef avec une variable de type fonction :


struct commmands {
    char *name;
    void (*func)(SockEvent *ev, char *args);
};

La fonction spécialisée vérifie les arguments, et effectue les traitements qui doivent être faits pour ce message.

4.5 Sécurité et assurance de stabilité

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.

L'accès aux fichiers externes en écriture/lecture

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.

Vérification de la stabilité du serveur

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 tests/, dans lequel un mini-client a été implémenté.

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 fd_set varie selon le système). Pour vous en assurer, il est possible de lancer le serveur avec l'option -V, qui donnera le nombre maximal de clients possibles pour un processus.

Vérification des débordements de structures et tableaux

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 strncpy, et non 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.

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.


Page suivante Page précédente Table des matières