688 lines
40 KiB
TeX
688 lines
40 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{Quickstart}
|
|
|
|
La compilation du programme se fait grâce au \texttt{Makefile} fourni.
|
|
Il n'a été testé qu'en environnement GNU/Linux.
|
|
Il est recommandé d'utiliser un compilateur compatible \texttt{C99} à cause de la syntaxe des macros de debug et de log (voir~\ref{debug-macros}).
|
|
|
|
En plus de compiler le programme, le \texttt{Makefile} compile le présent rapport avec \texttt{latexmk}.
|
|
Ne pas hésiter à lancer les recettes séparément si \LaTeX{} n'est pas installé.
|
|
|
|
Une configuration du programme est possible avant compilation dans le fichier header \texttt{src/config.h}.
|
|
Des explications y sont sous forme de commentaires.
|
|
|
|
Tous les logs écrivent sur \texttt{stderr}.
|
|
Ainsi, si l'on veut supprimer les logs de la sortie standard et les rediriger vers un fichier pour n'avoir que le déroulement du programme~:
|
|
|
|
\begin{lstlisting}[gobble=4]
|
|
$ ./server 2> server.log
|
|
$ ./client localhost 2> client.log
|
|
\end{lstlisting}
|
|
|
|
Le socket utilisé est un socket IPv4.
|
|
Le client et le serveur ne sont donc pas obligatoirement sur la même machine.
|
|
Le port d'écoute par défaut est 9999, et le client intègre la résolution du nom de domaine.
|
|
|
|
\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}
|
|
|
|
Un envoi de message peut être schématisé 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 write};
|
|
\draw[-latex] (dispatcher) -- (messagebuffer) node[right,midway]{\small read};
|
|
\draw[-latex] (dispatcher) -- (clientpipen) node[right,midway]{\small write};
|
|
\draw[-latex] (clientsendern) -- (clientpipen) node[above,midway]{\small read};
|
|
\draw[-latex] (clientsendern) -- (listenern) node[above,midway]{\small send};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
|
|
\section{Protocole de communication}
|
|
|
|
Un protocole de communcation a été défini pour que le client et le serveur puissent correctement échanger les données.
|
|
Ainsi, le fichier header \texttt{common.h} contient des définitions de commandes qui correspondent chacune à un aspect fonctionnel du programme.
|
|
|
|
\subsection{Symbole de fin de transmission}
|
|
|
|
Pendant l'exécution du programme, le client lit sur la sortie standard jusqu'au premier retour à la ligne (`\verb+\n+').
|
|
Le serveur attend donc le `\verb+\n+' comme signe d'une fin de transmission.
|
|
|
|
Lors de l'initialisation de la session, le client et le serveur vont s'échanger quelques messages avec `\verb+\n+' comme caractère de fin de transmission.
|
|
Mais pour la suite des échanges, le `\verb+\n+' ne pourra pas être utilisé comme symbole de fin de transmission par le serveur, car le message à envoyer peut contenir des retours à la ligne (par exemple \texttt{cmd\_help}).
|
|
|
|
Le caractère \texttt{ETX} (\emph{end of text}), qui correspond au `\verb+\03+', sera alors utilisé pour les messages en provenance du serveur.
|
|
|
|
\subsection{Initialisation de la connexion}
|
|
|
|
Lors de la connexion du client, celui-ci utilise deux commandes avant de poursuivre son exécution~:
|
|
|
|
\begin{itemize}
|
|
|
|
\item \texttt{cmd\_hello}~: envoie le caractère \texttt{CMD\_HELLO} (H), suivi d'un retour à la ligne.
|
|
|
|
\begin{center}
|
|
\begin{tikzpicture}
|
|
\node[minimum height=2cm] (client) at (-3,0) {client};
|
|
\node[minimum height=2cm] (server) at (3,0) {serveur};
|
|
\draw[-latex] (client) -- (server) node[midway,fill=white] {`\verb+H\n+'};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
|
|
Le serveur répond par l'identifiant du client~:
|
|
|
|
\begin{center}
|
|
\begin{tikzpicture}
|
|
\node[minimum height=2cm] (client) at (-3,0) {client};
|
|
\node[minimum height=2cm] (server) at (3,0) {serveur};
|
|
\draw[-latex] (server) -- (client) node[midway,fill=white] {`\verb+23\n+'};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
|
|
\item \texttt{cmd\_get\_name}~: envoie le caractère \texttt{CMD\_GET\_NAME} (N), suivi d'un retour à la ligne.
|
|
|
|
\begin{center}
|
|
\begin{tikzpicture}
|
|
\node[minimum height=2cm] (client) at (-3,0) {client};
|
|
\node[minimum height=2cm] (server) at (3,0) {serveur};
|
|
\draw[-latex] (client) -- (server) node[midway,fill=white] {`\verb+N\n+'};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
|
|
Le serveur répond par le nom du client~:
|
|
|
|
\begin{center}
|
|
\begin{tikzpicture}
|
|
\node[minimum height=2cm] (client) at (-3,0) {client};
|
|
\node[minimum height=2cm] (server) at (3,0) {serveur};
|
|
\draw[-latex] (server) -- (client) node[midway,fill=white] {`\verb+alice\n+'};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
|
|
\end{itemize}
|
|
|
|
\subsection{Fonctionnement des échanges}
|
|
|
|
Maintenant que la session est initialisée, le client va simplement envoyer tel quel ce que l'utilisateur aura tapé.
|
|
C'est à l'utilisateur de taper les bonnes commandes.
|
|
Pour cela il y a la commande \texttt{CMD\_HELP} (h).
|
|
|
|
Les messages reçus par le serveur sont découpés grâces aux espaces, puis le premier caractère de la commande est analysé.
|
|
Ceci autorise l'utilisateur à faire certaines fautes de frappes~: \texttt{h}, \texttt{help} ou \texttt{hippopotame} donneront tous trois la commande \texttt{cmd\_help}.
|
|
|
|
Côté serveur, le \texttt{ClientListener} va transmettre le message au \texttt{MessageBuffer} après l'avoir préfixé de l'identifiant du client suivi d'un espace.
|
|
|
|
Le \texttt{Dispatcher} pourra alors parser le message qui doit être de la forme suivante~:
|
|
|
|
\begin{center}
|
|
`\verb+<id> <commande> [<sous-commande>] [<suite>]\n+'
|
|
\end{center}
|
|
|
|
Le message envoyé au client par le serveur sera alors de la forme~:
|
|
|
|
\begin{center}
|
|
`\verb+<output>ETX+'
|
|
\end{center}
|
|
|
|
\subsection{Quelques exemples de transmission}
|
|
|
|
\paragraph{\texttt{CMD\_HELP} (h)}
|
|
|
|
\begin{center}
|
|
\begin{tikzpicture}
|
|
\node (client) at (-4,0) {client$_3$};
|
|
\node (clientlistener) at (4,0) {ClientListener};
|
|
\node (dispatcher) at (4,-3) {Dispatcher};
|
|
\node (clientsender) at (-4,-3) {ClientSender};
|
|
\draw[-latex] (client) -- (clientlistener) node[midway,fill=white] {`\verb+h\n+'};
|
|
\draw[-latex] (clientlistener) -- (dispatcher) node[midway,fill=white] {`\verb+3 h\n+'};
|
|
\draw[-latex] (dispatcher) -- (clientsender) node[midway,fill=white] {`\verb+<output>+'};
|
|
\draw[-latex] (clientsender) -- (client) node[midway,fill=white] {`\verb+<output>ETX+'};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
|
|
\paragraph{\texttt{CMD\_SEND\_MESSAGE} (s)}
|
|
|
|
\begin{center}
|
|
\begin{tikzpicture}
|
|
\node (client) at (-4,0) {client$_3$};
|
|
\node (clientlistener) at (4,0) {ClientListener};
|
|
\node (dispatcher) at (4,-3) {Dispatcher};
|
|
\node (clientsender) at (-4,-3) {ClientSender};
|
|
\node (alice) at (-4,-6) {alice};
|
|
\draw[-latex] (client) -- (clientlistener) node[midway,fill=white] {`\verb+send alice hello\n+'};
|
|
\draw[-latex] (clientlistener) -- (dispatcher) node[midway,fill=white] {`\verb+3 send alice hello\n+'};
|
|
\draw[-latex] (dispatcher) -- (clientsender) node[midway,fill=white] {`\verb+hello\n+'};
|
|
\draw[-latex] (clientsender) -- (alice) node[midway,fill=white] {`\verb+hello\nETX+'};
|
|
\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 quand la place est libre.
|
|
\item \texttt{name}~:
|
|
Le nom d'usage du client, vaut l'\texttt{id} du client 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.
|
|
|
|
\paragraph{\texttt{get\_next\_free\_client\_id} et \texttt{get\_next\_free\_group\_id}}
|
|
|
|
Respectivement pour la liste des clients et la liste des groupes, ces fonctions parcourent la liste et retournent le premier index libre trouvé.
|
|
Si la fonction retourne \texttt{MAX\_CLIENTS} (ou \texttt{MAX\_GROUPS}), cela veut dire qu'aucune place n'est libre.
|
|
|
|
\paragraph{\texttt{get\_client\_id\_from\_name} et \texttt{get\_group\_id\_from\_name}}
|
|
|
|
Permettent de retrouver l'identifiant d'un client (respectivement d'un groupe) grâce à un nom.
|
|
Ces fonctions sont notamment utilisées par la commande \texttt{cmd\_send\_message} pour l'envoi via un nom.
|
|
|
|
\paragraph{\texttt{create\_group}}
|
|
|
|
Cette fonction reçoit un nom pour créer un groupe de ce nom.
|
|
Elle vérifie d'abord si la liste des groupes n'est pas pleine, puis qu'un tel groupe n'existe pas déjà.
|
|
Si le groupe peut être créé, la structure correspondant au premier index libre est remplie avec les valeurs adéquates.
|
|
|
|
La fonction retourne l'identifiant du groupe créé.
|
|
Ceci permet de signifier que le groupe n'a pas pu être créé en renvoyant -1.
|
|
|
|
\paragraph{\texttt{delete\_group\_if\_empty}}
|
|
|
|
Inverse de \texttt{create\_group}, la fonction \texttt{delete\_group} reçoit un identifiant de groupe pour le retirer de la liste des groupes.
|
|
La suppression du groupe a lieu seulement si le groupe est vide.
|
|
|
|
\paragraph{\texttt{disconnect\_client}}
|
|
|
|
Lors de la déconnexion d'un client, le serveur a du ménage à faire.
|
|
Cette fonction commence par vérifier que le client existe bien, puis le retire de tous ses groupes.
|
|
Pour cela, la liste des groupes est à parcourir une fois, en mettant le booléen du client à 0 dans la liste des clients de chaque groupe.
|
|
De plus, si un groupe contient le client à déconnecter, la fonction appelle \texttt{delete\_group\_if\_empty} pour supprimer le groupe s'il est nouvellement vide.
|
|
|
|
Puis, les threads \texttt{ClientListener} et \texttt{ClientSender} sont terminés.
|
|
|
|
Enfin, les champs du \texttt{struct} du client dans la liste des clients sont remis à leur valeur par défaut, après avoir fermé les file descriptors du socket et du \texttt{ClientPipe}.
|
|
|
|
\paragraph{\texttt{Dispatcher}}
|
|
|
|
La fonction \texttt{Dispatcher} est appelée par le thread \texttt{Dispatcher}.
|
|
|
|
Dans une boucle, les messages sont lus depuis le \texttt{MessageBuffer}.
|
|
Chaque message finit par `\verb+\n+', puisqu'il a été envoyé depuis la ligne de commande du client.
|
|
|
|
Commence alors le parsing.
|
|
Nous avons décidé de ne pas créer de fonction séparée pour parser les messages, car le message peut être parsé en plusieurs fois.
|
|
Cela permet au \texttt{Dispatcher} d'avancer dans le routage au fur et à mesure de ce qui a déjà été parsé.
|
|
|
|
On commence donc par extraire l'identifiant du client émetteur.
|
|
Vient ensuite la commande.
|
|
Les commandes ont été définies par un seul caractère à chaque fois.
|
|
Le message arrivant étant découpé par espaces, le \texttt{Dispatcher} prend en compte seulement le premier caractère et ignore les autres.
|
|
Ceci permet d'être résilient aux fautes de frappes.
|
|
|
|
Le reste de la fonction consiste en un \texttt{switch}.
|
|
En fonction de la commande reçue, la fonction correspondante est appelé avec, si besoin, un traitement préliminaire.
|
|
|
|
Un exemple de parsing en plusieurs fois peut être trouvé pour la commande \texttt{cmd\_list}.
|
|
Une sous-commande \texttt{cmd\_list\_groups} existe, mais n'est évidemment pas vérifiée si l'on ne se situe pas dans la commande \texttt{cmd\_list}.
|
|
|
|
Le parsing pour la commande \texttt{cmd\_send\_message} est un peu plus complexe.
|
|
Il faut extraire le nom du destinataire, obtenir son identifiant, obtenir un groupe portant ce nom si aucun client n'a été trouvé, puis envoyer le message (au client ou au groupe, dépendant de ce qui a été trouvé).
|
|
|
|
Les commandes \texttt{cmd\_group\_join} et \texttt{cmd\_group\_quit} sont en fait des sous-commandes de \texttt{cmd\_group}.
|
|
C'est donc un autre exemple de parsing en plusieurs fois.
|
|
|
|
Le \texttt{default} du \texttt{switch} renvoie au client un message d'erreur.
|
|
|
|
\paragraph{\texttt{ClientListener}}
|
|
|
|
Code correspondant au thread du \texttt{ClientListener}.
|
|
|
|
Nous avons un cas d'exemple de synchronisation~: dans la fonction \texttt{main}, le thread du \texttt{ClientListener} est démarré avant d'être ajouté au \texttt{struct} du client.
|
|
Cela veut dire qu'au démarrage de son exécution, le \texttt{ClientListener} peut ne pas encore avoir totalement accès aux éléments constituant le client.
|
|
Ces éléments sont à priori enregistrés avant le démarrage du thread, mais dans le cadre de la programmation défensive, une boucle permet au \texttt{ClientListener} d'attendre l'enregistrement complet du client avant de poursuivre son exécution.
|
|
|
|
Le reste de la fonction consiste en une boucle infinie.
|
|
Un premier \texttt{recv} bloquant sur le socket du client attend qu'un message arrive.
|
|
Dès qu'un message commence à être reçu, le mutex des \texttt{ClientListener} est verrouillé.
|
|
|
|
Le \texttt{ClientListener} va alors pouvoir écrire le message dans le \texttt{MessageBuffer}.
|
|
Il commence par envoyer l'identifiant du client, ce qui permettra au \texttt{Dispatcher} de router correctement ensuite.
|
|
Puis il démarre une deuxième boucle, mais sur un \texttt{recv} non bloquant cette fois.
|
|
Ceci permet de quitter la boucle dès qu'il n'y a plus rien à recevoir.
|
|
Le \texttt{ClientListener} libère alors le mutex, puis redémarre un nouveau cycle de lecture.
|
|
|
|
\paragraph{\texttt{ClientSender}}
|
|
|
|
Cette fonction est appelée par le thread du \texttt{ClientSender}.
|
|
|
|
La même boucle de synchronisation que pour le \texttt{ClientListener} a lieu.
|
|
Puis ce qui est lu dans le \texttt{ClientPipe} est envoyé au client via son socket.
|
|
|
|
\paragraph{\texttt{clean\_and\_close}}
|
|
|
|
Le serveur a tout comme le client une fonction pour quitter proprement le programme.
|
|
Elle est appelée par le handler lors de la capture des signaux \texttt{SIGINT} et \texttt{SIGTERM}.
|
|
|
|
Cette fonction commence par déconnecter tous les clients~: pour chacun d'entre eux, un message \texttt{EOT} est envoyé, puis la fonction \texttt{disconnect\_client} est appelée.
|
|
|
|
Le \texttt{MessageBuffer} et le mutex des \texttt{ClientListener} sont fermés, puis le thread du \texttt{Dispatcher} est tué.
|
|
Enfin, le socket du serveur est fermé pour mettre fin à la connexion.
|
|
|
|
\paragraph{\texttt{interrupt}}
|
|
|
|
Les signaux \texttt{SIGINT} et \texttt{SIGTERM} sont capturés pour permettre d'appeler la fonction \texttt{clean\_and\_close}.
|
|
|
|
\subsubsection{Les commandes}
|
|
|
|
Certaines fonctions sont préfixées par \texttt{cmd\_}~: il s'agit de commandes provenant du client et qui font une action précise.
|
|
|
|
\paragraph{\texttt{cmd\_help}}
|
|
|
|
Envoie au client l'aide, c'est-à-dire les commandes disponibles et leur syntaxe.
|
|
|
|
\paragraph{\texttt{cmd\_hello}}
|
|
|
|
C'est une commande interne, envoyée par le client lors de sa connexion.
|
|
Le serveur envoie simplement l'identifiant du client.
|
|
|
|
\paragraph{\texttt{cmd\_group\_join}}
|
|
|
|
Ajoute un client à un groupe.
|
|
Si le groupe n'existe pas, il est créé.
|
|
|
|
\paragraph{\texttt{cmd\_group\_quit}}
|
|
|
|
Retire le client d'un groupe.
|
|
Une fois la suppression effectuée, la fonction \texttt{delete\_group\_if\_empty} est appelée pour faire le ménage si jamais le groupe est vide.
|
|
|
|
\paragraph{\texttt{cmd\_set\_name}}
|
|
|
|
Modifie le nom d'usage du client.
|
|
Si le nom est déjà pris, la modification est refusée.
|
|
|
|
\paragraph{\texttt{cmd\_get\_name}}
|
|
|
|
Commande interne correspondant à la commande \texttt{cmd\_get\_name} du client, qui l'utilise pour modifier son prompt.
|
|
Cette fonction se contente donc d'envoyer au client son nom.
|
|
|
|
\paragraph{\texttt{cmd\_list\_clients}}
|
|
|
|
Envoie au client la liste des clients connectés.
|
|
|
|
Pour envoyer le message d'un coup tout en ajoutant les clients connectés au fur et à mesure du parcours de la liste des clients, une série de \texttt{malloc} est faite.
|
|
Un \texttt{char*} temporaire permet d'agrandir progressivement le message en libérant le \texttt{malloc} précédent.
|
|
|
|
\paragraph{\texttt{cmd\_list\_groups}}
|
|
|
|
Envoie au client la liste des groupes et les clients présents dans chaque groupe.
|
|
|
|
Cette fonction suit le même principe que la fonction \texttt{cmd\_list\_clients} avec la complexité supplémentaire d'ajouter une chaîne de \texttt{malloc} pour afficher chaque client au sein de chaque groupe.
|
|
|
|
\paragraph{\texttt{cmd\_broadcast}}
|
|
|
|
Pour broadcaster un message, il suffit de l'envoyer à chaque client connecté en ignorant l'émetteur.
|
|
|
|
\paragraph{\texttt{cmd\_send\_message}}
|
|
|
|
Fonction de base pour l'envoi d'un message.
|
|
Comme elle est utilisée par d'autres fonctions, la gestion de l'émetteur et du destinataire est laissée au \texttt{Dispatcher}.
|
|
|
|
La fonction \texttt{cmd\_send\_message} reçoit les identifiants émetteur et destinataire, ainsi que le message à envoyer.
|
|
Grâce à un \texttt{malloc}, le message est préfixé de l'émetteur, ainsi que de la date d'envoi, récupérée via la bibliothèque \texttt{time.h}.
|
|
Le message est également suffixé par le caractère \texttt{ETX} (\emph{end text}), pour signifier au \texttt{Listener} du client en face que le flux est terminé.
|
|
|
|
\paragraph{\texttt{send\_group\_message}}
|
|
|
|
Il ne s'agit en fait pas d'une commande à part entière, mais d'une fonction correspondant à une sous-commande de \texttt{cmd\_send\_message}.
|
|
Ceci est expliqué dans la fonction \texttt{Dispatcher}.
|
|
|
|
Semblablement à \texttt{cmd\_broadcast}, un message est envoyé à tous les clients d'un groupe.
|
|
|
|
\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 bibliothèque \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}\label{debug-macros}
|
|
|
|
Pour plus de clarté concernant les messages imprimés à la fois côté serveur et côté client, des macros ont été définies dans le fichier de header \texttt{common.h}.
|
|
|
|
En fonction du niveau de log, (\texttt{ERROR}, \texttt{WARNING}, \texttt{INFO}, \texttt{DEBUG}), les messages sont ou ne sont pas imprimés.
|
|
|
|
\begin{minipage}{\linewidth}
|
|
\begin{lstlisting}[gobble=12]
|
|
#define warn_print(fmt, ...) \
|
|
do { if (WARNING) fprintf(stderr, "[WARNING] %s:%d:%s(): " fmt, __FILE__, __LINE__, __func__); } while (0)
|
|
#define warn_printf(fmt, ...) \
|
|
do { if (WARNING) fprintf(stderr, "[WARNING] %s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)
|
|
\end{lstlisting}
|
|
\end{minipage}
|
|
|
|
Ces macros incluent des informations utiles~: le niveau du log, ainsi que le fichier, la ligne et la fonction ayant émis le log.
|
|
|
|
\section{Conclusion}
|
|
|
|
Ce projet nous a permis d'appréhender de multiples composantes d'un programme multitâches complet.
|
|
|
|
Avec le langage C, nous avons pu maîtriser toutes les étapes nécessaires, de la synchronisation à la gestion de la transmission de flux caractère par caractère.
|
|
|
|
La gestion d'erreur a été une partie fondamentale du développement de l'application.
|
|
Nous avons en effet choisi de programmer de manière défensive~: tout code de retour des fonctions doit être vérifié, toute branche conditionnelle doit être explorée.
|
|
Il a également fallu choisir en fonction des cas que faire lors d'une erreur~: quitter le programme, ignorer et reprendre la boucle, fermer une connexion, informer le client\ldots
|
|
|
|
Un élément particulièrement délicat à mettre en \oe{}uvre a été la gestion du mutex lors de la réception d'un message.
|
|
Lors de la boucle de lecture, la fonction \texttt{read} (ou \texttt{recv} en fonction des cas) est bloquante.
|
|
Si le mutex est verrouillé juste avant, le thread garde la main de manière infinie, que ce soit lors de la lecture ou lors de son attente.
|
|
|
|
Ce type de considération à prendre montre les implications à avoir pour un programme multitâches, qui ne se contente pas de faire plusieurs choses en parallèle.
|
|
|
|
\end{document}
|