Service POSIX de diffusion de messages en anneau


Le code source complet des programmes

telecharger le code source de service posix


- Le serveur

- Le client


1 – Présentation :

On veut implémenter un service POSIX qui permet à plusieurs clients d'échanger des messages sur le réseau. Le service maintient une structure en anneau. Un serveur doit être créé pour chaque machine afin de gérer les clients locaux et communiquer avec d'autres serveurs pour diffuser ou recevoir des messages. Les serveurs communiquent entre eux à travers les sockets UDP. Les serveurs sont identifiés par leurs adresses IP et sont triés en ordre croissant dans l'anneau. Un serveur à un successeur et un prédécesseur, le successeur du serveur ayant le plus grand identifiant est le serveur ayant le plus petit identifiant, le prédécesseur du serveur ayant le plus petit identifiant est le serveur ayant le plus grand identifiant. Un serveur commence d'abord par envoyer une demande d'insertion dans l'anneau en mode broadcast sur le port 9999, un serveur qui reçoit un broadcast vérifie si le serveur à insérer est un voisin direct (id_prédécesseur < id_entrant < id_local ou id_successeur > id_entrant > id_local), si c'est le cas le serveur envoie une réponse au serveur entrant puis met à jour son chaînage dans l'anneau, de même quand le serveur entrant reçoit la réponse il met à jour son chaînage. Quand un serveur veut quitter l'anneau il envoie un message à ses voisins afin qu'ils mettent à jour leurs chaînages puis attend leurs réponses avant de quitter l'anneau. Les clients communiquent avec leur serveur local à travers un segment de mémoire partagée POSIX. Quand un client désire envoyer un message, il met son message dans le segment de mémoire partagée à destination du serveur local, ce dernier émet le message à son successeur. Le successeur reçoit le message, le diffuse à ses clients locaux puis le transmet à son successeur et ainsi de suite jusqu'à ce que le successeur soit égal au serveur initiateur (celui qui à émis le message). Les clients lisent les messages reçus à partir d'un fichier mis à leur disposition par le serveur.

2 – Analyse du problème :

La réalisation du service décrit ci-dessus pose plusieurs problèmes, en particulier l'insertion d'un serveur dans l'anneau et le détachement d'un serveur de l'anneau. Dans la suite on va essayer d'étudier tous les cas possibles.

Dans toutes les images suivantes, la flèche bleue désigne le successeur et la flèche verte désigne le prédécesseur.

Cas 1 : Un seul serveur

services posix cas 1

Le serveur pointe sur lui-même. Ce cas se présente avant l’insertion du serveur dans l’anneau ou si les voisins du serveur ont quitté l’anneau, du coup ce serveur peut se déconnecter directement.

Cas 2 : Deux serveurs

service posix cas 2

S2 est le successeur et le prédécesseur de S1 et S1 est le successeur et le prédécesseur de S2. Supposant qu’on a au départ le serveur S1 et que S2 veut s’insérer dans l’anneau, il envoie donc une demande de connexion en broadcast qui sera reçue par S1, ce dernier trouve que l’identifiant de S2 est supérieur à son identifiant mais il n’est pas inférieur au successeur de S1 puisque le successeur de S1 est S1 avant l’arrivée de S2 et donc dans ce cas là si S1 n’a pas un successeur et un prédécesseur autre que lui, S2 sera inséré directement dans l’anneau soit après S1 soit avant S1 selon la valeur de l’identifiant de S2. Comme S1 est le seul voisin de S2, si S2 veut quitter l’anneau il n’envoie qu’un seul message à destination de S1 et dès la réception d’une réponse, S2 peut quitter directement l’anneau.

Cas 3 : Trois serveurs ou plus

service posix cas 3

Dans ce cas, le successeur de S3 est S1 car S3 a le plus grand identifiant et le serveur S1 a le plus petit identifiant, de même le prédécesseur de S1 devrait être S3. Quand à S2, son successeur est S3 et son prédécesseur est S1.

Serveur S2 : Au départ on a les serveurs S1 et S3 et S2 veut s’insérer dans l’anneau, il envoie alors une demande de connexion qui sera reçue par S1 et S3. Ces derniers remarquent que S2 est un voisin direct à eux et donc ils envoient une réponse à S2 puis ils mettent à jour leurs chaînages, quand S2 reçoit la réponse il met à jour son chaînage également. Quand S2 veut quitter l’anneau, il envoie un message à ses deux voisins et attend la réponse de ces derniers puis quitte l’anneau.

