Tutorat C/Unix : Serveur de canaux de discussions
Nathalie Devesa & Xavier Redon
1 Description générale du projet
1.1 Objectif
L'objectif du projet est de concevoir et de réaliser un mécanisme rudimentaire
de conférences entre utilisateurs, contrôlable par un administrateur.
Un serveur maintient des canaux de communication auxquels les clients
utilisateurs peuvent s'abonner. En s'abonnant à un canal, un client connecté
au serveur par telnet ou nc, peut envoyer un message sur le canal.
Ce message est immédiatement transmis aux autres abonnés (les messages ne
sont pas mémorisés) et donc, un abonné "voit" tous les messages envoyés sur
le canal auquel il est abonné.
L'administrateur, chargé de la gestion des canaux et des utilisateurs, peut à
tout moment intervenir pour valider ou invalider un canal, espionner les
communications, déconnecter un utilisateur, ....
1.2 Architecture générale de l'application
L'application est constituée de 2 processus : le processus serveur et le
processus gérant. Pour permettre de traiter simultanément les requêtes de
plusieurs clients ainsi que celles de l'administrateur, le serveur doit
être multithreadé. Un thread est lancé pour chaque connexion de client,
ce thread est chargé de satisfaire les requêtes de ce client.
De même, un thread est chargé de traiter d'éventuelles requêtes de
l'administrateur. Pour chacune de ses requêtes, l'administrateur lance
le processus gérant. Celui-ci communique avec le processus serveur par
IPC (Files de messages).
1.3 Les requêtes client
Un client, ayant obtenu une connexion par nc <host> <port> peut
émettre les requêtes suivantes au serveur :
-
lister_canaux :
- lister les titres des canaux valides,
- créer <titre> :
- demande de création d'un nouveau canal nommé
<titre>. Si la demande est acceptée, le canal est créé mais non
validé. Seul l'administrateur peut vraiment le valider,
- quitter :
- déconnexion,
- abonner <n> :
- demande d'abonnement au canal numéro <n>
( le canal doit être valide et le client peut être abonné au plus à un
canal).
Un client abonné à un canal peut effectuer les requêtes suivantes :
-
envoyer <msg> :
- permet d'envoyer un nouveau message sur le
canal,
- lister_abonnés :
- obtenir la liste des clients abonnés au canal,
- censurer <utl> :
- filtrer les messages en lecture ; le client ne
verra plus les messages écrits par <utl>,
- quitter_canal :
- fin d'abonnement.
1.4 Les requêtes 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_canaux :
- lister tous les canaux (valides et non-valides),
- valider <n> :
- valider le canal numéro <n>,
- invalider <n> :
- invalider le canal numéro <n>,
- libérer <n> :
- supprimer le canal numéro <n>,
- lister_utls :
- lister pour chaque canal valide, les clients
abonnés,
- déconnecter <client> :
- terminer la connexion du client,
- scruter_messages :
- espionner les canaux pour obtenir tous les
messages sous la forme <canal> <utl> <msg>. L'espionnage ne se
termine que lorsque l'administrateur envoie le signal SIGINT
en tapant CTRL+C,
- stopper :
- stopper 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).
1.5 Le serveur
Le processus serveur tourne en tâche de fond. Il doit pouvoir traiter les
demandes de connexion des clients, les requêtes des clients connectés et les
requêtes du processus gérant. Pour cela, il lance un thread chargé de traiter
la file de messages dans laquelle sont déposées les requêtes du gérant et un
thread "client" pour chaque connexion sur son port d'écoute. Ce dernier
thread est chargé de traiter les requêtes du client connecté.
2 Organisation du travail
2.1 Généralités
Il est fortement conseillé de suivre les étapes proposées ci-après pour réaliser le travail.
2.2 Organisation modulaire
Le projet est constitué de 3 bibliothèques et des programmes serveur et gérant.
Chacune de ces entités est développée dans un répertoire propre. On gére donc
les 5 répertoires suivants :
-
Socket -
- pour les sources permettant de générer une bibliothèque C
libsck,
- Threads -
- pour la bibliothèque libthrd,
- IPC -
- pour la bibliothèque libipc,
- Demon -
- pour le programme du serveur et les modules de traitement des
commandes client et administrateur,
- Admin -
- pour le programme d'administration.
Cette arborescence et quelques squelettes de fichiers sont disponible sous forme d'un fichier
au format tar et compressé à l'URL
http://www.plil.net/~rex/Enseignement/Systeme/Tutorat.GIS4.canaux/canaux.tgz.
Transférez ce fichier dans votre compte Polytech'Lille et décompressez-le avec la
commande tar xvzf canaux.tgz.
Le répertoire créé contient un fichier Makefile qui est incomplet. Notamment,
la cible all est vide. 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.3 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.
Écrivez 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, void (*)(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 et son type de retour doit être void. Lors
d'une connexion, la fonction boucleServeur lance donc cette
fonction avec la socket de connexion en paramètre.
Testez cette bibliothèque en écrivant un programme servConf (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 <port> ou --port <port>
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://www.plil.net/~rex/Enseignement/Reseau/Reseau.GIS2)
Testez votre serveur avec plusieurs telnet ou nc simultanés.
Conclusions ?
2.4 Un serveur à base de processus légers
Pour que votre serveur puisse accepter plusieurs clients simultanément, vous
allez lancer un processus léger (thread) par client. Pour cela, implémentez la
fonction publique de libthrd (répertoire Threads) :
void lanceClientLeger(int) ;
Cette fonction doit avoir comme action de lancer un thread dans le mode
détaché. Ce thread doit exécuter la fonction
void gestionClient(int) ;
Cette dernière, définie dans servConf.c, est le point d'entrée pour
la gestion d'un client du serveur de conférences. Elle sera implantée plus
tard (sous-section 2.7).
2.5 Structure de données du serveur
Remarque préliminaire : à chaque client connecté est associé un pipe dont
l'utilité sera justifiée ultérieurement.
Les principales informations gérées par le serveur sont :
-
la table des canaux qui, pour chaque canal indique : le titre du canal,
un champ indiquant si le canal est libre, valide ou invalide, le nombre
de clients abonnés et la liste des clients abonnés.
- la table des clients connectés qui, pour chaque client indique le
descripteur de pipe associé au client, le nom de la machine du client
et le nom de connexion du client,
- deux sémaphores d'exclusion mutuelle (de type pthread_mutex) pour
protéger les accès à chacune des tables et les sémaphores de contrôle
nécessaires au bon déroulement des opérations !
Définissez les structures de données correspondantes dans le fichier
entête servconf.h et écrivez les fonctions d'initialisation des tables
(ne vous préoccupez pas encore des sémaphores).
2.6 Analyse des opérations sur la structure 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.a 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 cdesClient.c, les poses et levées de verrous
nécessaires.
2.7 La gestion des clients
La fonction gestionClient crée une nouvelle entrée dans la table des
utilisateurs puis, boucle sur une écoute sur la socket de dialogue, en attente
d'une requête du client. Lorsque le client s'abonne à un canal, il doit alors
recevoir les messages envoyés par d'autres sur ce canal après les avoir
éventuellement filtrés (dans le cas où le client a préalablement censuré
d'autres utilisateurs). Étant donné que l'on ne souhaite pas mémoriser les
messages, un message envoyé sur un canal doit immédiatement être transmis aux
abonnés du canal. Si le message est directement écrit sur la socket de dialogue
des clients abonnés, il n'est plus possible de filtrer les réceptions de
messages. La solution que l'on retiendra consiste à associer un pipe à chaque
client connecté. Envoyer un message sur un canal revient alors à écrire le
message dans chaque pipe de client abonné. Un client abonné est à l'écoute
d'une écriture sur son DP (descripteur de pipe), en règle générale le message
doit être écrit sur le DSD (descripteur de socket de dialogue) du client mais
si l'expéditeur du message est censuré par le client, l'écriture ne doit pas
avoir lieu.
Vous utiliserez la fonction select pour réaliser l'écoute simultanée
sur le DP et le DSD (l'écoute sur le DSD est nécessaire car le client peut
taper des ordres sur son terminal).
2.8 Le processus gérant
Il ne vous reste plus qu'à implanter le processus gérant et le thread chargé
de traiter les requêtes du gérant dans le serveur !
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.