LASFAR Omar Farouk ROCHE Raphael ERDAL Muhamed
Le sujet initial reposait sur un unique processus main qui capturait les entrées clavier et les transmettait à un processus 2048 via un pipe nommé. Nous avons fait évoluer cette architecture vers un modèle client-serveur pour supporter plusieurs parties simultanées.
Le processus main est devenu le client : il conserve son rôle de capture clavier et d'affichage, mais communique désormais avec un serveur centralisé plutôt qu'avec son propre processus de jeu. Le processus 2048 est devenu ce serveur, qui gère la logique de toutes les parties en parallèle.
Tous les clients partagent un uique pipe nommé pour envoyer leurs commandes au serveur. Pour que le serveur sache à qui répondre, chaque message embarque le PID du client :
typedef struct {
pid_t client_pid;
char move;
} client_message_t;Pour la réponse, chaque client crée à son démarrage son propre pipe de retour /tmp/2048_<PID client>. Le serveur y écrit la grille mise à jour après chaque mouvement. Ce choix évite d'avoir à gérer un pipe par client côté serveur.
Le client se fork en deux processus :
- Le père capture les entrées clavier et les envoie au serveur.
- Le fils lit en boucle sur le pipe de retour et rafraîchit l'affichage.
Cette séparation permet de ne pas bloquer la lecture clavier pendant l'affichage et vice-versa.
Le serveur tourne avec quatre threads permanents :
| Thread | Rôle |
|---|---|
thread_main (boucle principale) |
Lit le pipe nommé, identifie le client, dispatche le mouvement |
thread_move |
Applique le déplacement sur la grille |
thread_goal |
Vérifie victoire/défaite, renvoie la grille au client |
thread_admin |
Écoute q au clavier pour arrêter le serveur proprement |
Les données de toutes les sessions sont stockées dans un segment de mémoire partagée, alloué au démarrage selon le nombre de joueurs passé en argument (./serveur nb_joueurs). Chaque slot contient la grille, le PID du client, le dernier mouvement et un champ state.
Ce champ state pilote la coordination entre les threads comme un automate :
state = 0 → thread_main écrit le mouvement → state = 1
state = 1 → thread_move applique le coup → state = 2
state = 2 → thread_goal vérifie et répond → state = 0
L'accès à la mémoire partagée est protégé par mutex unique. Trois variables de condition (cond_move, cond_goal, cond_main) permettent à chaque thread de dormir jusqu'à ce que du travail lui soit destiné, sans attente active.
Quand le serveur reçoit q, il envoie SIGUSR1 à tous les clients encore connectés. Chaque client intercepte ce signal pour arrêter son fils d'affichage, nettoyer son pipe de retour et quitter sans laisser de ressources orphelines. SIGPIPE est ignoré côté client et serveur pour éviter les crashes lors de fermetures inattendues de pipes.
- Le problème du Game Over et de la lecture bloquante : Si le jeu se terminait, le processus d'affichage ou le client pouvait rester bloqué indéfiniment en attente d'une lecture. La solution a été d'utiliser l'option
O_NONBLOCKsur les tubes et de bien manipuler les signaux de fermeture. - L'erreur fatale SIGPIPE : Lorsqu'un client quittait la partie et que le serveur tentait d'écrire la nouvelle grille dans son pipe devenu inexistant, le serveur crashait entièrement. Il a fallu trouver comment ignorer explicitement le signal
SIGPIPE. - La synchronisation des threads sans Deadlock : Passer d'une boucle d'attente active (
while) à une synchronisation fine avecpthread_cond_ta été complexe. Il fallait s'assurer que les threads Move et Goal se réveillent dans le bon ordre sans bloquer le Mutex indéfiniment.
- Le multijoueur simultané : Le serveur gère plusieurs parties en même temps sans aucun lag ni mélange de grilles.
- Zéro gaspillage CPU : Grâce aux variables de condition, les threads s'endorment à 0% d'utilisation processeur lorsqu'ils attendent un coup.
- La gestion des déconnexions brutales (Ghost Buster) : Le serveur vérifie l'état des clients avec
kill(pid, 0). Si un client ferme brutalement son terminal, sa place mémoire est automatiquement libérée. - Fermeture sans fuite mémoire : Le nettoyage de la mémoire partagée (IPC_RMID) et des tubes est systématique. Valgrind affiche 0 erreur et 0 fuite.