Serveur S1 : Au départ on a les serveurs S2 et S3 et S1 veut s’insérer dans l’anneau, il envoie alors une demande de connexion en broadcast. Quand S2 reçoit le message de S1, il fait le même traitement que celui du cas 2, par contre le traitement que doit faire S3 est plus délicat car S1 est bel et bien le successeur de S3 (S3 est le serveur ayant le plus grand identifiant et S1 est le serveur ayant le plus petit identifiant). Pour résoudre ce problème S3 doit vérifier que :

  • L’identifiant de S3 est plus grand que l’identifiant de son successeur c'est-à-dire S2, ce cas peut se présenter si l’anneau est composé de deux serveurs seulement. Si S3 n’est pas plus grand que son successeur ça veut dire qu’il n’est pas le serveur ayant le plus grand identifiant (il a déjà un successeur qui a un identifiant plus grand) donc S1 ne peut pas devenir le successeur de S3. 
  • L’identifiant de S1 est plus petit que celui du successeur de S3.

Pour la deuxième condition ça parait logique mais ce n’est pas vrai dans tous les cas ! Dans notre cas la condition est vraie car le successeur de S3 était S2 et S1 est plus petit que S2 mais dans le cas de quatre serveurs ou plus ça peut ne pas marcher, par exemple supposant qu’on a la configuration suivante : X1 < X2 < X3 < X4 et l’anneau actuel est formé de X1 et X3 et X4. X2 veut s’insérer dans l’anneau, X1 ne trouve aucune difficulté pour mettre à jour son chaînage (son successeur devient X2). Quand à X3, la condition une n’est pas satisfaite (X3 < X4). On applique maintenant la condition pour X4 ce qui donne : X2 n’est pas plus petit du successeur de X4 car le successeur de X4 n’est autre que X1 et X2 > X1 d’après l’hypothèse donc X2 ne peut pas être le successeur de X4. En remarque que X2 est inséré correctement dans l’anneau puisque il a fallu juste que X1 modifie son successeur pour mettre X2, les autres serveurs ne sont pas concernés. Avec cette méthode on est sûr d’avoir cerné tous les cas possibles. Maintenant, si S1 veut quitter l’anneau il envoie un message à S3 (son prédécesseur) et à S2 (son successeur) et dès qu’il aura la réponse des deux serveurs, il pourra quitter l’anneau. 

Serveur S3 : On a les serveurs S1 et S2 et S3 veut s’insérer dans l’anneau, on applique le même traitement que celui du serveur S1 sauf que maintenant les conditions à vérifier sont les suivantes :

  • L’identifiant de S1 est plus petit que l’identifiant de son prédécesseur c'est-à-dire S2. 
  • L’identifiant de S3 est plus grand que celui du successeur de S1. 

3 – Architecture de la solution :

Le service est composé de deux programmes : un serveur et un client.

Ressources partagées entre le serveur et le client :

Pour permettre la synchronisation entre le serveur et le client, on utilise deux sémaphores POSIX (de type sem_t) :

  • Un sémaphore sem_req qui permet de débloquer le serveur quand un client fait une requête, on utilise ce sémaphore bien évidemment pour éviter les attentes actives du serveur. 
  • Un sémaphore sem_sync qui permet une fois le serveur à terminé le traitement d’une requête, de débloquer les clients qui sont en attente, ce sémaphore est utile pour pouvoir gérer plusieurs clients en exclusion mutuelle.

Pour la communication entre le client et le serveur, on utilise un segment de mémoire partagée client_message (de type ring_message), ce segment a un descripteur shm_id associé (de type int). La lecture et l’écriture des messages partagés par le serveur et le client se font dans le fichier ring_msg_log qui a comme descripteur fd_file (de type int).

Le programme client :

Variables et structures utilisées :

  • int pid : Pour stocker le résultat de l’appel système fork().
  • char buffer[1024] : Pour lire les messages à partir du fichier ring_msg_log.
  • char msg[1024] : Pour lire les messages à partir de l’entrée standard stdin.
  • struct sigaction act_sigint : Pour associer une action (un handler) à SIGINT.
  • struct sigaction act_sigusr1 : Pour associer une action (un handler) à SIGUSR1.
  • sigset_t ens : Pour le masque des signaux. 

Processus utilisés :

  • Un processus qui boucle en attente de messages sur l’entrée standard stdin et qui envoie les messages lus dans le segment de mémoire partagée client_message à destination du serveur local. 
  • Un processus qui boucle en attente du signal SIGUSR1, ce signal est envoyé régulièrement par le serveur local afin d’informer le processus qu’un nouveau message est arrivé, la fonction void SIGUSR1_handler(int sig) sera exécutée automatiquement.

Fonctions utilisées :

  • void SIGINT_handler(int sig) : Cette fonction permet de traiter le signal SIGINT dès sa réception, elle ferme tous les IPC ouverts puis termine le programme. 
  • void SIGUSR1_handler(int sig) : Cette fonction permet de traiter le signal SIGUSR1, elle lit et elle affiche les nouveaux messages reçus à partir du fichier ring_msg_log
  • int main(int argc, char *argv[ ]) : C’est la fonction principale du programme client, elle commence d’abord par dérouter les signaux SIGINT et SIGUSR1 (modifier le comportement par défaut) puis bloquer les autres signaux. Ensuite elle ouvre les IPC nécessaires et le fichier ring_msg_log puis elle envoie une demande de connexion au serveur local et enfin elle crée les processus décrits plus haut.

