projet-multitaches/rapport.tex

218 lines
13 KiB
TeX

\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
\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.
Le code source peut être trouvé ici~: \\
\url{https://git.tunuifranken.info/efrei/projet-multitaches}.
\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}
\section{Architecture technique}
\subsection{Les clients}
Un client doit essentiellement gérer deux tâches parallèles~: le \texttt{Sender} et le \texttt{Listener}.
Le client est donc composé des quelques fonctions nécessaires pour cela~:
\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é.
Pour cela, un premier \texttt{waitpid} est lancé et qui va récupérer le numéro du processus ayant retourné en premier.
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}. \\
Nous avons le même système de gestion des signaux qui permettront de quitter proprement l'application. \\
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.
Elle est surtout utile dans la messure 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.
\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}
\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.
\section{Protocole de communication}
\end{document}