projet-multitaches/rapport.tex

327 lines
20 KiB
TeX
Raw Normal View History

2022-11-05 12:43:50 +01:00
\documentclass[a4paper,french,12pt]{article}
\title{%
\includegraphics[width=0.25\linewidth]{./img/efrei-logo.jpg}
\vfill
Projet de programmation Multitâches \\
{\Large Application de messagerie instantanée}
\vfill
}
\author{Adam Belghith, Tunui Franken, Maroua Ombaya}
\date{\small Dernière compilation~: \today{} à \currenttime}
\newcommand{\versionnumber}{1.0}
\usepackage{rapport}
\begin{document}
\maketitle
\thispagestyle{empty}
\clearpage
\tableofcontents
\clearpage
2022-11-05 14:31:29 +01:00
\section{Description du projet, objectifs}
Cette application multitâches, codée en C, est une illustration sur un cas concret des problématiques liées à~:
\begin{itemize}
\item la synchronisation d'une application client/serveur
\item l'exclusion mutuelle
\item l'interblocage de processus
\end{itemize}
Il s'agit d'une application de messagerie instantanée qui permet à plusieurs utilisateurs de communiquer par l'intermédiaire d'un serveur.
2022-11-05 16:18:24 +01:00
Le code source peut être trouvé ici~: \\
\url{https://git.tunuifranken.info/efrei/projet-multitaches}.
2022-11-05 16:35:36 +01:00
\section{Choix du langage}
Nous avions le choix entre C et Java pour le code de cette application.
Le langage C, plus bas niveau, nous a paru intéressant pour deux raisons~:
\begin{enumerate}
\item Il permet de savoir précisément ce que l'on fait et donc de contrôler d'avantage de choses.
Il est également plus dangereux, dans le sens ou il ne pardonne pas les erreurs de gestion de mémoire ou d'implémentation.
La gestion des erreurs a été une fin en soi.
\item D'un point de vue pédagogique, l'absence d'objets et de fonctions de très haut niveau nous a obligés à prendre en considération un grand nombre de détails nécessaires à l'implémentation d'une telle application.
\end{enumerate}
2022-11-05 14:31:29 +01:00
\section{Architecture fonctionnelle}
Le projet est composé de deux exécutables~:
\begin{enumerate}
\item \texttt{server}, qui utilise plusieurs threads~:
\begin{itemize}
\item \texttt{Dispatcher}~: gère le parsing des messages et le routage vers les bonnes fonctions et les bons clients.
\item \texttt{ClientListener}~: est à l'écoute des messages entrants.
\item \texttt{ClientSender}~: envoie les messages au client.
\end{itemize}
Le serveur crée une instance de \texttt{ClientListener} et une instance de \texttt{ClientSender} par client connecté.
\item \texttt{client}, composé de deux processus~:
\begin{itemize}
\item \texttt{Listener}~: est à l'écoute des messages en provenance du serveur, les imprime sur la sortie standard.
\item \texttt{Sender}~: lit les messages sur l'entrée standard et les envoie au serveur.
\end{itemize}
\end{enumerate}
Les fonctionnalités de haut niveau peuvent être schématisées de la manière suivante~:
\begin{center}
\begin{tikzpicture}
\node[rectangle,fill=purple!20,thick,text depth=2cm,text width=2cm] (client1) at (-5,1) {\large Client$_1$};
\node[text width=2cm,align=right] (sender1) at (-5,0.5) {Sender};
\node[text width=2cm,align=right] (listener1) at (-5,0) {Listener};
\node[rectangle,fill=purple!20,thick,text depth=2cm,text width=2cm] (clientn) at (-5,-3) {\large Client$_n$};
\node[text width=2cm,align=right] (sendern) at (-5,-3.5) {Sender};
\node[text width=2cm,align=right] (listenern) at (-5,-4) {Listener};
\node[rectangle,fill=blue!20,thick,text depth=7cm,text width=8.8cm] (server) at (3,-1) {\large Serveur};
\node (clientlistener1) at (0,0.5) {ClientListener$_1$};
\node (clientsendern) at (0,-4) {ClientSender$_n$};
\node[rectangle,draw] (messagebuffer) at (5,0.5) {MessageBuffer};
\node[rectangle,draw] (dispatcher) at (5,-1.5) {\parbox{4cm}{%
Dispatcher
\begin{itemize}
\item parse message
\item get client$_n$
\end{itemize}
}};
\node[rectangle,draw] (clientpipen) at (5,-4) {ClientPipe$_n$};
\draw[-latex] (sender1) -- (clientlistener1) node[above,midway]{\small send};
\draw[-latex] (clientlistener1) -- (messagebuffer) node[above,midway]{\small push};
\draw[-latex] (dispatcher) -- (messagebuffer) node[right,midway]{\small pop};
\draw[-latex] (dispatcher) -- (clientpipen) node[right,midway]{\small push};
\draw[-latex] (clientsendern) -- (clientpipen) node[above,midway]{\small pop};
\draw[-latex] (clientsendern) -- (listenern) node[above,midway]{\small send};
\end{tikzpicture}
\end{center}
2022-11-05 12:43:50 +01:00
2022-11-05 16:18:24 +01:00
\section{Architecture technique}
\subsection{Les clients}
Un client doit essentiellement gérer deux tâches parallèles~: le \texttt{Sender} et le \texttt{Listener}.
2022-11-05 16:35:36 +01:00
Le client est donc composé de quelques fonctions nécessaires pour cela~:
2022-11-05 16:18:24 +01:00
\paragraph{\texttt{main}}
Point d'entrée de tout programme C, la fonction \texttt{main} commence par vérifier les arguments passés en ligne de commande.
Ensuite, elle ignore les signaux d'interruption, puisqu'ils seront gérés dans les processus fils.
Puis un socket est créé pour la communication avec le serveur. \\
Le prompt est instancié dans un espace de mémoire partagée (voir~\ref{shared-memory}). \\
Le \texttt{main} crée alors un sémaphore nommé (voir~\ref{named-semaphore}).
Pour éviter que différents clients éventuellement lancés sur la même machine partagent le même sémaphore, l'identifiant du client est alors récupéré pour être utilisé dans ce nom. \\
C'est aussi le moment de récupérer le nom du client (qui par défaut vaut l'ID), pour l'affecter au prompt. \\
Il ne reste plus qu'à faire deux \texttt{fork} successifs, pour le \texttt{Listener} et pour le \texttt{Sender}.
Le \texttt{main} pourra alors terminer son exécution quand les deux processus auront terminé.
2022-11-05 16:35:36 +01:00
Pour cela, un premier \texttt{waitpid} est lancé et va récupérer le numéro du processus ayant retourné en premier.
2022-11-05 16:18:24 +01:00
En fonction du processus retourné, l'autre est alors tué. \\
Avant de terminer l'exécution du \texttt{main}, une fonction est lancée pour quitter proprement le programme.
\paragraph{\texttt{Listener}}
Cette fonction regroupe toutes les actions réalisées par le processus fils correspondant au \texttt{Listener}.
On commence par la gestion des signaux d'interruption \texttt{SIGINT} et \texttt{SIGTERM}.
Le handler appelé à la réception de ces signaux est décrit par la fonction \texttt{interrupt} (voir plus bas). \\
Le reste de la fonction consiste en une boucle infinie.
On lit caractère par caractère ce qui arrive sur le socket créé dans \texttt{main}.
Pour cela, on commence par une lecture d'un caractère, sachant que la fonction est bloquante.
Dès que quelque chose est reçu, le sémaphore est bloqué (\texttt{sem\_wait}) et on démarre une boucle de lecture jusqu'à un caractère de fin.
Chaque caractère lu est analysé puis envoyé sur \texttt{stdout}.
Le caractère \texttt{EOT} entraîne une fermeture de session, et le caractère \texttt{ETX} indique une fin de flux, ce qui déclenche la mise à jour du prompt et son impression.
Le sémaphore est alors libéré (\texttt{sem\_post}) et la boucle infinie redémarre. \\
L'utilisation de deux appels distints à \texttt{recv} a été rendu nécessaire par l'utilisation du sémaphore.
Le premier, bloquant, doit être fait avant le \texttt{sem\_wait}, sinon le \texttt{Listener} s'approprie le sémaphore aussi longtemps qu'il en a l'occasion.
Le second appel à \texttt{recv} est rendu non bloquant pour permettre de libérer le sémaphore en fin de flux.
\paragraph{\texttt{Sender}}
Tout comme pour le \texttt{Listener}, la fonction \texttt{Sender} est exécutée par le processus fils correspondant au \texttt{Sender}. \\
2022-11-05 16:35:36 +01:00
Nous avons le même système de gestion des signaux qui permettra de quitter proprement l'application. \\
2022-11-05 16:18:24 +01:00
La boucle infinie est ici beaucoup plus simple que celle du \texttt{Listener}.
On commence par bloquer le sémaphore pour pouvoir écrire le prompt, puis le libérer tout de suite après.
Cela peut paraître court, mais il s'agit essentiellement de ne pas interrompre l'écriture du \texttt{Listener}. \\
La suite consiste simplement à lire une ligne complète (terminant par `\verb+\n+') grâce à \texttt{fgets}, puis de l'envoyer au serveur grâce au socket.
\paragraph{\texttt{handle\_args}}
Cette fonction permet d'arrêter très tôt l'exécution de notre programme si les arguments passés sont incorrects.
Dans ce cas, une simple explication de l'invocation en ligne de commande est affichée~:
\begin{lstlisting}[gobble=16]
usage: ./client <server_hostname|server_ip> [<server_port>]
\end{lstlisting}
La fonction \texttt{handle\_args} permet également de renvoyer à \texttt{main} le port à utiliser pour le socket.
En effet, il est possible de fournir ce numéro de port en \texttt{CLI} ou de l'omettre pour utiliser le port par défaut.
\paragraph{\texttt{clean\_and\_close}}
Cette fonction est simplement appelée à la toute fin de la fonction \texttt{main} afin de permettre une fermeture propre de la session.
La gestion des signaux (ignorés par le processus père et gérés seulement dans les fils) permet de s'assurer de l'exécution de cette fonction de nettoyage dans tous les cas. \\
Elle ferme le sémaphore, informe le serveur de la déconnexion (\texttt{cmd\_bye}), fait un \texttt{munmap} pour libérer la mémoire partagée pour le prompt, puis ferme le socket.
\paragraph{\texttt{interrupt}}
À part afficher un message de debug lors de la réception des signaux \texttt{SIGINT} et \texttt{SIGTERM}, cette fonction ne fait pas grand chose.
2022-11-05 16:35:36 +01:00
Elle est surtout utile dans la mesure ou la structure \texttt{sigaction} permet de n'appeler ce handler que dans le processus fils, le père ignorant les signaux pour éviter de mettre fin à la session trop tôt.
2022-11-05 16:18:24 +01:00
\paragraph{\texttt{cmd\_hello}}
Les fonctions préfixées par \texttt{cmd\_} correspondent à des commandes, ici des commandes internes (mais qui peuvent très bien être appelées par l'utilisateur). \\
La commande \texttt{cmd\_hello} est utilisée pour deux choses~:
\begin{enumerate}
\item Elle permet au client de recevoir son identifiant, utile pour la création du sémaphore.
\item Elle informe le serveur de l'arrivée du client, lui permettant ainsi de l'enregistrer dans sa base.
\end{enumerate}
\paragraph{\texttt{cmd\_bye}}
Cette commande consiste simplement à envoyer le caractère correspondant à la commande \texttt{CMD\_BYE} au serveur, lui permettant de libérer les informations de ce client dans sa base.
\paragraph{\texttt{cmd\_get\_name}}
La commande \texttt{cmd\_get\_name} demande au serveur de renvoyer le nom d'utilisateur du client.
Ce nom, qui peut être amené à changer pendant la session, est stocké et géré par le serveur. \\
Le client en a simplement besoin pour l'afficher dans le prompt.
\subsubsection{Mémoire partagée}\label{shared-memory}
Le prompt utilisé par le client est modifié par le processus \texttt{Listener} et accédé par le processus \texttt{Sender}.
Pas besoin d'exclusion mutuelle donc, mais l'espace mémoire pour cette chaîne de caractères doit être partagée.
\subsection{Le serveur}
2022-11-05 17:09:29 +01:00
Le serveur est beaucoup plus complexe que le client.
Commençons par une définition des différents éléments qui entrent en jeu.
\paragraph{\texttt{MessageBuffer}}
Le \texttt{MessageBuffer} est un pipe global, utilisé par le \texttt{Dispatcher} et tous les \texttt{ClientListener}.
Il permet d'implémenter un buffer géré par le kernel.
2022-11-06 08:52:39 +01:00
Ce buffer contient les messages lus par le \texttt{ClientListener} et qui seront récupérés par le Dispatcher pour traiter les messages un à un.
\paragraph{\texttt{ClientPipe}}
Le \texttt{ClientPipe} est un pipe qui permet la communication entre le \texttt{Dispatcher} et les différents \texttt{ClientSender}.
Chaque instance de \texttt{ClientSender} a accès en lecture au pipe affecté à son client.
Le \texttt{Dispatcher}, quant à lui, utilise la liste globale des clients pour écrire dans le pipe du bon client.
\paragraph{Liste des clients (\texttt{struct client\_details})}
La liste des clients est une liste globale qui contient pour chaque client les informations le concernant.
Pour cela, un \texttt{struct} contient les éléments suivants pour représenter un client~:
\begin{itemize}
\item \texttt{id}~:
L'identifiant du client, qui doit être égal à l'index de la structure dans la liste.
Ceci permet d'accéder à un client particulier en indexant simplement son \texttt{id} et d'affecter rapidement un nouveau client à l'index libre le plus bas.
Ce champ vaut -1 par défaut, quand la place est libre.
\item \texttt{name}~:
Le nom d'usage du client, vide par défaut.
\item \texttt{sockid}~:
Le socket de communication avec le client.
\item \texttt{pipe}~:
Instance de \texttt{ClientPipe} pour la communication entre le \texttt{Dispatcher} et le \texttt{ClientListener} de ce client.
\item \texttt{listener\_thread}~:
Le numéro du thread créé pour le \texttt{ClientListener}.
Il est utilisé pour permettre la déconnexion d'un client en particulier en mettant fin au thread.
\item \texttt{sender\_thread}~:
Le numéro du thread créé pour le \texttt{ClientSender}.
Il est utilisé pour permettre la déconnexion d'un client en particulier en mettant fin au thread.
\end{itemize}
La longueur de cette liste est prédéfinie grâce à la constante \texttt{MAX\_CLIENTS}.
\paragraph{Liste des groupes (\texttt{struct group\_details})}
La liste des groupes et aussi une liste globale du même type que la liste des clients.
Sa longueur est définie à \texttt{MAX\_GROUPS} et chaque \texttt{struct} la composant contient les éléments suivants~:
\begin{itemize}
\item \texttt{id}~:
L'identifiant du groupe, qui fonctionne de la même façon que pour la liste des clients.
\item \texttt{name}~:
Le nom affecté au groupe lors de sa création.
\item \texttt{clients}~:
Une liste de longueur \texttt{MAX\_CLIENTS}, qui est une table de vérité.
Chaque index dans cette liste identifie un client, et le booléen indique la présence ou non du client dans le groupe.
Ceci permet de ne garder aucune information de groupe dans la liste des clients, et de vérifier très rapidement si un client fait partie d'un groupe, sans parcourir de liste.
\end{itemize}
2022-11-05 17:09:29 +01:00
\paragraph{Mutex des \texttt{ClientListener}}
Comme les \texttt{ClientListener} de chaque client peuvent à tout moment écrire dans le \texttt{MessageBuffer}, un mutex global est créé.
\paragraph{\texttt{ClientListener}}
2022-11-06 08:52:39 +01:00
Le \texttt{ClientListener} est un thread qui gère la réception des messages en provenance du client associé.
Chaque message reçu est transmis au \texttt{Dispatcher} par l'intermédiaire du \texttt{MessageBuffer}.
2022-11-05 17:09:29 +01:00
\paragraph{\texttt{ClientSender}}
2022-11-06 08:52:39 +01:00
Le \texttt{ClientSender} est un thread qui lit les messages dans le \texttt{ClientPipe} et les envoie au client qui lui est associé.
2022-11-05 17:09:29 +01:00
\paragraph{\texttt{Dispatcher}}
2022-11-06 08:52:39 +01:00
Ce thread a la plus grosse part de travail.
Il lit un à un les messages du \texttt{MessageBuffer}.
Chaque message est parsé puis routé vers la bonne fonction pour un traitement dépendant de ce qui est reçu.
2022-11-05 17:09:29 +01:00
\subsubsection{Les fonctions utilisées}
\paragraph{\texttt{main}}
De la même façon que le client, la fonction \texttt{main} récupère le port d'écoute via la fonction \texttt{handle\_args}. \\
Ensuite, la liste des clients est initialisée avec des valeurs par défaut, qui permettront d'établir quel index est libre.
De même, la liste des groupes est initialisée. \\
Puis, le socket est créé pour la communication, et le pipe du \texttt{MessageBuffer} est défini. \\
Le mutex global est initialisé, il sera utilisé par chaque instance de \texttt{ClientListener}. \\
Le thread du \texttt{Dispatcher} est alors lancé avant la gestion des signaux, pour éviter que le Dispatcher en hérite. \\
On entre alors dans la boucle d'acceptation des connexions entrantes.
Lors d'une connexion, on vérifie qu'il y a bien une place libre dans la liste des clients. \\
On crée alors un \texttt{ClientPipe} et on l'affecte au client lors de l'enregistrement du client dans la liste globale. \\
Il ne reste alors plus qu'à démarrer les threads pour \texttt{ClientSender} et \texttt{ClientListener}, et les ajouter à l'enregistrement du client.
2022-11-05 16:18:24 +01:00
\subsection{Les exclusions mutuelles}
\subsubsection{Chez le client~: sémaphore nommé}\label{named-semaphore}
Pour s'assurer que le flux affiché sur la sortie standard soit atomique, il a fallu implémenter un sémaphore pour le client.
Le \texttt{Sender} et le \texttt{Listener} ont en effet des fonctions bien différentes, mais écrivent tous les deux sur la sortie standard. \\
Le sémaphore étant partagé par des processus (et non des threads), il y avait deux possibilités d'implémentation~:
\begin{enumerate}
\item Sémaphore non nommé, utilisant un espace de mémoire partagé.
\item Sémaphore nommé, pour laquelle la commande \texttt{sem\_open} crée un fichier dans \texttt{/dev/shm/}.
\end{enumerate}
Le sémaphore nommé a permis de ne pas créer l'espace de mémoire partagé à la main.
\subsubsection{Chez le serveur~: mutex}
Le serveur utilisant des threads, l'utilisation de mutex a été faite avec la librairie \texttt{pthread}. \\
Le seul cas où une exclusion mutuelle est possible est lors de l'écriture dans le \texttt{MessageBuffer} par les différents \texttt{ClientListener}.
En effet, il a fallu s'assurer que les messages poussés dans le \texttt{MessageBuffer} (implémenté par un pipe), soient atomiques.
Ceci permet au \texttt{Dispatcher} de les récupérer un par un sans devoir les reconstituer.
2022-11-05 17:09:29 +01:00
\subsection{Les messages de debug/log}
2022-11-06 08:52:39 +01:00
\subsection{Les header files}
2022-11-05 16:18:24 +01:00
\section{Protocole de communication}
2022-11-05 12:43:50 +01:00
\end{document}