326 lines
20 KiB
TeX
326 lines
20 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{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}
|
|
|
|
\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é de 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 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 permettra 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 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.
|
|
|
|
\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}
|
|
|
|
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.
|
|
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}
|
|
|
|
\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}}
|
|
|
|
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}.
|
|
|
|
\paragraph{\texttt{ClientSender}}
|
|
|
|
Le \texttt{ClientSender} est un thread qui lit les messages dans le \texttt{ClientPipe} et les envoie au client qui lui est associé.
|
|
|
|
\paragraph{\texttt{Dispatcher}}
|
|
|
|
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.
|
|
|
|
\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.
|
|
|
|
\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.
|
|
|
|
\subsection{Les messages de debug/log}
|
|
|
|
\subsection{Les header files}
|
|
|
|
\section{Protocole de communication}
|
|
|
|
\end{document}
|