Tutorat C/Unix : Un Rapido Client/Serveur
Nouredine Melab
1 Description générale du projet
1.1 Objectif
L'objectif du projet est de concevoir et de réaliser un jeu de hasard dénommé
Rapido. Un serveur effectue un tirage aléatoire de nombres toutes les 5
minutes, et maintient la liste des tirages effectués dans la journée. Un joueur
(client) connecté au serveur par nc, peut participer au jeu, consulter
les tirages effectués depuis le matin, et/ou vérifier ses gains à l'aide d'un reçu.
1.2 Description du jeu
1.2.1 Tirages du jeu
Les tirages du Rapido sont effectués par un serveur informatique, par
désignation au hasard de 8 numéros différents parmi 20 numéros possibles
chiffrés de 1 à 20 (grille A) et de 1 numéro parmi 4 numéros possibles chiffrés
de 1 à 4 (grille B). Environ 250 tirages sont réalisés chaque jour. Les
résultats des tirages de la journée sont stockés sur le serveur et sont, au fur
et à mesure, affichés à l'écran du serveur. Les joueurs peuvent, à tout moment
de la journée, consulter les tirages précédemment effectués.
1.2.2 Prises de jeu et mises
Un joueur peut participer à un ou plusieurs tirage(s), soit en utilisant un
bulletin de prise de jeu, soit en ayant recours au système Flash de génération
aléatoire de combinaisons.
Si le joueur choisit la prise de jeu par bulletin, il doit renseigner lui-même
les grilles A et B. Il doit donc indiquer 8 numéros parmi 20 dans la grille A,
et de 1 à 4 numéro(s) dans la grille B. Au contraire, si le joueur choisit le
système Flash, les numéros sont tirés au hasard par le serveur. Dans ce cas, il
doit indiquer au serveur le nombre de numéros à tirer dans B.
Dans les deux cas de prise de jeu, le joueur doit également choisir le nombre
total de tirages auxquels il participe à compter du prochain tirage, et la mise
par tirage. Pour chaque numéro choisi dans la grille B, la mise par tirage est,
au choix du joueur, de 1 euro, 2 euros, 3 euros, 5 euros ou 10 euros. Par
conséquent, si le joueur choisit 2, 3 ou 4 numéros dans B le montant de la mise
est multipliée respectivement par 2, 3 ou 4. D'autre part, le nombre de tirages
choisi ne doit pas dépasser le nombre de tirages restant à effectuer jusqu'à la
fin de la journée.
1.2.3 Reçus de jeu
Après enregistrement d'un jeu, pris par bulletin ou par Flash, un reçu est
renvoyé au joueur. Le reçu contient notamment : le type de prise de jeu
(bulletin ou Flash), les numéros de la grille A, le(s) numéro(s) de la grille
B, le nombre de tirages auxquels participe le joueur, le numéro de tirage de
debut, la mise par tirage, la mise totale, la date et le numéro du reçu. Le
joueur peut interroger le serveur pour vérifier ses gains par simple
communication de l'identifiant du reçu.
1.2.4 Calcul de gains
Suivant la somme d'argent misée et les numéros trouvés dans A et B pour un
tirage donné, différents rangs de lots gagnants sont définis dans le tableau
ci-dessous. La dernière colonne du tableau indique le facteur par lequel on
multiplie la mise pour trouver le gain du joueur. Par exemple, si pour un
tirage donné, 7 numéros sont trouvés dans A et le (seul) numéro choisi dans B
est trouvé (ligne correspondant au rang 3), le lot pour une mise respectivement
de 1 euro, 2 euros, 3 euros, 5 euros et 10 euros, est de 150 euros, 300 euros,
450 euros, 750 euros et 1500 euros.
Rang |
Bons numéros dans A |
Bons numéros dans B |
Facteur multiplicatif |
1 |
8 |
1 |
10000 |
2 |
8 |
0 |
1000 |
3 |
7 |
1 |
150 |
4 |
7 |
0 |
50 |
5 |
6 |
1 |
30 |
6 |
6 |
0 |
10 |
7 |
5 |
1 |
6 |
8 |
5 |
0 |
2 |
9 |
4 |
1 |
1 |
1.3 Espérance de gain
Pour votre culture mathématique et pour vérifier que la Française des jeux ne laisse
pas grand chose au hasard, on va faire une étude statistique de l'espérance de gain
au Rapido. Dans la suite de la sous-section on ne considère que le cas d'un numéro
coché dans la grille B; les cas avec plusieurs numéros cochés dans la grille B
revenant à jouer plusieurs bulletins. L'espérance est calculée pour chaque rang :
-
Cas des 8 numéros trouvés avec le complémentaire :
- Cas des 8 numéros trouvés sans le complémentaire :
- Cas des 7 numéros trouvés avec le complémentaire :
(pour avoir 7 numéros gagnants, il faut tirer 7 numéros parmi les 8 numéros
gagnants et 1 numéro parmi les 12 numéros non gagnants)
- Cas des 7 numéros trouvés sans le complémentaire :
- Cas des 6 numéros trouvés avec le complémentaire :
- Cas des 6 numéros trouvés sans le complémentaire :
- Cas des 5 numéros trouvés avec le complémentaire :
- Cas des 5 numéros trouvés sans le complémentaire :
- Cas des 4 numéros trouvés avec le complémentaire :
En faisant la somme des espérances de gain par rang on trouve quelque
chose comme : E = 0.6592045723.
En conclusion, l'espérance de gain est d'environ 2/3; un joueur
perd donc en moyenne 1/3 du montant de sa mise par tirage.
1.4 L'architecture de l'application
L'application est constituée d'un processus serveur multithreadé et d'un processus gérant. Le
serveur et le gérant communiquent par IPC, les joueurs se connectent au serveur par nc.
1.5 Les requêtes du client
Un client, ayant obtenu une connexion par nc <host> <port> peut émettre les
requêtes suivantes au serveur :
-
afficher_regles_du_jeu : afficher les règles du jeu Rapido.
- jouer_un_flash <nB> <nbT> <mise> : jouer en utilisant le système Flash.
Les arguments <nB>, <nbT> et <mise> désignent respectivement
le nombre de numéros choisis dans la grille B, le nombre de tirages auxquels
le joueur participe à compter du prochain tirage et la mise par tirage.
Le joueur doit recevoir un reçu de type Flash.
- jouer_un_bulletin <nbT> <mise> <numeros_grilleA> <numeros_grilleB> :
jouer en utilisant un bulletin de prises de jeu. Les arguments <nbT>,
<mise>, <numeros_grilleA> et <numeros_grilleB> désignent
respectivement le nombre de tirages auxquels le joueur participe à compter
du prochain tirage, la mise par tirage, 8 numéros choisis dans la grille A
et un ou plusieurs numéros choisis dans la grille B. Le joueur doit recevoir
un reçu de type bulletin.
- consulter_tirages : consulter les tirages de la journée effectués
précédemment. Pour chaque tirage, le serveur renvoit le numéro du tirage,
les 8 numéros de la grille A et le numéro de la grille B.
- verifier_recu <id_recu> : vérifier ses gains en donnant l'identifiant
<id_recu> d'un reçu.
- quitter : quitter le jeu en se déconnectant.
1.6 Le serveur
Le processus serveur tourne en tâche de fond. Il doit pouvoir traiter les
demandes de connexion des joueurs et les requêtes des joueurs connectés. Pour
cela, il lance un thread "tirages" qui effectuera les tirages. Il lance
également un thread "joueur" pour chaque connexion sur son port d'écoute,
chargé de traiter les requêtes du joueur connecté.
1.7 Le processus gérant
Le processus gérant a pour rôle de "superviser" le jeu. Il permet à un
administrateur d'envoyer des requêtes au serveur pour "espionner" les prises de
jeu, pour déconnecter un joueur, etc. La communication entre le gérant et le
serveur est basée sur le mécanisme d'IPC.
2 Méthodologie de développement
Afin de faciliter votre travail, il est fortement conseillé de suivre les
étapes proposées ci-après pour réaliser le travail.
2.1 Organisation modulaire
Le projet est initialement constitué de 2 bibliothèques et du programme serveur.
Chacune de ces entités est développée dans un répertoire propre. On gère donc les 3
répertoires suivants :
-
Socket :
- pour les sources permettant de générer une bibliothèque C libsck.
- Threads :
- pour la bibliothèque libthrd.
- Demon :
- pour le programme du serveur et les modules de traitement des
requêtes des joueurs.
Cette arborescence et les sources sont disponibles sous forme d'un fichier au
format tar et compressé à l'URL
http://rex.plil.fr/Enseignement/Systeme/Tutorat.GIS4.rapido/rapido.tgz.
Transférez ce fichier dans votre compte
Polytech'Lille et décompressez-le avec la commande tar xvzf rapido.tgz.
Le répertoire créé contient un fichier Makefile qui est incomplet. Notamment, la
cible all et la variable DIRS sont vides. Complétez afin que, lorsque l'on
lance make, tous les Makefile qui se trouvent dans les différents répertoires
soient exécutés.
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.
2.2 Sockets et serveur
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 libsck.c (répertoire Socket), 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.
- int boucleServeur(int, int (*)(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, la fonction boucleServeur lance donc cette fonction
avec la socket de dialogue en paramètre.
Testez cette bibliothèque en écrivant un programme serveur (répertoire Demon)
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.
Ce programme peut prendre des arguments : -p <numPort> ou --port <numPort>
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 que le serveur affiche
le nom de la machine distante (sur la sortie standard) et qu'elle ne ferme la
connexion que si elle lit le mot-clé quitter sur la socket. Pour ceci,
écrivez dans libsck une fonction SocketVersNom qui prend une socket
en argument et renvoie le nom dans une chaîne de caractères. Vous utiliserez les
fonctions getpeername et gethostbyaddr (voir les pages de manuel et le
support du cours de réseau http://rex.plil.fr/Enseignement/Reseau/Reseau.GIS4).
Testez votre serveur avec plusieurs nc simultanés. Conclusion ?
2.3 Un serveur à base de processus légers
Pour que votre serveur puisse accepter plusieurs joueurs simultanément, vous
allez lancer un processus léger (thread) par joueur. Pour cela, implémentez la
fonction publique de libthrd (répertoire Threads) :
void creerThreadJoueur(int) ;
Cette fonction doit avoir comme action de lancer un thread dans le mode
détaché. Ce thread doit exécuter la fonction :
void gestionJoueur(int) ;
Cette dernière, définie dans gestionJoueur.c (répertoire Demon), est le point
d'entrée pour la gestion d'un client du serveur de jeu. Elle sera implantée plus tard
(sous-section 2.7). Pour les tests vous pouvez la remplir avec le
code de la fonction de traitement des connexions de la sous-section précedente.
2.4 Structures de données du serveur
Le serveur gère principalement trois tables, dont le contenu est décrit ci-dessous :
-
Table des tirages :
- chaque entrée de la table contient des informations
concernant un tirage donné, notamment : le numéro du tirage, les numéros
tirés dans la grille A, le numéro tiré dans la grille B.
- Table des joueurs :
- cette table indique pour chaque joueur connecté : le
descripteur socket qui lui est associé, le nom de sa machine et son nom
de connexion.
- Table des reçus :
- chaque reçu correspondant à une prise de jeu est décrit
dans cette table par : son identifiant (composé de la date du jour et un
numéro de séquence), le type du reçu (bulletin ou Flash), le nombre de
tirages pour lesquels le reçu est valable, le numéro de début de ces
tirages, les numéros choisis/tirés dans la grille A, les numéros choisis/
tirés dans la grille B, et la mise par tirage.
Ecrivez les fonctions d'initialisation des tables (ne vous préoccupez pas
encore des sémaphores).
2.5 Synchronisation des opérations sur les structures de données
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. Ajoutez dans le module gestionJoueur.c,
les poses et levées de verrous nécessaires.
2.6 Gestion des tirages
Les tirages sont effectués dans le serveur par un processus léger. Implémentez
la fonction publique suivante de la bibliothèque libthrd (Répertoire Threads).
void creerThreadTirages(int);
Cette fonction permettra de lancer un thread dans le mode détaché. Ce thread
aura comme tâche d'exécuter la fonction :
void gestionTirages(int);
Cette fonction exécute une boucle, à chaque itération elle effectue un tirage
qu'elle enregistre dans la table des tirages, puis l'affiche à l'écran. Entre
deux tirages, le thread s'endort pendant 30 secondes (et non pas 5 minutes pour
pouvoir effectuer des tests plus rapidement).
2.7 Gestion des joueurs
La fonction gestionJoueur crée une nouvelle entrée dans la table des joueurs
puis, boucle sur une écoute sur la socket de dialogue, en attente d'une requête
du joueur.
Lorsqu'un joueur joue un Flash ou un bulletin, le thread associé au joueur crée
une nouvelle entrée dans la table des reçus. Une copie du reçu ainsi ajouté est
envoyée au joueur via sa socket.
2.8 Gestion des reçus
La gestion des reçus se traduit par la gestion de la table des reçus. Cela
dépend de l'endroit où est stockée la table. Vous allez considérer deux
versions : la version où la table est stockée en mémoire centrale ; et la
version où la table est stockée sur disque.
2.8.1 Reçus en mémoire centrale
La table des reçus est mise à jour par les threads joueurs lors du traitement
des requêtes de prise de jeu par bulletin ou par Flash (jouer_un_bulletin et
jouer_un_flash), et des requêtes de vérification des gains (verifier_recu).
Les accès concurrents à la table des reçus doivent être synchronisés au moyen des
sémaphores de la sous-section 2.5.
2.8.2 Reçus sur disque
Cette version permet de pallier les problèmes de pannes du serveur. Pour
augmenter l'interactivité du serveur et assurer le recouvrement des E/S, les
requêtes de mise à jour de la table des reçus ne sont plus gérées par les
threads joueurs eux-mêmes. Ainsi, chaque thread joueur crée un autre thread,
avec lequel il communique par tube, pour gérer les requêtes de mise à jour
des reçus sur disque. Modifier la fonction gestionJoueur pour lancer le
thread en question en mode détaché. Pour cela, implémentez la fonction publique
suivante de la bibliothèque libthrd (Répertoire Threads).
void creerThreadES(int, int);
Cette fonction permettra de lancer un thread dans le mode détaché. Les deux
arguments désignent respectivement le descripteur de socket de dialogue avec le
joueur et le descripteur du tube de communication entre les deux threads. Le
thread lancé aura comme tâche d'exécuter la fonction :
void gestionES(int, int);
Cette fonction boucle en attente de requêtes d'ES en provenance du thread
joueur. Les réponses sont directement envoyées au joueur via sa socket
attitrée.
3 Le processus gérant
3.1 Objectif
Une fois le serveur de jeu mis en oeuvre, on se propose d'implanter un
processus gérant, chargé de la gestion des joueurs et de la ``supervision'' du
jeu. En lançant ce processus gérant, l'administrateur peut à tout moment
intervenir pour espionner les prises de jeu, déconnecter un joueur, .... Le
processus gérant communique avec le processus serveur par IPC (Files de
messages). Dans le serveur, un thread est chargé de traiter d'éventuelles
requêtes de l'administrateur.
3.2 Les requêtes de l'administrateur
L'administrateur lance le processus gérant chaque fois qu'il veut effectuer une
requête (la requête est fournie en argument). Dans un premier temps, on suppose
que les requêtes sont effectuées séquentiellement, c'est à dire que
l'administrateur ne lance qu'un processus gérant à la fois. Les requêtes que
peut effectuer l'administrateur sont les suivantes:
-
lister_joueurs : afficher la liste des joueurs actuellement connectés.
- deconnecter <joueur> : Déconnecter le joueur dont le nom est <joueur>.
- scruter_recus : espionner les prises de jeu pour obtenir toutes les
prises de jeu sous la forme <numeros_grilleA> <numeros_grilleB>
type_prise. Les arguments désignent respectivement les numéros joués dans
la grille A, le(s) numéro(s) joué(s) dans la grille B et le type de prise
de jeu (bulletin ou Flash). L'espionnage ne se termine que lorsque
l'administrateur envoie le signal SIGINT en tapant CTRL-C.
- arreter_serveur : arrêter le serveur.
- aide : obtenir la liste des requêtes possibles.
Tout dialogue nécessaire entre le processus serveur et le processus gérant est
réalisé par des communications IPC (envoi et réception de messages).
3.3 Organisation modulaire
Pour garder l'organisation modulaire de votre projet, vous utiliserez les deux
répertoires :
-
Ipc : pour la bibliothèque libipc.
- Admin : pour le programme d'administration.
3.4 Réalisation
Les commandes sont passées en argument au processus gérant (donc pas
d'interface textuelle, mais une analyse des arguments au moyen de getopt_long).
Ce processus communique avec le serveur au moyen de 2 files de messages : une
pour les commandes et une pour les réponses. La file des commandes est créée
par le serveur à l'initialisation. Un thread, lancé par le serveur est chargé
de scruter en permanence cette file. La file des réponses est créée par le
gérant. La bibliothèque libipc contiendra toutes les fonctions permettant de
cacher le fait que la communication est implantée par IPC (selon le même
principe que celui utilisé pour les sémaphores).
This document was translated from LATEX by
HEVEA.