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 :
\begin{figure}[!hbtp] \begin{center} \epsffile{figures/couche.eps} \caption{Repartition des couches} \end{center} \end{figure}
Nous avons pu dégager quatre modules indépendants de leur couche inférieure :
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 socket_.
Cette couche, qui est totalement indépendante est appelée par la couche 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 secur_.
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 morpion_
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 Gestion Morpion pour communiquer avec les clients. Toutes les fonctions de cette couche sont identifiables par le préfixe proto_.
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 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.
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.
.
|-- client
|-- doc
|-- etc
|-- images
|-- include
|-- scripts
|-- server
`-- tests
Tous les fichiers relatif au serveur sont dans le répertoire server
, ceux
relatif au client sont dans le répertoire client
. Les fichier include de
déclaration des fonctions sont dans include
. Cela inclus les fichiers
include du client et du serveur. Le répertoire 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).
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.
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':
int socket_print(int sock, char *msg);
Cette méthode de nommage a de nombreux avantages, elle permet de :
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 :
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
Alors que celle d'une machine sous HP-UX est :
int select (size_t nfds, int *readfds, int *writefds, int *exceptfds,
const struct timeval *timeout);
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 warning
supplémentaire à la compilation. Grace à autoconf
, il nous suffit de
tester le système sur lequel on se trouve par l'intermédiaire de #ifdef
.
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 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 Hachage parfait pour plus d'informations.