Projet de programmation Système en C

Transcription

Projet de programmation Système en C
Destinataires :
Rendu le :
Nombres de pages :
Dominique REVUZ, Sébastien PAUMIER,Sylvain CHERRIER
Dimanche 17 Mai 2009
21
Rédacteurs
Aurélien OLIVIER
François JANNIN
Logins UMLV
aolivi02
fjannin
")'#*)#" 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111:
1 ,% )#"(#!% !")'((*' (*)11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111;
(('+(%'#%#((%'
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111;
1 )))* 2%% )#")(%)#"( 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111<
1
(#,+ #%%!")11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111=
("% ((#)(*('+*'1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111=
"(!()#" #"*''"0( )451111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111>
!#")#""!")
(+( )*' 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111?
)'*)*'()#"( ")("#*'()')!")( 111111111111111111111111111111111111111111111111111111111111111111111111111111111111@
'("('&*)(111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111198
()#"(+'(#"(*%'#)## 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111198
381@ 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111198
3918 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111198
3919 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111199
("$*+'*#2 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119:
+ #%%!")(#!!"( 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119:
()#"*)#""' 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119;
1 " -(('%#"((
( 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119;
381@ 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119;
3918 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119<
3919 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111119=
1 %)#"*!#")('%)(11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111:8
VI. (* )('"#")'( 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111:9
1 #" *(#" 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111:9
L’objectif du projet « MIDAS »est le développement d’un programme
permettant de gérer un
dictionnaire accessible à la fois par le web et en local par des commandes systèmes.
Dans ce cadre, ce rapport présentera dans un premier temps l’état actuel du programmeainsi que les
choix de développement. Par la suite, nous nous intéresserons aux différents choix de développements
effectués, et aux difficultés que nous avons rencontrées lors de leur implantation. Pour conclure, nous
ferons un bilan sur le travail réalisé et sur les enseignements tirés du projet.
:
MIDAS offre des fonctionnalités de dictionnaire. Il propose en outre les 3 opérations suivantes:
•
•
•
put: permettant de mettre un mot dans le dictionnaire,
le dictionnaire ne contenant pas de doublons.
get: permettant de tester si un mot donné est dans le dictionnaire
list: permettant d’obtenir la liste de tous les mots du dictionnaire
Midas se présente sous la forme d'un démon, c'est-à-dire un programme autonome réalisant une tâche de
fond. Le fonctionnement du démon ne devant pas être remarqué par l'utilisateur. Le démon Midas devra
être interrogeable soit via des requêtes HTTP, soit via des commandes exécutables.
Les mots traités par le dictionnaire Midas sont supposés encodés en ISO-Latin-1 (ISO-8859-1).
Les requêtes HTTPacceptés par Midas sont de la forme suivante:
•
•
•
mettre un mot: HTTP://host:port/put_word
Retourne une page web indiquant si le mot a été ajouté ou s'il était déjà présent.
tester un mot: HTTP://host:port/get_word
Retourne une page web indiquant si le mot est dans le dictionnaire.
lister le contenu: HTTP://host:port/list
Retourne une page web donnant tous les mots contenus dans le dictionnaire, triés.
Toute autre requête génère donc 'une page d'erreur, conformément aux bonnes pratiques d'un serveur
web poli.
Midas propose 3 commandes systèmes, qui fonctionnent soit en local, soit en précisant par des options
l'hôte et le port de la machine hébergeant le service.
Elles fonctionnent de la façon suivante:
•
•
•
put: prend en paramètre un ou plusieurs mots à mettre dans le dictionnaire.
Accepte des mots sur l'entrée standard, si aucun paramètre.
get: teste si LE mot passé en paramètre est dans le dictionnaire et retourne 0 ou 1 selon le
résultat. S'il n'y a pas de paramètre, la commande lit des mots sur son entrée standard et,
pour chacun d'eux, produit sur sa sortie standard une ligne de la forme mot:TRUE/FALSE
list: produira sur la sortie standard la liste des mots, triée.
Au terme du développement, nous avons un programme fonctionnel répondant aux contraintes de
l’énoncé du projet. De plus notre serveur Midas est concurrent. En effet il permet de gérer de multiples
clients simultanément grâce au mécanisme de sélecteur. Toutes les commandes linux (./put, ./get,
./list) fonctionnent parfaitement de telle sorte qu’il est possible de les combiner :
•
•
./list –s <ADRESSE SERVEUR> -p <PORT> | ./put
o Produit un ajout de tous les mots présent dans le dictionnaire du SERVEUR dans le
dictionnaire local.
cat sample/TEXTE-ISO-8851-1.txt | ./put
o Produit l’ajout des mots du fichier .txt dans le dictionnaire local
Cela s’avère en effet très pratique.
Le programme Midas permet donc de :
•
•
•
•
•
•
•
Gérer plusieurs clients effectuant des demandes différentes, et cela en concurrence.
Reposer sur un fonctionnement HTTP (socket AF_INET) et un fonctionnement local (AF_UNIX)
Gérer les demandes des clients en fonction de leurs requêtes
Détecter la version HTTP utilisée par le client et lui répondre en conséquence
Respecter la norme HTTP quant aux requêtes générées
Gérer les mauvaises requêtes par des messages d’erreurs
Ajouter, demander, lister les mots d’un dictionnaire.
Spécifications du programme :
•
•
•
Les mots réceptionnés par le serveur sont supposés en latin-1 (ISO-8859-1). Cela signifie que
Midas ne peut pas gérer les mots encodés en UTF8, et cela se traduira par l’apparition de
caractères bizarres, non décodés. Avec un navigateur récent, les requêtes seront encodés en URL
encoding et donc il n’y aura pas de soucis, cependant avec les commandes UNIX, il faut bien
veiller à ce que le terminal soit en paramétré en encodage latin-1.
Afin de rester cohérent avec la réalité, une variable MAX_WORD_SIZE présente dans daemon.h
permet de modifier la taille maximale d’un mot admis par Midas. Si le mot demandé est plus petit
que cette taille cela ne posera pas de problème, dans le cas contraire, un message d’erreur
signalera au client que son mot est trop long.
Seuls les champs « Host : » ainsi que « Connection : » sont traités par Midas. Nous gérons donc le
Keep-Alive si nécessaire, mais les champs Date, e-Tag, Accept… ne sont pas gérés.
Nous avons effectué différentes batteries de test afin de valider le fonctionnement du démon.
En effet, bien que nous pensions que le programme fonctionnait, nous trouvions encore des bugs, c’est
pour cette raison que nous avons écrit des scripts python générant une multitude de requêtes par
seconde. En testant pendant plusieurs heures l’envoi de plusieurs requêtes par seconde vers Midas, nous
pouvons dire que notre démon est stable. Aucune erreur n’a été détectée dans la version finale. Midas a
été testé avec succès sur des systèmes UNIX (Ubuntu 9.04, Debian, ainsi que Mac OS X).
Nous avons découpé le développement de ce projet en différents points, ou phases. En premier lieu il
convenait d’identifier les différentes actions ou modules à réaliser.
Parmi eux on peut citer :
•
•
•
•
•
•
•
•
•
La mise en place de sockets
L’intégration du mécanisme de concurrence inter-clients : le sélecteur
La structure de gestions des clients
Le parsing des requêtes clientes
La gestion des différentes versions de HTTP (0.9, 1.0 et 1.1)
La mise en œuvre du décodage d’URL – encodage en latin-1
Le développement des commandes UNIX (./get, ./put, ./list)
La gestion du dictionnaire
L’écriture d’un script bash permettant le démarrage du démon.
Midas fonctionne en TCP (mode connecté). Il doit écouter à la fois les clients en HTTP (port 29200) et les
clients utilisant les commandes UNIX. Nous avons donc choisit d’utiliser 2 sockets différentes. L’une
étant de la famille AF_INET (pour le réseau IP classique), l’autre étant de la famille AF_UNIX (pour les
commandes UNIX locales). Comme nous utilisons le protocole TCP, le type de socket est SOCK_STREAM.
La déclaration et l’attachement à un port spécifique ainsi que l’écoute se font très simplement.
Voici un exemple pour notre socket HTTP :
HTTP_addr_in.sin_family = AF_INET;
HTTP_addr_in.sin_port = htons(PORT_HTTP);
HTTP_addr_in.sin_addr.s_addr = INADDR_ANY;
/* HTTP Socket creation */
if((HTTP_sock = socket(AF_INET, SOCK_STREAM, 0))==-1){
perror("Socket AF_INET error: ");
exit(1);
}
/* HTTP Socket binding */
if(bind(HTTP_sock, (struct sockaddr *)&HTTP_addr_in, sizeof(HTTP_addr_in))==-1){
perror("Binding AF_INET error");
exit(1);
}
/* HTTP Socket listening */
if(listen(HTTP_sock, BACKLOG)==-1){
perror("Listening AF_INET error");
exit(1);
}
Afin de gérer au mieux les clients en concurrence, nous avons analysé les différentes solutions qui
s’offraient à nous. Les principales fonctions POSIX qui réalisent cela sont « poll() » et « select() ». Nous
avons choisit d’utiliser « select() » qui est la norme. De plus « poll() » et « epoll() » peuvent ne pas être
supportés sur toutes les plate-forme, tandis que select() oui. Ceci nous a conforté dans notre choix.
Nous réalisons donc un multiplexage d’entrées-sorties pour être prévenu des lectures possibles sur les
sockets clientes, et également pour être prévenu du moment où l’on pourra répondre au client (écriture
sur la socket). « Select() » permet de déclarer des ensembles de descripteurs de fichiers (socket AF_UNIX
et AF_INET dans notre cas) et de procéder à l’action adéquate pour chacun de ses événements.
Nous avons ainsi découpé les lectures et écritures. Cela permet de lire à un instant ‘t’ ce que dit un
client, mais de lui répondre uniquement lorsque l’on pourra et non tout de suite. Chaque client qui se
connecte est mit dans un ensemble de descripteur de fichier (une socket étant vu comme un fichier)
pour la lecture. A tour de rôle le sélecteur va itérer sur les éléments de cet ensemble et nous prévenir
lorsque de nouvelles données seront disponibles à lire. De même pour la réponse au client, nous utilisons
un 2ème ensemble de descripteur de fichier qui aura le même fonctionnement que le précédent sauf qu’il
est destiné aux écritures.
Structure de fonctionnement du sélecteur en pseudo-code:
FAIRE TOUJOURS
|
|
Réinitialisation des ensembles
|
|
Appel à Select() afin de récupérer les actions possibles
|
|
POUR CHAQUE SOCKET i ENREGISTREE DANS LES ENSEMBLES DU SELECTEUR
|
|
|
|
SI ‘i’ fait partie de l’ensemble de LECTURE
|
|
|
|
|
|
SI ‘i’ est la socket AF_UNIX ou la socket AF_INET
|
|
|
|
>NOUVELLE CONNEXION D’UN CLIENT
|
|
|
SINON
|
|
|
>NOUVELLE LECTURE POSSIBLE sur la socket i
|
|
|
|
|
SINON, SI ‘i’ fait partie de l’ensemble d’ECRITURE
|
|
|
|
>NOUVELLE ECRITURE POSSIBLE sur la socket i
|
|
|
FIN POUR
FIN FAIRE
Afin de réaliser l’attente infinie de notre programme nous avons utilisé une boucle for sans condition.
Cela permet d’attendre indéfiniment les futurs événements disponibles sur les sockets.
Ensuite, on remet à zéro les ensembles de lecture et d’écriture par sécurité. Pour ce faire nous avons
une copie de sauvegarde de chaque ensemble, que nous réaffectons à chaque tour de boucle for( ;;).
Enfin, on initialise le sélecteur par un appel à « select() » avec en argument les 2 ensembles de lecture
et écriture. Il faut également spécifier à « select() » la valeur du plus grand descripteur de socket
existant dans nos ensembles. Il convient donc de mettre cette valeur à jour à chaque fois que nous
accepterons un nouveau client.
Les quelques lignes de code peuvent se résumer à cela :
/* Defining READ and WRITE sets */
fd_set read_fds;
fd_set write_fds;
fd_set cp_read_fds;
fd_set cp_write_fds;
/* Initiate the sets to 0 */
FD_ZERO(&read_fds);
FD_ZERO(&cp_read_fds);
FD_ZERO(&write_fds);
FD_ZERO(&cp_write_fds);
/* Adding HTTP and UNIX socket to the proper sets */
FD_SET(HTTP_sock,&read_fds);
FD_SET(local_sock,&read_fds);
/* Calling select() */
if(select(maxFD+1,&cp_read_fds,&cp_write_fds,NULL,NULL)== -1){
perror("Select error");
exit(1);
}
Par la suite nous pouvons tester si la socket sur laquelle la boucle for itère fait partie de tel ou tel
ensemble par la commande suivante:
if(FD_ISSET(i,&cp_read_fds))
La macro FD_ISSET nous permet de tester si le descripteur de fichier contient un événement, si c’est le
cas on rentre dans notre condition de traitement qui traite une lecture, ou bien une écriture.
Pour l’ensemble READ, qui représente tous les clients connectés, à chaque connexion d’un client nous
ajouterons son descripteur de socket dans cet ensemble. Un deuxième ensemble tampon cp_read_fds
est une copie à chaque tour du for( ;;) de l’ensemble de lecture. La copie servira à tester si un client a
un message à transmettre, on utilise ce tampon pour éviter la déconnexion directe d’un nouveau client
sur le serveur, le nouveau client sera pris en compte au prochain tour de la boucle.
Voici le fonctionnement des ensembles de lecture/écriture du sélecteur :
Lorsqu’un client se connecte, le descripteur de socket associé est ajouté dans l’ensemble de lecture.
Une fois que le sélecteur aura fait un tour de boucle for( ;;), il sera à même de nous prévenir qu’une
lecture est possible sur cette socket. Le processus d’analyse de la requête cliente sera donc appelé. Si
c’est une requête en provenance de la socket AF_UNIX, elle sera considérée comme valide d’office,
cependant si c’est une requête provenant de la socket de service AF_INET, alors il y aura remise dans
l’ensemble de lecture tant que la requête ne contiendra pas \r\n\rn. En effet, ces caractères sont
essentiels et indique une fin de requête HTTP, ils sont parfois noté CRLF.
Le processus d’analyse de la requête est un « parsing » de la requête. On regarde simplement les
premiers caractères pour connaitre l’action souhaitée par le client. De plus, cette phase inclus
également la détection de la version HTTP, ainsi que le test de conformité HTTP. Il est nécessaire de
regarder la conformité car selon la version du protocole HTTP, certains champs sont optionnels, et
d’autres requis. Par exemple, en HTTP/1.1 le champ « Host : » est obligatoire. En HTTP/1.0 et HTTP/1.1
le champ « Connection : » est également obligatoire.
La requête étant maintenant complète et analysée, on traite en conséquence la totalité du résultat ou
seulement une partie (ex. en HTTP/1.1 chunked, on ne traite pas tout d’un coup) et l’on va copier dans
le buffer de sortie du client le résultat de sa requête. Une fois le buffer de sortie rempli, on retire le
descripteur de la socket du client de l’ensemble de lecture et on l’ajoute à l’ensemble d’écriture.
Lors du prochain tour de sélecteur, l’appel a « select() » nous préviendra que l’on peut écrire sur la
socket du client. C’est alors à ce moment précis que l’on déclenchera l’envoi des informations contenues
dans le buffer de sortie du client. Cette opération est considérée comme atomique, car nous l’effectuons
de A à Z, mais cette opération est rapide.
De la façon dont nous avons implémenté Midas, il est donc facile de gérer plusieurs clients en
concurrence. Si nous avions fait un programme bloquant, lors d’une opération de listage du dictionnaire,
tous les accès à Midas auraient été bloqués. Dans l’état actuel, il est possible de lancer plusieurs /list du
dictionnaire en même temps, sans que l’un des clients se rendent compte de quoi que ce soit. La
rapidité de traitement est légèrement divisée par le nombre de client, ce qui est normal.
Afin de gérer les clients, une structure était nécessaire. Pour chaque client, de sa connexion à sa
déconnexion plusieurs informations sont stockées.
Tout d’abord le descripteur de sa socket, mais également deux buffers (un en entrée, un en sortie) ainsi
que plusieurs flags. Certains de ces marqueurs permettent de déterminer si le client a finit sa requête, à
quel endroit du dictionnaire est t’il arrivé (pour le listage), s’il supporte le Keep-Alive, si sa requête est
bien formée, ou encore quelle est la version du protocole HTTP qu’il a utilisé.
Nous avons d’abord pensé utiliser un tableau contenant ses structures, car cela évite l’allocation
dynamique, mais un inconvénient majeur nous en a dissuadés. En effet, si nous avions stocké les
structures clientes dans un tableau, ce tableau aurait du être borné. Or nous ne voulions pas borner
notre programme Midas à N clients maximum. Nous avons donc opté pour une structure par liste chainée,
même si cela nous oblige à effectuer un petit peu d’allocation dynamique.
Voici la structure d’un client :
/* Structure used for client
typedef struct client{
char buffer_in [BUFSIZ];
char buffer_out [BUFSIZ];
enum HTTP_version version;
enum AF_type socket_type;
int socket_desc;
int request_complete;
int in_process;
int has_done;
int offset;
int keepalive;
int malformed;
struct client* next;
}Client,*List;
information */
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
the input buffer */
the output buffer */
the HTTP version */
the socket type */
the socket file descriptor */
to know if the initial request is complete (2 CRLF) */
to check if all data have been sent to client */
to check if all data have been sent to client */
used as reminder of the dictionary position */
is a keepalive request ? */
is a malformed request ? */
the next client is linked here */
Les buffers de nos clients ont une taille égale à BUFSIZ, cette constante définie dans le fichier <stdio.h>
représente la taille d’un tampon de lecture/écriture optimisée pour le système sur lequel le code sera
exécuté.Cette taille est en rapport avec les tampons gérés par les primitives systèmes gérant les entrées
/sorties. Généralement BUFSIZ vaut 8192 octets.
Le « parsing » des requêtes est primordial dans l’application Midas. C’est cette étape du processus de
réponse à un client qui va permettre de déterminer la bonne action à réaliser. Cela est fait par simple
comparaison de chaine de caractères. Par exemple on regarde si les premiers caractères de la requête
du client commence par « GET » ou par « / », afin de savoir si c’est une requête HTTP ou locale. En
gérant tous les cas d’erreurs, cette étape fonctionne bien et permet rapidement de connaitre la bonne
sous-fonction chargée de répondre au client.
La deuxième partie consiste à analyse le buffer d’entrée du client, c'est-à-dire sa requête afin de trouver
quelle version du protocole HTTP il a utilisé par exemple, et ainsi vérifier la conformité de la requête.
De plus il est possible dans cette phase de connaitre des informations cruciale pour la réponse (la
présence ou non de keep-alive). Les informations récoltées lors de cette étape, sont présentes en tant
que flag dans la structure du client concerné. Si nous avions voulus gérer les champs optionnels d’HTTP
ou d’autres champs que Midas ne supporte pas dans sa version actuelle, nous aurions pu effectuer les
détections de champs supplémentaires ici.
Le protocole HTTP comporte trois versions. La version actuelle est la version 1.1. Cependant, les
évolutions apportées à ce protocole depuis sa création, implique une rétrocompatibilité envers les
clients fonctionnant en HTTP/0.9 par exemple. Nous avons donc du nous documenter sur le protocole
HTTP afin de connaitre les spécifications exactes en termes de requêtes HTTP.
C’est la version de base. Les requêtes sont brutes, sans options.
Le format des requêtes est le suivant :
GET /list\r\n
\r\n
GET /list HTTP/0.9\r\n
\r\n
La spécification de la version est optionnelle. Le serveur doit répondre à ces requêtes par un message
HTTP sans entête, comportant directement le code HTML nécessaire à l’affichage de la page.
C’est la version un peu plus évoluée. Dans cette version des champs optionnels se rajoutent, et surtout
la présence d’entête dans la réponse est obligatoire. La connexion peut être coupée ou maintenue.
GET /list HTTP/1.0\r\n
\r\n
GET /list HTTP/1.0\r\n
Connection: Keep-Alive\r\n
\r\n
Le client peut spécifier de garder la connexion ouverte a la suite de la réponse ou non. Dans le cas d’un
maintient de connexion, un champ « Keep-Alive » permettra de dire au client combien de temps il doit
garder la connexion ouverte.
C’est la version la plus évoluée. Dans cette version d’autres champs optionnels se rajoutent.
La connexion peut être coupée ou maintenue. La présence du champ « Host : » est obligatoire. Ce champ
doit normalement permettre de terminer l’hôte désiré par le client dans le cas ou le serveur gère des
VHOST. Cependant nous vérifions que ce champ est présent mais le client peut y mettre n’importe quoi,
car aucun test n’est fait pour vérifier l’existence de l’hôte.
GET /put_myNewWordHTTP/1.1\r\n
Host: midas.biniou.fr
Connection: close
\r\n
GET /get_CeProjetMeRendFouHTTP/1.1\r\n
Connection: Keep-Alive\r\n
Host: 127.0.0.1
\r\n
La version HTTP/1.1 permet également le transfert en mode chunked, c'est-à-dire que le client va
recevoir une partie de la réponse avant que celle-ci soit complètement terminée. Cela passe par l’envoi
de différents morceaux appelés « chunks » dont la taille est précisée en hexadécimal avant l’envoi du
morceau. Nous avons trouvé ce mode de transfert très intéressant et nous l’avons implémenté avec
succès.
Selon la version du protocole spécifié dans la requête, le serveur Midas va répondre différemment.
Pour visualiser les messages de réponse de notre serveur Midas, veuillez vous référez à la partie
concernée.
Les navigateurs d’aujourd’hui encodent leurs requêtes afin de préserver l’intégrité des données.
Notamment pour les caractères accentués du style « é » qui pourrait être corrompu. Le format utilisé par
les navigateurs est appelé URL encoding, c’est une sorte de format UTF8. Les navigateurs peuvent
encoder les caractères accentués sur 1 octet ou sur 2 octets. Nous avons donc géré les deux cas.
Afin de réaliser le décodage des caractères envoyés par les navigateurs, nous avons des tableaux de
correspondance. Ils sont définit en « static » afin que le système ne réalloue pas a chaque fois, sachant
que c’est le même tableau de caractère. De plus le tableau de correspondance des caractères latin-1 est
définit en hexadécimal, afin d’éviter que les caractères soit corrompus lors du changement de
plateforme ou d’encodage des fichiers sources.
Voici un exemple de tableaux de correspondance :
static char hexacode[23]=
{
0xE9,0xE8,0xE0,…
};
/* URL encoded characters on 2 bytes */
static char* encoding2[23]=
{
"%C3%A9","%C3%A8","%C3%A0",…
};
/* URL encoded characters on 1 bytes */
static char* encoding1[23]=
{
"%E9","%E8","%E0",…
};
De cette façon, des fonctions de décodage sont définies. Elles regardent la requête du client afin de
détecter la présence de la séquence « %C3%A9 » par exemple. Si elles détectent la présence de cette
séquence, le caractère sera remplacé par sa valeur hexadécimale en latin-1.
Lors de la réponse, il n’est pas nécessaire de ré-encoder les caractères en UTF8. En effet, un champ de
l’entête de réponse permet de spécifier au navigateur que la réponse se fait en latin-1, ainsi il affichera
les caractères correctement.
Les commandes linux ./put, ./get et ./list se ressemblent et restent toutefois assez basique par rapport
au serveur Midas. Selon les paramètres gérés par la fonction « getopt », ils utilisent une socket AF_INET
ou AF_UNIX afin de se connecter au serveur pour envoyer leurs requêtes. Ils sont cependant très stables
et permettent de se combiner. Comme nous l’avons dit précédemment, par le biais d’un pipe (‘|’) il est
facile d’ajouter des mots dans le dictionnaire tout en listant. (./list | ./put).
Les commandes UNIX sont polies, en effet il suffit d’effectuer un CTRL+D dans un terminal pour mettre
fin au processus d’ajout de mot ou de demande de mot. La connexion au serveur Midas est alors
instantanément proprement fermée.
L’objectif de Midas étant la gestion d’un dictionnaire, nous avons du réfléchir à un moyen simple et
rapide et stocker des mots, et de pouvoir les lister facilement de façon trié. La complexité algorithmique
n’étant pas évalué dans ce projet, nous avons décidé d’utiliser la commande unix existante « sort ».
Cette commande permet de trier un fichier par ordre alphabétique facilement.
Le
dictionnaire
est
donc
un
fichier
physique
présent
sur
disque
dur
dans
/tmp/Midas/dictionary/midas_dico que l’on trie au besoin via la commande sort. Afin de vérifier qu’un
mot est présent ou non dans le dictionnaire, nous avons choisit d’utiliser la commande grep. Par le biais
de ces deux commandes unix, nous avons donc une implémentation simple d’un dictionnaire.
Les commandes unix grep et sort sont exécutés par un appel à fork() suivi d’un appel à la primitive
système « execvp() » ou « execle() ». Le triage des mots du dictionnaire doit en effet se faire par le biais
d’un appel « execle() » qui permet de définir une variable d’environnement avant d’exécuter une
commande. Dans notre cas, nous spécifions la variable d’environnement LC_ALL=C qui permet de
changer la LOCALE du système. Cela est nécessaire pour que sort() ai le comportement désiré.
En récupérant le code de retour de la fonction « grep », on peut aisément savoir si le mot est présent ou
non dans le dictionnaire. Le dictionnaire est trié à chaque ajout de mot.
Dans cette partie vous trouverez les différents cas de réponses possible avec notre serveur Midas. Les
exemples sont classés par version du protocole HTTP.
GET /put_biniou HTTP/0.9
<html>
<head>
<title>Welcome on MIDAS - Multi-Interface Dictionnary Access</title>
</head>
<body>
<br><table BORDER=1 BGCOLOR='#C0C0C0' ALIGN='center'>
<tr>
<td>
<center><font color='black'><b>MIDAS</b></font></center>
</td>
<td>
<center><font color='green'> Putting '<font color='red'>biniou</font>' in dictionary
</font></center>
</td>
</tr>
</table>
</center></body></html>
!$
GET /get_biniou HTTP/1.0
Connection: Keep-Alive
HTTP/1.0 200 OK
Server: BZH MIDAS server
Content-type: text/html; charset=ISO-8859-1
Connection: Keep-Alive
Keep-Alive: timeout=15, max=50
Content-Length: 418
<html>
<head>
<title>Welcome on MIDAS - Multi-Interface Dictionnary Access</title>
</head>
<body>
<br><table BORDER=1 BGCOLOR='#C0C0C0' ALIGN='center'>
<tr>
<td>
<center><font color='black'><b>MIDAS</b></font></center>
</td>
<td>
<center><font color='blue'> Checking the word '<font color='red'>biniou </font>'
</font></center></td><td><font color='green'>OK</font></td></tr>
</table>
</body></html>
La requête produit une réponse avec maintient de la connexion. Si le client avait demandé un
« Connection : Close » la connexion aurait été coupée. On peut noter la présence du champ « ContentLength » qui précise la taille de la page en octet, ainsi que l’encodage ISO-8859-1.
"#
GET /put_BINIOU HTTP/1.1
Connection: close
HTTP/1.1 400 Bad Request
Server: BZH MIDAS server
Content-type: text/html; charset=ISO-8859-1
Connection: close
Content-Length: 290
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>400 Bad Request</TITLE>
</HEAD><BODY>
<H1>Bad Request</H1>
Your browser sent a request that this server could not understand.<P>
Malformed HTTP Request! <P><HR>
<ADDRESS>MIDAS server Port 29200</ADDRESS>
</BODY></HTML>
Le client a initié une requête en HTTP/1.1 et n’a pas spécifié le champ « Host : » qui est obligatoire.
Cela produit un message d’erreur « 400 Bad Request » avec le texte « Malformed HTTP Request »
GET /put_midas HTTP/1.1
Host: 127.0.0.1
Connection: close
HTTP/1.1 200 OK
Server: BZH MIDAS server
Content-type: text/html; charset=ISO-8859-1
Connection: close
Content-Length: 394
<html>
<head>
<title>Welcome on MIDAS - Multi-Interface Dictionnary Access</title>
</head>
<body>
<br><table BORDER=1 BGCOLOR='#C0C0C0' ALIGN='center'>
<tr>
<td>
<center><font color='black'><b>MIDAS</b></font></center>
</td>
<td>
<center><font color='green'> Putting '<font color='red'>midas</font>' in dictionary
</font></center>
</td>
</tr>
</table>
</center></body></html>
Ici on est en HTTP/1.1 et l’on répond proprement au client. Cependant le mode chunked HTTP/1.1 est
implémenter uniquement pour le transfert de la liste du dictionnaire, c'est-à-dire lors d’un /list et non
pour les commandes /get et /put qui produise une page très petite en terme de taille.
GET /list HTTP/1.1
Host: oneill.imj.fr:29200
User-Agent: Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.10) Gecko/2009042523
Ubuntu/9.04 (jaunty) Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
HTTP/1.1 200 OK
Server: BZH MIDAS server
Content-Type: text/html; charset=ISO-8859-1
Connection: Keep-Alive
Keep-Alive: timeout=15, max=50
Transfer-Encoding: chunked
MODE DE TRANSFERT PAR MORCEAUX
13c
TAILLE DU 1erMORCEAU EN HEXADECIMAL
<html>
<head>
<title>Welcome on MIDAS - Multi-Interface Dictionnary Access</title>
</head>
<body>
<br><table BORDER=1 BGCOLOR='#C0C0C0' ALIGN='center'>
<tr>
<td>
<center><font color='black'><b>MIDAS</b></font></center>
</td>
<td>
<center><font color='red'> Words Listing </font></center>
</td>
</tr>
</table>
<ul>
79
TAILLE DU 2nd MORCEAU
<li>accentu.</li>
<li>biniou</li>
<li>càractère_spéci@uX</li>
<li>foo</li>
<li>midas</li>
<li>test</li>
<li>zorglub</li>
14
</ul></body></html>
0
FIN DE TRANSMISSION
Ici on voit bien le transfert en mode mode chunked. Le navigateur du client commence l’affichage de la
liste dès la réception du premier morceau.
La liste des mots triés est formatée en HTML par des balise <li> et </li> qui affiche une puce devant
chaque mot. Les entêtes des pages utilisent quant à elle des balises simples, et des tableaux avec mise
en forme de la couleur.
Voici quelques captures du rendu de notre serveur Web Midas dans un navigateur :
Dans le cas ou l’utilisateur de Midas insère un mot valide, la page précédente est générée. Selon si le
mot existe ou non dans le dictionnaire, la page comportera la notation « OK » ou « Already Exist ».
Dans le cas où l’utilisateur désire lister les mots du dictionnaire, nous générons la page précédente.
Chaque mot est présent sur une ligne précédée d’une puce.
Si le mot est trop long, une page d’erreur est générée.
Si l’utilisateur entre une mauvaise URL, la page d’erreur précédent est générée.
Comme tout démon qui se respecte, Midas dispose d’un script bash permettant de le lancer proprement,
de connaître son statut d’exécution, ou encore de l’arrêter. De plus Midas respecte les grandes règles
des démons. Ainsi lors de son démarrage il va crée un fichier .pid permettant à tout moment de
connaître le PID du démon. Ce fichier est crée dans le répertoire /tmp/Midas/Midas.pid.
De même, un fichier de log permet à Midas de rester cohérent en ne polluant pas la sortie standard de
message d’erreur, surtout que c’est un démon. A l’aide de dup2() nous redirigions donc les sortie
standard et d’erreur dans le fichier /tmp/Midas/Midas.log.
Midas s’exécute en toute autonomie et n’est pas attaché au terminal qui l’a lancé. Cela est réalisé par
l’appel à la fonction setsid() qui crée une nouvelle session d’exécution pour le processus Midas. Ainsi
nous pouvons lancer Midas et fermer la console qui l’a lancé, sans pour autant mettre fin au processus
Midas, car c’est un démon.
Afin de simplifier la vie de l’utilisateur, deux scripts bash supplémentaire permettent l’installation de
Midas et sa désinstallation sans soucis. Le but étant de créer un répertoire $HOME/Midas contenant les
commandes unix ./get, ./put, ./list et le démon ./midas mais également un script bash ./Midas pour le
lancement du serveur. Le script de désinstallation supprime quant à lui le dossier $HOME/Midas.
Si nous étions root sur les machines de la fac, nous aurions pu faire écrire les commandes dans
/usr/local/bin afin qu’elle se trouvent dans le PATH et soit exécutable de n’importe où. De plus le script
bash aurait été placé dans /etc/init.d/midas. Cela nous aurait permis d’être totalement conforme à
l’installation d’un démon Unix.
VI. Durant la phase de développement du projet Midas, nous avons rencontrés quelques difficultés. Tout
d’abord pour obtenir un sélecteur qui fonctionnait a peu près, cela nous a pris plusieurs jours. Il était
nécessaire que le noyau du programme soit stable et sans failles. Une fois que Midas répondait aux
requêtes HTTP, nous avons essayé de faire une version basique, sans gestion des différentes versions
d’HTTP. Bizarrement nous avons commencé par l’implémentation du mode chunked, qui nous paraissait
logique. Mais le débogage d’un démon tournant donc en tâche de fond sur un système n’est pas une
chose facile. Bien souvent le programme faisait des buffers overflow assez difficile à détecter. Quoi qu’il
en soit, nous avons réussit a complètement stabiliser le démon Midas.
Au terme du développement de ce projet, nous pouvons dire que le démon Midas fonctionne très bien.
Nous avons implémenté les fonctionnalités minimales pour faire de Midas un serveur Web poli et
respectueux des normes HTTP existantes. Nous avons testé l’accès à notre service de dictionnaire avec
plusieurs navigateurs (Firefox, Internet Explorer, Opera, Safari…) et aucun problème n’a été détecté.
Nous avons stress-testé Midas par l’envoi massif de multitudes de requêtes par seconde grâce à des
scripts en python, et là encore aucun problème n’a été détecté.
Ce projet nous a donc permis de mettre en application une grande partie des notions de programmation
système acquises lors des séances de cours et TD. Ce projet à tendance réseaux était très intéressant,
mais aurait pu l’être encore plus si nous avions eu plus de temps. En effet, pressé par le temps nous
n’avons pas réalisé tout ce que nous voulions. Il aurait par exemple pu être intéressant de gérer une
page index.html d’accueil, ainsi que l’icône favicon.ico, tel un serveur Web professionnel.