Le programme serveur :

Variables et structures utilisées :

  • struct hostent *hp : Pour récupérer des informations sur l’hôte.
  • struct in_addr ad : Pour récupérer l’adresse associée à un hôte. 
  • struct sockaddr_in ex : Adresse de l’expéditeur d’un message. 
  • int sock : L’identifiant associé au socket du serveur local.
  • int k : Compteur du nombre de clients connectés au serveur.
  • struct sigaction act : Pour associer une action (un handler) à SIGINT.
  • pthread_t thread[ ] : Tableau qui contient les identifiants des threads utilisés.
  • struct sockaddr_in dest : Adresse utilisée pour la diffusion en mode broadcast.
  • typedef struct ring_message : Cette structure est utilisée pour la communication entre les serveurs à travers les sockets UDP (insertion dans l’anneau, envoie et réception des messages … etc).
  • typedef struct serv : Pour stocker en permanence divers informations sur le serveur, ses clients et ses voisins (adresses, identifiants).
  • int nb : Lors de l’envoie d’une demande de connexion en mode broadcast, le serveur local reçoit également ce broadcast et donc on utilise cette variable pour éviter de traiter notre propre message émis.
  • int ok : Cette variable est utilisée pour vérifier s’il y a des serveurs connectés sur le réseau ou non.

Threads utilisés :

  • void *req_client(void *args) : Ce thread permet de recevoir et traiter les requêtes des clients, il boucle toujours en attendant l’arrivée d’une requête en se bloquant sur le sémaphore sem_req, ensuite il vérifie le type de la requête reçue, s’il s’agit d’une demande de connexion alors il enregistre le pid du client (le pid du processus qui reçoit les messages) dans le tableau pid_client_recepteur[ ] et il incrémente la variable k (décrite plus haut). S’il s’agite d’une demande d’envoie d’un message, le thread commence par diffuser le message aux clients locaux avant de l’envoyer à son successeur. Enfin dans les deux cas, à la fin du traitement le serveur débloque les clients en attente sur le sémaphore sem_sync
  • void *req_server(void *args) : Ce thread permet de gérer toutes les requêtes à destination ou en provenance des autres serveurs. Il boucle infiniment en attendant l’arrivée d’un message sur son socket local. Quand un message est reçu, le thread fait un traitement spécifique selon le type du message :

        - START_REQUEST : Le thread vérifie si le serveur entrant est un voisin direct
        du serveur local, si c’est le cas alors le thread envoie une réponse au serveur entrant
        et il met à jour le chaînage du serveur local.

        - START_REPLY : Le thread met à jour le chaînage du serveur local en se basant
        sur les données reçues par le serveur voisin.        

        - LEAVE_REQUEST : Le thread met à jour la référence obsolète puis il envoie une
        réponse au serveur qui veut quitter l’anneau.

        - APP_MSG : Le thread écrit le message reçu sur le fichier ring_msg_log puis il
        envoie un signal SIGUSR1 aux clients locaux pour les informer qu’il y a un
        nouveau message à lire. Ensuite le thread transmet le message à son successeur si
        ce dernier est différent du serveur initiateur (émetteur).

Fonctions utilisées :

  • void SIGINT_handler(int sig) : Fonction qui traite le signal SIGINT, elle commence d’abord par fermer le fichier ring_msg_log puis annuler les threads en cours d’exécution. Ensuite, elle calcule le nombre de voisins du serveur local (0, 1 ou 2) en suivant cet algorithme : Si le successeur ou le prédécesseur du serveur local est lui-même ça veut dire que ce serveur n’a aucun voisin puisque il pointe sur lui-même, si le successeur est différent du serveur local mais égal au prédécesseur ça veut dire que le serveur local a un seul voisin, sinon le serveur a deux voisins. Une fois le calcul des voisins fait, elle envoie une demande de détachement aux voisins du serveur. Enfin la fonction ferme le socket du serveur local et les IPC ouverts. 
  • void print_id() : Cette fonction affiche l’identifiant du serveur et ceux de ses voisins. 
  • int main(int argc, char *argv[ ]) : C’est la fonction principale du programme serveur. Cette fonction commence par ouvrir les IPC nécessaires et créer le fichier ring_msg_log puis modifier le comportement par défaut du signal SIGINT. Ensuite elle récupère l’adresse IP du serveur local puis elle ouvre le socket sock et elle autorise la diffusion en mode broadcast. Après, elle fait le nommage et elle crée les threads décrits plus haut (2 threads en tout) et elle envoie un message en mode broadcast pour l’insertion du serveur local dans l’anneau. Enfin elle attend la terminaison des threads créés (qui ne se terminent jamais d’ailleurs, la réception d’un signal SIGINT entraîne l’arrêt du programme serveur).