YooGoo

 
 
Bienvenue sur YooGoo
 
 

 
 
News

Description du Projet
Avancement du Projet
L'équipe
Photos du Projet et du reste
 
 

 
 
Téléchargement

Le Client
Le Serveur
Les Sources Serveur
Les Sources Client
Cahiers des Charges
Soutenance 1
Soutenance 2
Soutenance 3
Soutenance Finale
 
 

 
 
Contacts

Franck Tetzlaff CV Tessari Marco CV Bouhelier Stéphane CV Pouiller Jérôme CV
 
 

 
 

\includegraphics{YooGoo-Line.eps}
Rapport de deuxième soutenance

BOUHELIER Stéphane alias Folkensama (Spé C1)
POUILLER Jérôme alias Hargos (Spé C1)
TESSARI Marco alias Zapata (Spé C1)
TETZLAFF Franck alias Danao (Spé C1)

EPITA - Février 2002


Table des matières

Introduction

Description

De nos jours, le chat occupe une grande partie de l'Internet, on y trouve une grande communauté de chatteurs. Malheureusement les logiciels actuellement disponibles ne nous satisfont pas pleinement. C'est pourquoi nous avons décidé de créer notre propre serveur chat avec un protocole qui lui est propre ainsi qu'une nouvelle communauté : The YooGoo Community. En effet notre projet qui sera intitulé YooGoo. Le nom YooGoo ne signifie rien de spécial, c'est un mot sorti tout droit de l'esprit épitéen de ses concepteurs. Nous nous sommes un peu inspiré de Google et de Yahoo! Ce nom a le mérite d'être facile à retenir. De plus YooGoo.com était un des rares noms de domaine disponibles. YooGoo est un projet de chat sécurisé complet. Il intègrera un client et un serveur. Nous devrons aussi pour les besoins du projet recréer un protocole.

État du projet

Lors de la précédente soutenance nous avions terminé la phase de conception en réalisant le protocole et les plans de l'architecture du serveur. Nous avions aussi réalisé divers outils tels que la classe users et canal, ainsi que les différents types abstraits utiles pour la base de données. Une ébauche du serveur avait été faite, gérant les connexions, et l'échange d'informations sécurisées.
Pour cette deuxième soutenance, d'après le cahier des charges, il nous fallait réaliser un serveur permettant de faire les tâches de base d'un serveur de chat. Cela passait par la réalisation des commandes de gestion des users du protocole, par la mise en fonction de la base de données, et par un serveur qui permet de gérer toutes les requêtes des utilisateurs. Il nous a donc fallu travailler en groupe pour mettre ensemble les diverses parties réalisées précédemment. Pour obtenir ce que nous voulions, il nous s fallu faire quelques changements dans l'architecture que nous avions établie, mais au final, nous avons obtenu un serveur gérant toutes les fonctions de bases de notre protocole. De plus pour profiter, de toutes les fonctionnalités du serveur il a été réalisé un client basique qui sera à l'origine du futur client YooGoo.
Nous pouvons dire que nous sommes en avance sur notre planning car, les fonctions de gestions des canaux étaient considérées comme des fonctions avancées à réaliser plus tard. De plus la nécessité de faire un client graphique pour tester le protocole nous a fait prendre une certaine avance sur la réalisation du futur client YooGoo. Le serveur quant à lui semble assez stable et seul quelques petite retouches pour une administration plus puissante vont être réalisées. C'est pourquoi le serveur YooGoo est déjà considéré comme terminé : il est en version 1.0.

Qui a fait quoi?

Danao
s'est occupé de la gestion des ressources système, c'est-à-dire des threads. Il s'est aussi intéréssé à l'initialisation de la base de données. De plus il s'est occupé de l'élaboration du premier client graphique.
Folkensama
a réalisé la base de données qui gère les différents utilisateurs, connectés et enregistrés. Celle-ci s'appuie sur plusieurs avls.
Hargos
après avoir conçu le protocole, il l'a codé. Il se sert des classes user et canal précédemment réalisées.
Zapata
a travaillé sur le serveur et l'intégration des diverses parties. De grosses modifications sur la parties winsock ont été faites, et de petits outils tels que les logs et le parseur ont été réalisés.

La nouvelle architecture du Serveur

Nous avons dès le début du projet pensé à la façon dont nous allions concevoir YooGoo et plus particulièrement le serveur. Le serveur doit pouvoir gérer un nombre illimité de connections. Il doit aussi pouvoir gérer les canaux et n'oublions pas la base de données. Une esquisse de l'architecture nous est donc venue à l'esprit.

Les différents Threads

Tout d'abord chaque utilisateur est lié à un thread. Ainsi, ils sont quasiment indépendants les uns des autres. Dans chaque thread est instancié un objet user. Un thread principal s'occupe d'instancier les threads user et de créer les connections. Nous avions aussi prévu un thread pour gérer les canaux. Nous gérerions tous les canaux par une seul thread. Ainsi, tous les objets canal sont instanciés dans la même thread. Les objets users et canal permettent d'avoir une interface simple à utiliser dans un environnement possédant plusieurs programmeurs. Les threads peuvent communiquer entre eux à l'aide d'une file de messages. Ainsi, si un thread user reçoit une demande qui doit être traitée par le thread canal, un message est envoyé dans la file de celle-ci. L'utilisation d'une file permet d'avoir plusieurs messages en attente de traitement. Dès qu'un thread reçoit un message, celui-ci est parsé puis la fonction protocole est appelée. Celle-ci s'occupe de vérifier la validité des arguments et réorienter les messages dans les bonnes files.

La Base de données

Chaque thread peut avoir besoin d'accéder à la base de données. La base de données est donc un objet global au serveur. N'importe quel thread peut appeler une de ses fonctions. De plus, permettre l'accès à la base de données à plusieurs threads permet de faire des recherches en parallèle. Effectivement, rien n'empêche d'appeller la fonction de recherche alors qu'une autre recherche est en cours. Il faudra juste faire attention lors de l'ajout/suppression. Effectivement, si on essaye d'accéder à une partie de l'arbre en cours de rééquilibrage, on risque d'obtenir des résultats impévisibles tels que ``segmentation fault''. Pour gérer ces problèmes, Windows possède une série d'outils: les mutex (cf partie sur les Mutex). La base de données sert en premier lieu à récupèrer les pointeurs sur les users/canaux quand on a le nom. Effectivement, lorsqu'un utilisateur enverra un message, il fera référence aux users/canaux par leur nom. Nous aurons donc souvent besoin de la base de données pour récupèrer le pointeur associé. Elle sera ensuite utilisée pour faire des recherches multicritère sur les utilisateurs. Il y a donc 4 arbres pour gérer les recherches multicritères : pseudo, sexe, date de naissance et ville. Effectivement, l'expérience nous montre que 95% des recherches sont du genre "personne de sexe opposé de mon age et pas trop loin de chez moi" (le fameux "asv?"). Ces quatre champs doivent donc être stockés en mémoire. Mais, chaque utilisateur possède un profil évolué semblable à celui d'ICQ qui contient beaucoup plus d'informations. Il n'est pas possible de mettre ces informations en mémoire primaire à cause de leur taille. Nous les stockons donc en mémoire secondaire. De plus ces informations n'ont pas souvent besoin d'être lue. Elles ne sont lues que lorsque que l'on fait un "who nick" (Cette commande permet de rapatrier le profil de 'nick'). Nous faisons aussi une sauvegarde des canaux permanents sur le disque à chaque arrêt du serveur. Elle est ensuite restaurée à chaque démarrage du serveur.

Les couches primaires du serveur

N'oublions pas non plus les couches de Cryptographie et Winsock. Dès que la couche Winsock reçoit un message, elle le transmet à la couche cryptographie. Celle-ci s'occupe de le décrypter. Ensuite, le message est placé dans la file du User concerné. De même lorsqu'on envoie un message, la couche cryptographie s'occupe de le crypter juste avant de l'envoyer. Ces deux couches sont pour le reste de l'application totalement transparente.

Au lancement du serveur

Lors du lancement du serveur, le thread principal est initialisé. Puis, les canaux enregistrés et les utilisateurs enregistrés sont remis en mémoire. Ensuite, les arbres sont reconstitués. Enfin, on initialise le thread canal. On attend une connexion. Lors d'une connexion, on initialise tout d'abord la connexion cryptée. Ensuite, on initialise un thread user. Puis, on vérifie si l'utilisateur est enregistré. Si c'est le cas, on demande le mot de passe et met à jour son profil. Enfin, on indexe l'utilisateur dans les arbres et on attend une commande. Lorsqu'une commande est reçue la couche Winsock la transmet à la couche cryptographie qui la décode et l'enfile dans l'objet user correspondant. L'objet user traite la commande en l'envoyant dans la fonction protocole. Cette fonction s'occupe alors de vérifier les arguments et la validité de la commande. Si la commande la concerne directement, elle la traite, sinon, elle la renvoie à la bonne file.

La nouvelle architecture

Mais cette architecture possède quelques inconvénients. Tout d'abord, la connexion Winsock multithread bloquant ne nous convient pas (voir partie sur Winsock). Ensuite, les threads permettent un meilleur rendement à condition de ne pas en abuser. Les programmes possèdent généralement une dizaine voire une vingtaine de threads. Or, nous avons ici un thread par utilisateur. Par conséquent, un serveur YooGoo moyen accueillant entre 500 et 1000 utilisateurs contient autant de threads. Nous avons donc changé d'architecture pour les threads user. Il y a maintenant un thread pour 64 utilisateurs. Ceci nous permet de limiter le nombre de threads et d'utiliser le mode socket non-bloquant de Winsock. Néanmoins, nous gardons une file de messages par utilisateur. Ensuite, nous avons remarqué que communiquer avec le thread canal nous oblige à refaire un protocole interne pour ces messages. Il est bien plus simple d'appeler les fonctions de la classe canal directement à partir de la fonction protocole pendant le traitement des messages (donc, dans un thread user).

Winsock, du bloquant au non-bloquant

Le problème des sockets bloquants

Comme nous l'avons vu au paragraphe précédent, l'ancienne architecture de YooGoo était basée sur des sockets en mode non-bloquant. Celui-ci nous semblait le meilleur choix parce qu'il permettait de gérer les utilisateurs indépendamments car chacun avait son propre thread. De plus cette technique était la plus facile à mettre en oeuvre. L'inconvénient de cette architecture, d'un point de vue réseaux, est qu'un thread est complètement bloqué lorsqu'il essaye de recevoir un message alors que rien arrive sur le réseau. Ceci pose un problème car la file des messages, traitée dans un autre thread, essaye peut être d'envoyer des messages à cet utilisateur. Malheureusement son socket est bloqué car on attend toujours la réception d'un quelconque message. De plus un autre problème se présentait quand on essayait d'envoyer deux messages en même temps au serveur. Celui ci se trompait dans la réception du message, il perdait un ou deux octets, et du coup le message, puisqu'il est crypté, était complètement illisible.
De même pour le client nous pension, malgré le changement d'architecture du serveur, le laisser en sockets bloquants. Car, le client ne pouvait envoyer qu'un message à la fois et devait forcement attendre la réponse avant de pouvoir exécuter une autre commande. Mais ceci pose problème pour des commandes à plusieurs réponses tel qu'une recherche. Par ailleurs, le client peut recevoir des messages alors qu'il n'a exécuté aucune commande, lorsqu'un utilisateur rejoint un canal, un message d'information est envoyé à tous les utilisateurs connectés à ce canal.

Le choix du non-bloquant

Pour remédier à tous ces problèmes il fallait trouver un mode winsock nous permettant de ne pas être bloqué lors de la réception d'un message. D'après les recherches qui ont été effectués lors de la précédente soutenance, le meilleur choix était d'utiliser des sockets non-bloquants, couplés avec la fonction select() qui permet de savoir s'il y a de l'activité sur un des sockets surveillés. Cette méthode malgré qu 'elle soit plus lente nous permet de ne jamais être bloqué lors d'une réception ou d'un envoie de message. Lorsqu'on envoie (ou reçoit), winsock va faire un appel à la fonction send() (ou recv()) et va envoyer (ou recevoir) le plus de données en un coup, et même si tout le message n'est pas envoyé, il va continuer l'exécution du programme. C'est au programmeur de gérer l'envoie de la fin du message qui n'a pas été envoyé.

La mise en oeuvre

Changements dans l'architecture du serveur

Ce changement de technique Winsock influe dans l'architecture du serveur au niveau du nombre de threads. En effet, au lieu d'avoir un thread par utilisateur, nous avons un thread pour 64 utilisateurs (voir " Problèmes des sockets non-bloquants. ") Ceci est réalisé grâce à la fonction select(). Pour utiliser la fonction select() il faut créer des ensembles de sockets à surveiller.
Dans chaque thread winsock on retrouve le même fonctionnement. Il y à un tableau statique de 64 pointeurs sur user, le socket qui sert à l'écoute pour savoir s'il y a des connections et trois ensembles de sockets. Un ensemble pour les sockets en écriture (envoie), un pour les sockets en lecture (réception), et un pour les sockets ayant des erreurs. La boucle principale est celle de l'attente de connexions, tout le reste est identique à la version socket bloquant.
Tout d'abord, il faut mettre à jour les ensembles. Le socket d'écoute des connections est mis dans l'ensemble d'erreur et d'écoute (sauf s'il y a déjà 64 utilisateurs gérés dans ce thread). Ensuite on boucle sur tous les utilisateurs gérés dans ce thread. Si l'utilisateur à des messages à envoyer alors son socket est mis dans l'ensemble des sockets en écriture, si l'utilisateur à encore de la place pour recevoir des messages alors son socket est mis dans l'ensemble des sockets en lecture. Tout les sockets sont mis dans l'ensemble des sockets pouvant avoir des erreurs. Cette mise à jour des ensembles est faite à chaque tour de boucle. C'est un des inconvénients de la fonction select().
Ensuite, on appelle la fonction select() avec les trois ensembles précédemment crée. On met un " timeout " à la fonction select(), car si on attend indéfiniment qu'il se passe quelque chose, il se peut qu'il y ait eu des changements dans les utilisateurs, que, par exemple, un utilisateur ai besoin d'envoyer un message, alors que les ensembles n'ont pas été mis à jour, cet utilisateur devra alors attendre qu'il se passe quelque chose dans l'actuelle configuration des ensembles. Ceci pose d'autant plus de problème s'il n'y a qu'un utilisateur connecté. Tout ceci peut se produire uniquement parce qu'il y a un décalage entre le moment où l'on reçoit le message et le moment où le message est traité. On préfère alors rafraîchir les ensembles régulièrement même s'il ne se passe rien. Lorsque la fonction select() attend un événement le thread est mis en pause et donc il n'utilise pas le processeur.
S'il se passe quelque chose sur un socket, la fonction select() renvoie le nombre de sockets en activité. Malheureusement on ne connaît pas le(s) socket(s) qui est(sont) en action. On doit alors vérifier tout les sockets. On commence par le socket en écoute de connections. S'il est actif c'est que quelqu'un essaye de se connecter. On va alors lancer la procédure de connexion (voir " connexion et déconnexion "). Ensuite on vérifie le socket de chaque utilisateur. S'il est actif en lecture on va appeler la fonction de réception des données, s'il est actif en écriture on va appeler la fonction d'envoie des données. A chaque fois on teste s'il y a des erreurs sur certains sockets, si oui on gère ces erreurs. Si l'erreur est grave (on a perdu la connexion), on va déconnecter l'utilisateur.
Enfin on recommence la boucle. Cette boucle continue jusqu'à l'arrêt du serveur. C'est ce qu'on appelle un thread winsock. Il ne s'occupe donc que de la réception, de l'envoient des messages des utilisateurs et de gérer les connexions et les déconnexions.

Changements dans la méthode d'envoient et de réception des données

La majeure différence entre le mode bloquant et non-bloquant se situe au niveau de l'envoie et de la réception des données. En effet, en mode bloquant, comme son nom l'indique, le programme, et winsock, est bloqué quand on envoie ou on reçoit des données. En mode non-bloquant, le programme lance la procédure de lecture ou d'écriture et renvoie tout de suite la main au programme. Il faut donc faire attention car toutes les données n'ont peut-être pas été envoyés, et il faudra reprendre plus tard au bon endroit. Pour cela il a fallut ajouter des champs à la classe user : on a donc un " buffer " d'envoie, un " buffer " de réception, le nombre d'octets dans chacun de ces buffers, et la taille du message en cours de réception.
On a découpé la gestion du transfert des messages en trois fonctions.
*
La fonction NetRecv() reçoit les messages qui sont sur le socket tant qu'il y a de la place dans le " buffer " de réception. Il faut à chaque appel de la fonction vérifier à quel point du message on est. On reçoit d'abord les données. Si on a une taille de message à recevoir égale à zéro alors on n'a pas encore commencé à recevoir de messages. Les deux premiers octets sont donc utilisés pour lire la taille du message qui arrive. On supprime ensuite ces deux octets du "buffer". Si le nombre d'octets dans le buffer est supérieur ou égal à la taille du message qu'on est en train de recevoir, on décrypte le message, on l'enfile dans la file de message de l'utilisateur et on enfile l'utilisateur dans la file des utilisateurs qui ont des messages. C'est le thread qui gère les utilisateurs qui traitera ces diverses files. A ce moment on boucle, on regarde si on a deux octets pour avoir la taille du prochain message et on regarde si on a assez d'octets pour recevoir le prochain messages aussi. La fonction se termine dès que le nombre d'octets dans le buffer n'est pas suffisant soit pour lire la taille du prochain message, soit est inférieur à la taille du message que l'on est en train de recevoir. Ce qu'il faut bien comprendre c'est qu'à chaque appel de la fonction on reprend là où l'on s'était arrêté à l'appel précédent, car la fonction peut se terminer dans n'importe quel état.
*
La fonction NetEnqueu() sert à ajouter un message dans le " buffer " d'envoie de l'utilisateur. La fonction s'occupe de crypter le message, et ajoute la taille du message dans le buffer, suivit du message.
*
La fonction NetSend() ne fait rien d'autre que d'envoyer les données qui sont dans le " buffer " d'envoie.
Dans toutes ces fonctions il faut faire attention au débordement de " buffer ". La fonction renvoie le nombre d'octets qui sont soit envoyés, soit reçus, soit enfilés. Si la valeur de retour est zéro c'est qu'il y a eu un problème de "buffer" et il faudra donc réessayer plus tard quand il y aura de la place dans le buffer (ou alors découpé le message en plusieurs morceau si on veut l'envoyé). Si la valeur de retour est -1 c'est qu'il y a eu une erreur de socket et dans ce cas l'erreur du socket sera géré par la boucle principale.

Les problèmes du non-bloquant

Comme d'habitude aucune méthode n'est parfaite à 100%. Tout comme le bloquant le non-bloquant à ses avantages et ses inconvénients. L'un de ces inconvénients majeur est qu'il ne traite que 64 utilisateurs à la fois. Ceci est dû à Windows (et Unix aussi) qui sont des systèmes 32 bits. La gestion des événements Win32 (ex : WaitForMultipleObjects() (voir partie sur les threads) ne peut attendre que 64 objets en même temps. De plus la fonction select() nécessaire aux sockets non-bloquants, est aussi limité à 64 sockets par appel. La constante FD_SETSIZE définie la taille maximum d'un ensemble de socket que l'on passe à la fonction select(). Cette constante est fixée à 64. Si l'on essaye de modifier cette constante dans winsock.h c'est alors une des piles d'une autre couche réseaux qui va poser problème car souvent on suppose la valeur à 64. Les autres sockets seront alors ignorés.
La solution à ce problème est alors de crée plusieurs threads gérant chacun 64 utilisateurs. Quant un thread arrive à 64 utilisateurs, il bloque le socket en écoute de connexion, et il crée un nouveau thread qui va gérer les connexions. Tandis que les threads de 64 utilisateurs ne font rien d'autres que s'occuper d'envoyer et de recevoir les messages des utilisateurs. Le problème se pose quand il y à des déconnections. Il faut alors remplir les trous qui se créent dans les threads. Certains utilisateurs sont alors déplacés de la dernière thread qui gère les connexions vers les autres. Un threads est détruit lorsqu'il n'a plus d'utilisateurs.

Connexion / deconnexion

Connexion

Lors de la précédente soutenance une partie de la procédure de connexion a été expliquée : l'échange de clé. L'échange de clé est le premier échange entre le client et le serveur. C'est normal car tous les messages doivent être cryptés, et si le serveur n'as pas la clé DES du client il ne peut pas décrypter ces messages. Si le client n'envoie pas la clé comme il se doit la connexion est tout simplement refusée. Aucun message d'erreur n'avertit le client. Son socket sera tout simplement fermé.
Après l'échange de la clé on crée un utilisateur qui pour l'instant est temporaire, il a juste une clé DES, un socket et tout le système pour envoyer et recevoir des messages. Son statut est alors offline. Tout ceci se passe dans le thread winsock.
L'utilisateur n'est officiellement connecté que lorsqu'il a envoyé son Nick. Ceci équivaut à l'envoie de la commande " connect Nikc [Pass] ". Etant données que les messages sont traités dans le thread utilisateurs et non dans le thread winsock, c'est dans le thread utilisateur que la fin de la connexion se fait. Ainsi le serveur n'est jamais bloqué car on attend pas de message du client, si celui ci ne se connecte jamais il ne pourra rien faire sur le serveur, à chacune de ses commandes il recevra un message d'erreur. Pour savoir si l'utilisateur est connecté, on regarde son status. Le seul moment où l'on peut recevoir un message d'un offline, c'est lorsqu'un utilisateur est en train de se connecter.
La fonction de connexion se déroule ainsi :
connect (user U, chaine nick, chaine pass = NULL)

    si nick n'est pas un pseudo valide alors
        retourne Err_NickNotAccepted

    si pass n'est pas un mot de passe valide alors
        retourne Err_InvalidArguments

    si nick existe dans la base de données alors
        si nick est online alors
            retourne Err_NickInUse
        sinon
            si le pass est bon alors
                On met l'utilisateur online
                on recopie le socket,la clé DES
                on recopie les buffers
                on supprime l'utilisateur temporaire
                on met le pointeur du user dans le
                tableau des connectés.
                retourne Inf_Ok

            sinon
                retourne Err_Password
    sinon
        on complete les champs de l'utilisateur temporaire
        on l'ajoute dans la base de données
        on met l'utilisateur online
        retourne Inf_NickUnregistered
Au final on obtient une base de données contenant tous les utilisateurs : tous les déconnectés sont les utilisateurs enregistrés, les connectés sont des utilisateurs enregistrés et des visiteurs. Dans les threads winsocks on a à chaque fois un tableau de 64 utilisateurs connectés.

Déconnexion

La déconnexion se déroule sur le même principe que la connexion. Une première partie dans la thread utilisateur, puis une deuxième dans la thread winsock. La première partie dans la thread utilisateur est toute simple. Lorsqu'on reçoit le message " disconnect " on met le status de l'utilisateur à " STAT_DISCONNECTING ". Puis dans la thread winsock, quand on traitera cet utilisateur on verra qu'il veut se déconnecter, il sera alors mis dans le cas d'une erreur de socket et la procédure de déconnexion classique sera lancée.
La procédure de déconnexion se déroule ainsi :
    si l'utilisateur n'est pas enregistré alors
        on le suppime de la base de données
        on libère la memoire
        on le supprime du tableau des connectées
    sinon
        on le met offline
        on libère les buffers
        on met sa cle DES à NULL
        on le supprime du tableau des connectées

Un serveur opérationnel

Le serveur est maintenant presque terminé, et largement utilisable. Il permet déjà l'utilisation de toutes les fonctions de base. On trouve donc plusieurs threads qui communiquent entre elles grâce à une base de données. Cet échange d'informations a nécessité du travail sur les threads, sur la protection des accès en mémoire et sur la bonne gestion du processeur. La base de données est le centre de tout le serveur et a nécessité une mise au point laborieuse. Grâce à elle, le protocole a pu être mis à l'épreuve, après avoir été codé bien sûr. Nous allons donc voir une introduction sur le multithread, et comment fonctionne la base de données. Nous verrons aussi comment a été réalisé le protocole.

Introduction sur le Multithread

Définitions

Multithread

Le multithread était à la base créé pour les systèmes informatiques basés sur plusieurs processeurs. L'idée était de donner un groupe d'information à traiter à chaque processeur. Le système d'exploitation a donc un rôle primordial dans le multithread car c'est lui qui définit la planification des processeurs.
Le principe de l'exécution en parallèle est quelque chose de relativement nouveau et fonctionne aussi très bien sur des configurations munies seulement d'un processeur. Pour cela le système d'exploitation regroupe un certain nombre de processus qui sont les programmes en cours d'exécution regroupant eux-mêmes des threads.
L'OS accorde un certain laps de temps pour l'exécution d'un thread en fonction de ses priorités. Avec ce principe on comprend mieux ce que signifie l'exécution en parallèle bien qu'il ne s'agisse pas vraiment de parallélisme dans un système monoprocesseur.

Thread

Un thread est une séquence d'instructions exécutées à l'intérieur du contexte d'un processus. Par exemple dans notre serveur, on trouve à l'intérieur du thread canal, exclusivement des instructions visant à gérer les canaux disponibles.

Programme Monothread

C'est un programme codé en séquentiel, c'est-à-dire que les instructions sont exécutées les unes après les autres. Il y a un thread pour un processus.

Programme Multithread

C'est un programme utilisant plusieurs threads au sein d'un même processus et qui partage un même espace mémoire.

MT-Safe

Pour un programme multithread on parle de MT-Safe lorsqu'il fonctionne correctement alors qu'il partage des données "sensibles". Pour une librairie c'est à peu près le même principe. Elle est MT-Safe lorsque chaque appel de fonction n'engendre aucune perte de donnée dans le programme.

User-level Thread

Comme leur nom l'indique les threads User se situent au niveau de l'utilisateur c'est-à-dire au niveau du programme. Les User-level Threads ne sont visibles qu'à l'intérieur du processus dans lequel ils partagent toutes les ressources du processus (espace d'adressage ...) Chacun de ces threads possède les informations suivantes :
*
Un unique identificateur (Thread Id)
*
Etat du registre.
*
Ressource nécessaire pour supporter un flux de contrôle telle que la pile.
*
Une priorité et une planification.
*
Un emplacement spécial pour stocker des données particulières telles que les numéros d'erreur.
Ils sont rapides à synchroniser lorsque la synchronisation s'effectue au niveau utilisateur et non au niveau du kernel sans dépendance avec le système d'exploitation. Facilement gérable par la librairie des threads. Ce sont les threads gérés par les programmeurs d'applications.

Kernel-level Thread ou Lightweight processes (LWPs)

Les Kernel-level Thread comme leur nom l'indique se situent au niveau du kernel. Leur rôle est de faire le lien entre les User-level thread et le matériel (CPU). On les trouve beaucoup plus souvent sous le nom de Lightweight Process(LWPs). Il n'y a pas de mappage direct entre le thread user et le LWP. Le thread user doit être capable de changer librement de LWP dans la zone des LWPs. C'est le système d'exploitation qui décide quel LWP doit utiliser quel processeur et quand. Ils sont donc gérés par l'OS et non par le programme.

Bound et Unbound Thread

Ce sont des threads qui sont gérés, planifiés dans l'espace des PWLs. Les Unbound Threads sont plus légers que les PWLs. De plus, seulement si cela est nécessaire, un thread utilisateur peut être attaché définitivement à un PWL, c'est un bound thread.
Il existe une énorme différence dans la création et la synchronisation entre les bound threads et les unbound threads.

Tableau: Ce tableau illustre les différences à la création
Création Micro Secondes Rapport
Unbound Thread 101.0 -
Bound Thread 422.0 4.2
Fork() 3057.0 30.3



Tableau: Ce tableau montre lui les différences à la synchronisation
Création Micro Secondes Rapport
Unbound Thread 1.8 -
Bound Thread 48. 26.7
Fork() 105. 58.3


Il est donc important d'utiliser les Unbound thread le plus souvent possible. Les Unbound threads sont les threads créés par défaut.

Concepts de la programmation multithread

Tout d'abord pour créer un programme multithread, le programmeur doit raisonner en pensant au model multithread sans en abuser. C'est-à-dire qu'il est inutile de mettre des threads partout dans le programme, mais il faut savoir en placer là où il y a nécessité. Un programme doit utiliser les threads pour faire apparaître les activités parallèles à travers :
1)
La décomposition géométrique, c'est-à-dire répartir les données que le programme utilise, et faire en sorte que chaque processeur s'occupe d'une zone de calcul prédéfinie. La décomposition géométrique découpe le projet en grosses parties.
2)
La décomposition des fonctions, c'est-à-dire répartir les fonctions de telle sorte que chaque processeur s'occupe si possible d'une zone de calcul. Dans cette décomposition le projet est encore redivisé.
C'est après avoir effectué ces décompositions qu'on peut savoir quelle partie du projet aura besoin de threads.
La librairie des threads est indispensable pour :
*
Créer et assigner des threads aux activités parallèles de l'application
*
Organiser la coopération et la synchronisation des threads lorsqu'elles accèdent à des données partagées ou des ressources du processus.
*
Faire en sorte que le programme multithread utilise le mieux possible une machine multiprocesseur.

Création et gestion des threads

Pour pouvoir utiliser les threads dans notre code, il y a un certain nombre de fonctions à savoir utiliser. Il y a la création de thread, la destruction, la mise en pause, la reprise et la synchronisation... Pour ce qui est de la synchronisation, il faut savoir utiliser d'autres objets qu'on nomme objets de synchronisation. Il peut s'agir des mutex ou des sémaphores. Il faut alors savoir les créer et les implémenter là où il faut dans le programme.

Création de threads

Pour créer un thread sous Windows, il faut appeler la fonction CreateThread qui se trouve dans l'API Windows. Tout ce qui va être dit ne sera vrai que sous le système d'exploitation de Microsoft. En ce qui concerne les autres systèmes d'exploitation, un paragraphe sera consacré au portage du serveur sous Unix. La fonction CreateThread() prend 6 paramètres.
*
Le premier est la taille de la pile qui sera attribuée au thread.
*
Le second est l'attribut de sécurité, qui sert à définir par exemple qui à le droit de détruire le thread.
*
Le troisième est l'adresse de démarrage du thread, on passe à ce paramètre l'adresse d'une fonction du programme.
*
Le quatrième représente les valeurs communiquées au point de démarrage du thread, c'est-à-dire les paramètres de la fonction principale du thread.
*
Le cinquième indique la priorité du thread. Ce paramètre est utile pour le système d'exploitation.
*
Le dernier paramètre sert à stocker un identificateur de thread.
Cette fonction retourne 0 en cas d'échec ou le handle du thread en cas de réussite.
Dans le serveur YooGoo la création de thread intervient au chargement du serveur pour la création des threads canal et user. La partie communication, Winsock, se trouve également dans un thread. Par ailleurs la thread Winsock a la possibilité de créer des threads lorsque le nombre de connectés dépasse un multiple de 64. Notre serveur compte donc au minimum 2 threads.

Destruction d'un thread

Pour sortir du thread en cours, il est possible d'utiliser la fonction ExitThread(), qui ne prend aucun paramètre et qui est l'équivalent d'un break.
Mais parfois il est utile de pouvoir terminer un thread sur ordre d'un autre. Par exemple pour l'extinction du serveur, le thread principal donnera lui-même l'ordre de détruire les threads encore actifs. Pour faire cela il utilise la fonction TerminateThread().
Cette fonction est assez simple et ne prend que 2 paramètres :
*
L'handle du thread à terminer
*
Le code de sorti du thread concerné.
Les valeurs de retour sont TRUE ou FALSE. Le code de retour est donc une valeur booléenne définit dans la librairie winbase.h.
Dans le serveur la destruction ne s'effectue donc que lorsque le serveur s'éteint ou lorsque le nombre d'utilisateur passe en dessous d'un multiple de 64.

Définir la priorité

La priorité d'un thread est quelque chose de vraiment important car c'est une information qui interagit directement avec le système d'exploitation. Il est possible de voir les différentes priorités attribuées à des programmes à travers le gestionnaire des taches de Windows par exemple. Dans notre projet, il n'a pas encore été pensé de définir une priorité aux threads, mais cela pourrait peut-être changé. Le programme qui est présenté à la soutenance utilise lui le système de priorité.
Pour définir une priorité, le plus simple est d'attribuer une priorité à la création du thread car il n'est pas recommandé de changer la priorité d'un thread durant son exécution. Cependant comme le montre le petit programme test, cela est tout à fait possible. Pour cela, il faut utiliser la fonction SetThreadPriority() qui prend les paramètres suivants :
*
Le handle du thread
*
La valeur de la priorité
Il existe des constantes textes déjà préfaites dans la librairie winbase.h mais il faut savoir que le niveau de priorité sous Windows s'étale jusqu'à la valeur 31 qui correspond à la valeur RealTime fortement déconseillée car il devient alors quasiment impossible d'effectuer quelque chose d'autre avec l'OS pendant ce temps là. La priorité peut également être négative pour désigner une utilité moindre.

Statistiques

Les threads pourront nous aider en ce qui concerne les statistiques. En effet les threads regroupent un grand nombre d'informations assez importantes aussi bien pour les personnes qui utiliseront YooGoo que pour nous programmeurs. Tout d'abord à l'intérieur du processus, il est possible de savoir pour chaque thread :
*
Sa date de création
*
Le temps d'occupation du processeur
*
Et le mode dans lequel il a utiliser le processeur (Utilisateur ou Superviseur)
Toutes ces informations utilisées intelligemment permettront par exemple de savoir depuis combien de temps le serveur YooGoo tourne, quel thread est le plus souvent demandé... Cette dernière information pourra nous permettre d'effectuer des mises à jour plus fines car nous sauront ce qui fait ralentir le serveur et ce que nous devons optimiser.
Pour obtenir toutes ces informations, il suffit d'appeler la fonction GetThreadTimes() avec les paramètres suivants :
*
L'handle du thread
*
Variable qui stockera la date de création du thread
*
Variable qui stockera la date de destruction du thread
*
Variable qui stockera le temps passé en mode superviseur
*
Variable qui stockera le temps passé en mode utilisateur
La valeur de retour est un booléen qui est à TRUE si l'opération s'est bien passée.

Suspension et reprise d'un thread

L'un des objectifs de chaque programme et particulièrement du notre est de faire en sorte que lorsque le serveur n'a rien à faire, il n'utilise pas 100% du processeur, à travers une boucle infinie par exemple. Pour cela il faut utiliser la programmation événementielle. C'est ce que nous avons fait pour le serveur mais également pour le petit client que nous présentons pour cette 2ème soutenance.
Tout d'abord avec Visual Studio et C++ Builder, la programmation est événementielle à travers les objets qu'ils proposent. Chaque objet placable que la fenêtre possède un certain nombre d'événements qui lui sont associés. Ainsi lorsqu'on clique sur un bouton de la fenêtre par exemple, cela appelle la fonction associée à l'événement OnClick du bouton.
Il faut également savoir que la programmation événementielle n'existe pas seulement avec ces deux logiciels. Il est possible par exemple sous une fenêtre DOS de pouvoir recréer de l'événementiel en analysant les entrées qui sont présentes dans la fenêtre DOS (entrée clavier / souris par exemple). La fonction select dans la thread Winsock fait également la même chose. Elle bloque un certain temps (en fonction du réglage du timer) et n'utilise que très peu de ressources systèmes.
Enfin la méthode la plus propre est de suspendre un thread lorsqu'il n'a rien à faire. C'est le cas de la thread de la file des messages. Lorsqu'aucun message n'est présent dans les files de messages des utilisateurs il est inutile de faire tourner la thread en boucle, il faut donc pouvoir la mettre en pause. Mais il faut obligatoirement savoir quand la réveiller. Il faut la reprendre lorsqu'un message est enfilé. A chaque défilage on décrémente un compteur de message. Une fois arrivé à 0, on suspend le thread qui n'occupe plus de ressources au niveau du processeur. Et le thread est réveillé après enfilage.
La fonction pour suspendre un thread est SuspendThread(HANDLE hThread) qui retourne -1 en cas d'erreur.
La fonction pour reprendre l'exécution d'un thread est ResumeThread(HANDLE hThread) qui retourne -1 en cas d'erreur.

Synchronisation

Les threads permettent donc de faire des choses qui n'étaient pas possible avant ou difficilement, car l'exécution en séquentielle pouvait bloquer comme c'était le cas dans le client par exemple où la fonction Select figeait l'affichage lorsqu'elle se trouvait dans le thread principal. Cependant il est indispensable de synchroniser les threads entre eux car s'il arrive qu'ils appellent simultanément la même ressource les conséquences peuvent être catastrophiques. Par exemple si une personne modifie un élément de la base de données alors qu'une autre personne essaie de lire la même entrée, le résultat est imprévisible. Il se peut que l'élément renvoyé soit simplement erroné mais il est fort probable que le serveur plante.
Pour cela il est nécessaire d'utiliser des objets de synchronisation. Il en existe plusieurs, mais seulement deux nous intéressent, il s'agit du Mutex et du Sémaphore.

Mutex

Par définition un Mutex autorise l'accès mutuellement exclusif, c'est-à-dire qu'il n'est possible qu'à une personne d'accéder à la ressource partagée. Un mutex ne peut appartenir qu'à un seul thread à la fois. Cependant, si un thread possède un mutex et qu'un autre thread en demande la propriété, le thread demandant ne peut l'acquérir que quand le thread propriétaire libère sa propriété.
Pour pouvoir utiliser les mutex, la première opération à effectuer est de le créer. Pour cela il faut faire appel à la fonction CreateMutex() qui prend trois paramètres :
*
Les attributs de sécurité qui déterminent si le Mutex est " héritable " c'est-à-dire s'il peut être appelé par un thread d'un autre processus par exemple
*
Le thread possesseur initial du thread
*
Le nom de l'objet de synchronisation (ex : " bddMutex ")
La valeur de retour en cas de succès est l'handle du mutex, sinon le code retour est NULL.
Il faut également pouvoir libérer le mutex après la libération de la propriété. Pour cela il faut faire appel à la fonction ReleaseMutex() qui ne prend en paramètre que l'handle du Mutex à libérer. Le Mutex est alors passé au thread en attente s'il y en a.

Sémaphore

Par définition un Sémaphore autorise l'accès à un nombre défini de threads. Un objet Sémaphore démarre avec un compte initial attribué à sa création. Chaque fois qu'un thread devient propriétaire de l'objet (en utilisant la fonction d'attente), le compteur du sémaphore est décrémenté. Chaque fois qu'un thread libère la propriété, le compteur du sémaphore est incrémenté. Si celui-ci est à 0, le sémaphore est bloqué sur l'état non signalé et aucun autre thread ne peut y accéder.
De même que pour les Mutex il faut pouvoir créer l'objet sémaphore. Pour cela il faut utiliser la fonction CreateSemaphore() qui avec le même principe que les mutex prend quatre paramètres :
*
Les attributs de sécurités
*
La valeur initial du compteur
*
La valeur maximum du compteur
*
L'identifiant de l'objet comme pour le mutex
La valeur de retour est l'handle du sémaphore ou NULL en cas d'échec.
Les sémaphores auraient pu nous permettre de limiter l'accès à la base de données dans l'ancienne structure à un nombre maximum d'utilisateurs. Malheureusement cela n'est plus le cas avec le serveur repensé.
Toutefois il est intéressant de remarquer qu'un mutex n'est rien de plus qu'un sémaphore avec un compteur initial à 1 et un maximum de 1. Cette solution va être utilisée pour la compatibilité et le portage sous Unix. De plus c'est la méthode employée dans le petit exemple sur les threads.
La fonction de ReleaseSemaphore() doit être appelée pour désigner la libération de la ressource. Elle prend 3 paramètres :
*
L'handle du sémaphore
*
Un nombre désignant la valeur de l'incrémentation
*
Un paramètre de sortie qui récupère la valeur du compteur avant l'incrémentation
La valeur de retour est 0 en cas d'échec et n'importe quelle autre valeur en cas de réussite.

Implémentation des objets de synchronisation dans le programme

Après avoir créé les objets, il faut pouvoir les appeler juste avant la propriété à protéger. Il faut donc bien veiller à n'oublier aucune zone sensible du programme.
Les tests effectués juste avant la propriété sont en fait des appels aux objets de synchronisation. Ces appels sont effectués par la fonction WaitForSingleObject() qui prend 2 paramètres :
*
L'handle de l'objet Mutex ou Sémaphore.
*
Un time-out qui définit l'attente maximale d'un thread avant de zapper la propriété. Cette valeur est un double mot qui représente une valeur en millisecondes Dans notre cas ce paramètre est égal à 0, ce qui signifie que le time-out est infini.
Il existe 4 valeurs de retour :
*
WAIT_ABANDONED : c'est un message d'erreur dû à un problème de droit
*
WAIT_OBJECT_0 : c'est la valeur en cas de succès
*
WAIT_TIMEOUT : c'est un message d'erreur qui signifie que le délai d'attente est passé et que le programme continue son exécution
*
WAIT_FAILED : c'est un message d'erreur dû au fait que l'handle passé n'est pas valide.
La fonction WaitForSingleObject() n'attend que pour un objet donné, ce qui est suffisant dans notre cas. Pour pouvoir attendre plusieurs objets de synchronisation il faut appeler la fonction WaitForMultipleObject().

Base de données

Types et classes d'objets utilisés dans la base de données

A la dernière soutenance, nous avions présenté une méthode de stockage des données selon le principe des AVL (Arbres de recherches équilibrés de Adelson Velskii et Landis). Pour le classement et la recherche d'éléments a occurrence unique, cela suffisait parfaitement.
Dans le cas de notre base de données, cela ne suffisait plus et il a fallut modifier notre type abstrait pour qu'il convienne à nos besoins : classer un objet selon plusieurs critères. En effet, nous avions vu la dernière fois que l'objet " user " devait être classé dans plusieurs arbres selon ses différentes caractéristiques. Cet objet etait classe selon son " NickName " (terme anglophone signifiant le surnom) dans l'arbre " sort_by_nick " de la base de données, et respectivement par sa date de naissance, sa ville et son genre dans les arbres " sort_by_birth ", " sort_by_city " et " sort_by_gender ".
Ces modifications consistent en un changement du type sur lequel pointent les noeuds des arbres équilibrés : ils pointent sur une liste chaînée d'objets dont les caractéristiques de classement pour cet arbre sont similaires.
Par définition, un AVL considère que si un objet d'un noeud a déjà la même valeur que celui inséré, il n'a pas lieu d'être inséré.
Pensons par exemple à plusieurs utilisateurs du même âge (mais avec d'autres caractéristiques différentes), qui essaient de créer un compte sur YooGoo et qu'ils ne le peuvent pas tout simplement parce qu'un autre utilisateur du même âge est déjà inscrit. Cette idée est inconcevable pour un logiciel tel que YooGoo.
Ce changement est radical et a eu pour conséquence une refonte totale des algorithmes d'ajout, de recherche et de suppression sur les AVL (Rappelons qu'ils restent AVL car ils sont un classement d'utilisateurs selon un critère bien défini et qu'ils restent équilibrés en toute occasion).
Le seul problème, cependant, repose sur la suppression d'un utilisateur.
Comment supprimer un utilisateur, alors que tous les noeuds à sa première occurrence dans les arbres ont les même caractéristiques. Pour remédier à cela, notre équipe a décidé que les utilisateurs auraient un NickName unique qui les différencierait les uns des autres (évidemment, cela demandera de l'imagination de la part des utilisateurs, mais nous ne doutons pas que cela aura une conséquence positive sur le résultat).
Depuis la dernière soutenance, nous avons également rajouté l'arbre des utilisateurs "online" (terme anglophone désignant les utilisateurs en ligne ou encore connectés) totalement gérés par la partie serveur ainsi que l'arbre des canaux. Voici l'aperçu de la classe " database " (terme anglophone signifiant base de données) :
class   database {
    private:
        t_node   *sort_by_nick;
        t_node   *sort_by_gender;
        t_node   *sort_by_birth;
        t_node   *sort_by_city;
        t_node   *sort_by_status;
        t_node   *channels;
        short int   updating;
    public:
        database();
        ~database();
    .
    .
    .
}
L'entier " updating " sert en particulier à savoir si la base de données est en activité critique telles que la suppression et l'insertion. En effet, il ne vaut mieux pas se risquer à lancer une recherche sur un AVL en pleines modifications (au risque de se retrouver sur une branche morte un bien une seconde occurrence de branche). Cette variable n'est qu'une deuxième sécurité ajoutée aux objets de synchronisation et sera peut-être supprimée par la suite.
Les trois points verticaux représentent les fonctions relatives à la base de données que nous verrons plus tard (Recherches, ajouts et autres suppressions).
La fonction database() est la fonction constructive, qui sert donc à construire des objets " database " (elle met tous les arbres à NULL, ils sont vides et elle initialise updating à 0). La fonction  database() est la fonction destructrice d'un objet base de données. (Elle vide tous les AVL et les listes chaînées qui les composent).
Le type " t_node " désigne le type abstrait des AVL. Voici sa déclaration :
typedef     struct s_node {
    t_elt       *data;
    int     balance;
    struct s_node   *lc;
    struct s_node   *rc;
} t_node;
L'entier " balance " désigne le déséquilibre de l'arbre.
Les composants " lc " et " rc " désignent les sous arbre gauche et sous arbre droit du noeud : ils sont bien évidemment implicitement du type " t_node * " (On peut parler de sous fils droit et sous fils gauche).
L'élément " data " de type " t_elt * " et la liste chaînée décrite ci-dessus. Voici la définition du type :
typedef     struct s_elt {
    void       *data;
    struct s_elt    *nxt;
} t_elt;
L'élément " data " de type " void * " est directement le pointeur sur la donnée que l'on veut stocker et classer.
Le " nxt " de type implicite " t_elt * " est le pointeur sur l'élément suivant dans la liste chaînée.

Modifications des Algorithmes Classiques pour les AVL de YooGoo

L'insertion d'un élément

L'insertion d'un élément dans un AVL classique fonctionne principalement en insertion en feuille (noeud terminaison d'un arbre). Dans nos AVL, il est possible d'insérer un élément dans un noeud interne.
Principe :
Soit " A " l'arbre où l'on désire insérer l'élément " X " de type pointeur non typé. Précisons que l'algorithme est récursif est que lorsque l'on parle d'insertion, on relance l'algorithme sur les paramètres indiqués.
Si A est non NUL Alors :
    Si l'élément X est strictement inférieur    à l'élément de A,
        alors on insère X dans le sous fils gauche de A.
    Sinon
        Si l'élément X est strictement supérieur à l'élément de A alors :
            On insère X dans le sous fils droit de A.
        Sinon
            On est dans le cas où l'élément de A a la même valeur que X.
            On insère X dans la liste chaînée  du fils courant.
            La modification n'aboutit pas à un déséquilibre de l'arbre.
Sinon
    On crée un noeud et on insère X dans sa liste chaînée.
    Le nouvel élément se trouve donc en feuille.
    La modification aboutit à une modification de l'équilibre dans l'arbre.
    On fait ce qu'il faut pour rééquilibrer.
Voici l'algorithme dans le langage " Algo " enseigné à Epita :
NB : les flèches d'affectation sont remplacées par des " = " pour simplifier la lecture et les déférences de pointeurs " " sont remplacées par des " -> ".
Les " = " inclus dans les comparaisons des " Si - Alors " sont des fonctions de comparaison et non d'affectation.
Les fonctions externes utilisées sont les fonctions classiques des types abstraits sur les listes (surtout " Insérer(Liste l, élément X, Entier place) ") et la fonction passe-partout " compare " qui retourne -1, 0 ou 1 si respectivement l'élément est inférieur, égal ou supérieur à celui comparé (elle est composée d'un grand " Selon les valeurs de () " qui fonctionne selon la variable " type " mise en paramètre). Rappel : Le rééquilibrage des AVL se fait selon les valeurs des " déseq " de chaque noeud et des rotations qui s'ensuivent.
Algorithme fonction at_avl_insert : Entier
    Paramètres globaux
        AVL A
    Paramètres locaux
        Elément X
        Entier type
    Variables
        Entier tmp_int
        AVL B
Début Si (A <> NUL) Alors
        Si (compare(X, ième(A->data, 0), type) = -1) Alors
            tmp_int = at_avl_insert(A->fg, X, type)
            A.déseq = A.déseq + tmp_int
            Si (tmp_int = 0) Alors
                Retourne 0
            Sinon
                Selon les valeurs de (A.déseq)
                    Cas 0 : Retourne 0
                    Cas 1 : Retourne 1
                    Cas 2 :
                        Si (A->fg.déseq <> -1) Alors
                            Rotation_GD(A)
                        Sinon
                            A.balance = 0
                            A->fg.balance = 0
                            Rotation_D(A)
                        Fin Si
                        Retourne 0
                Fin Selon
            Fin Si
        Sinon
            Si (compare(X, ième(A->data, 0), type) = 1) Alors
            tmp_int = at_avl_insert(A->fd, X, type)
                A.déseq = A.déseq - tmp_int
                Si (tmp_int = 0) Alors
                    Retourne 0
            Sinon
                    Selon les valeurs de (A.déseq)
                        Cas 0 : Retourne 0
                        Cas 1 : Retourne 1
                        Cas 2 :
                            Si (A->fd.déseq <> -1) Alors
                            Rotation_DG(A)
                            Sinon
                                A.balance = 0
                                A->fd.balance = 0
                            Rotation_G(A)
                            Fin Si
                            Retourne 0
                Fin Selon
            Fin Si
            Sinon
                Si (compare(X, ième(A->data, 0), type) = 0) Alors
                    Insérer(A->data, X, 0)
                    Retourne 0
                Fin Si
            Fin Si
        Fin Si
Sinon
        Allouer (B)
    B->déseq = 0
    B->fg = NUL
    B->fd = NUL
    B->data = NUL
Insérer(A->data, X, 0) Retourne 1 Fin Si Fin Algorithme fonction
at_avl_insert
Voici les algorithmes des procédures de rotation :
Algorithme Procédure Rotation_D
    Paramètres globaux
        AVL A
    Variables
        AVL B
Début
    Si ((A <> NUL) et (A->fg <> NUL)) Alors
        B = A->fg
        A->fg = A->fg->fd
        B->fd = A
        Si (feuille(A)) Alors
            A.déseq = 0
        Fin Si
        A = B
    Fin Si
Fin Algorithme Procédure Rotation_D

Algorithme Procédure Rotation_G
    Paramètres globaux
        AVL A
    Variables
        AVL B
Début
    Si ((A <> NUL) est (A->fd <> NUL)) Alors
        B = A->fd
        A->fd = A->fd->fg
        B->fg = A
        Si (feuille(A) Alors
            A.déseq = 0
        Fin si
        A = B
    Fin Si
Fin Algorithme Procédure Rotation_G

Algorithme procédure Rotation_DG
    Paramètres globaux
        AVL A
Début
    Rotation_D(A->fd)
    Rotation_G(A)
Fin Algorithme Procédure Rotation_DG

Algorithme procédure Rotation_GD
    Paramètres globaux
        AVL A
Début
    Rotation_G(A->fg)
    Rotation_D(A)
Fin Algorithme Procédure Rotation_GD

Suppression d'un élément

C'est certainement la partie qui a demandé le plus de réflexion dans la base de données. En définitive, la suppression marche par adresse. Il faut tout d'abord récupérer l'adresse de l'élément à supprimer pour pouvoir ensuite lancer la suppression sur tous les AVL de la base de données.
Principe :
L'algorithme se divise en fait en deux parties :
  • La suppression de l'élément en lui-même.
  • La suppression des noeuds qui font références à des listes chaînées référencées plus haut dans l'arborescence.
Rappelons que lors de la suppression dans un AVL classique, elle peut avoir lieu en feuille mais également plus haut dans l'arborescence ; et dans ce cas-là, selon le déséquilibre du noeud, on fait pointer son élément sur le minimum du sous fils droit ou le maximum du sous fils gauche ; puis ensuite on relance la suppression sur ce sous-fils pour supprimer le noeud d'où l'on a remonté l'élément. Dans notre cas, cela a lieu lorsque la liste chaînée du noeud où a eu lieu la suppression est vide.
Soit A l'arbre dans lequel on lance la recherche et X soit le pointeur sur élément que l'on veut supprimer soit l'adresse de la liste chaînée du noeud que l'on veut supprimer. (Les deux algorithmes se différencient uniquement par la comparaison : " X " ou " ième(X, 0) ").
Si A est non-nul Alors
    Si le premier élément de la liste de A est strictement supérieur à X
    (ou supérieur au premier élément de la liste X)
    Alors
        Lancer la suppression sur le sous fils gauche de A.
    Sinon
        Si le premier élément de la liste de A est strictement inférieur à X
        (ou inférieur  au premier élément de la liste X)
        Alors
            Lancer la suppression sur le sous fils droit de A.
        Sinon
            Si le premier élément de la liste de A est égal à X
            (ou égal au premier élément de la liste X)
            Alors
                Si A est une feuille Alors
                    Supprimer X dans la liste de A.
                    (Supprimer A et rééquilibrer l'arbre.)
                    Si la liste chaînée de A est vide supprimer A
                    et rééquilibrer l'arbre.
                Sinon
                    Supprimer X dans la liste de A.
                    Si la liste de A est vide Alors
                        Si le déséquilibre de A est -1 Alors
                            La liste chaînée de A devient le min du
                            Sous fils droit de A.
                            On lance la suppression de la liste
                            Chaînée de A dans son sous-fils droit.
                        Sinon
                            La liste chaînée de A devient le Max du
                            Sous fils gauche de A.
                            On lance la suppression de la liste
                            Chaînée de A dans son sous-fils gauche.
Voici les Algorithmes correspondants en Langage " Algo " :
Première fonction : Celle qui supprime l'élément.
Algorithme Fonction at_avl_delete : Entier
    Paramètres globaux
        AVL A
    Paramètres locaux
        Element X
        Entier Type
        Booleen DelData
    Variables
        Entier t
Debut
    Si (compare(X, ième(A->data, 0)) = -1) Alors
        t = at_avl_delete(A->fg, X, deldata)
        Si (t = 0)
            Retourne 0
        Sinon
            A.balance = A.balance - t
            Retourne at_avl_rebalance(A)
        Fin Si
    Sinon
        Si (compare(X, ième(A->data, 0)) = 1) Alors
            t = at_avl_delete(A->fd, X, deldata)
            Si (t = 0)
                Retourne 0
            Sinon
                A.balance = A.balance + t
                Retourne at_avl_rebalance(A)
            Fin Si
        Sinon
            Si (compare(X, ième(A->data, 0)) = 0) Alors
                Si (feuille(A)) Alors
                    Retourne at_avl_delsel(A, X, DelData)
                Sinon
                    at_avl_delselbis(A, X, Deldata)
                    Si (estvide(A->data)) Alors
                        Si (A.balance = -1) Alors
                            A->data = at_avl_min(A->fd)
                            t = at_avl_delete2(A->fd, A.data, Type,False)
                            Si (t = 0) Alors
                                Retourne 0
                            Sinon
                                A.balance = A.balance + t
                                Retourne at_avl_rebalance(A)
                            Fin si
                        Sinon
                            A->data = at_avl_max(A->fg)
                            t = at_avl_delete2(A->fg, A.data, Type,False)
                            Si (t = 0) Alors
                                Retourne 0
                            Sinon
                                A.balance = A.balance - t
                                Retourne at_avl_rebalance(A)
                            Fin si
                        Fin Si
                    Fin Si
                Fin Si
            Sinon
                Retourne 0
            Fin Si
        Fin Si
    Fin Si
Fin Algorithme Fonction at_avl_delete
Seconde fonction : Celle qui supprime la liste chaînée.
Algorithme Fonction at_avl_delete2 : Entier
    Paramètres globaux
        AVL A
    Paramètres locaux
        Liste X
        Entier Type
        Booleen DelData
    Variables
        Entier t
Debut
    Si (compare(ième(X, 0), ième(A->data, 0)) = -1) Alors
        t = at_avl_delete2(A->fg, X, DelData)
        Si (t = 0)
            Retourne 0
        Sinon
            A.balance = A.balance - t
            Retourne at_avl_rebalance(A)
        Fin Si
    Sinon
        Si (compare(ième(X, 0), ième(A->data, 0)) = 1) Alors
            t = at_avl_delete2(A->fd, X, DelData)
            Si (t = 0)
                Retourne 0
            Sinon
                A.balance = A.balance + t
                Retourne at_avl_rebalance(A)
            Fin Si
        Sinon
            Si (compare(X, ième(A->data, 0)) = 0) Alors
                Si (feuille(A)) Alors
                    Retourne at_avl_delsel(A, X, DelData)
                Sinon
                    at_avl_delselbis(A, X, Deldata)
                    Si (estvide(A->data)) Alors
                        Si (A.balance = -1) Alors
                            A->data = at_avl_min(A->fd)
                            t = at_avl_delete2(A->fd, A.data, Type,False)
                            Si (t = 0) Alors
                                Retourne 0
                            Sinon
                                A.balance = A.balance + t
                                Retourne at_avl_rebalance(A)
                            Fin si
                        Sinon
                            A->data = at_avl_max(A->fg)
                            t = at_avl_delete2(A->fg, A.data, Type,False)
                            Si (t = 0) Alors
                                Retourne 0
                            Sinon
                                A.balance = A.balance - t
                                Retourne at_avl_rebalance(A)
                            Fin si
                        Fin Si
                    Fin Si
                Fin Si
            Sinon
                Retourne 0
            Fin Si
        Fin Si
    Fin Si
Fin Algorithme Fonction at_avl_delete2
Voici l' algorithme de la fonction at_avl_rebalance :
Algorithme Fonction at_avl_rebalace : Entier
    Parametres globaux
        AVL A
Debut
    Selon les valeurs de (A.balance)
        Cas -2 :
            Selon les valeurs de (A->fd.balance)
                Cas -1 :
                    Rotation_G(A)
                    Retourne 1
                Cas 0 :
                    Rotation_G(A)
                    Retourne 0
                Cas 2 :
                    Rotation_DG(A)
                    Retourne 1
            Fin selon
        Cas -1 :
            Retourne 0
        Cas 1 :
            Retourne 0
        Cas 2 :
            Selon les valeurs de (A->fg.balance)
                Cas -1 :
                    Rotation_GD(A)
                    Retourne 1
                Cas 0 :
                    Rotation_D(A)
                    Retourne 0
                Cas 1 :
                    Rotation_D(A)
                    Retourne 0
            Fin selon
    Fin selon
Fin Algorithme Fonction at_avl_rebalance
Voici les algorithmes de Suppression :
Algorithme Fonction at_avl_delsel : Entier
    Paramètre globaux
        AVL A
    Paramètres locaux
        Pointeur data   (* pointeur non typé*)
        Booleen DelData
Debut
    Si (A = NUL) Alors
        Retourne 1
    Sinon
        at_avl_delselbis(A, data, DelData)
        Si (estvide(A->data)) Alors
            Libèrer(A)
            A = NUL
            Retourne 1
        Sinon
            Retourne 0
        Fin Si
    Fin Si
Fin Algorithme Fonction at_avl_delsel

Algorithmes Procédure at_avl_delselbis
    Paramètres globaux
        AVL A
    Paramètres locaux
        Pointeur data
        Entier Deldata
    Variables
        Entier counter
        Liste tmp_head
Debut
    Si (A <> NUL) Alors
        Si (data = A->data) Alors
            A->data = NUL
        Sinon
            tmp_head = A->data
            counter = 0
            Tant que ((non(vide(tmp_head))) et (ième(tmp_head, 0) <> data)) faire
                Tmp_head = tmp_head->nxt
                Counter++
        Fin tant que
        Si ((non((vide(tmp_head))) et (ième(tmp_head, 0) = data)) Alors
            SupprimerIème(A->data, counter, DelData)
        Fin Si
    Fin Si
Fin Algorithme Procédure at_avl_delselbis

Recherche dans un AVL de YooGoo

Le plutôt simple et répétitif. Cela correspond à une recherche classique dans un arbre binaire de recherche.
Principe :
En général :
Si le premier élément de la liste de A est supérieur à l'élément X
alors
    On relance la recherche sur le sous fils gauche de A.
Sinon
    Si le premier élément de la liste de A est inférieur à X alors
        On relance la recherche sur le sous fils droit de A.
    Sinon
        On ajoute à la liste chaînée des résultats, les éléments de la liste
        chaînée de A.
Si on recherche un Utilisateur ou un Canal bien précis, par exemple, on retourne directement le pointeur sur le premier élément de la liste chaînée de A (vu que chaque Canal et Utilisateur ont chacun un nom unique, A ne peut qu'avoir un élément par liste chaînée) : ce principe sera utilisé lorsque l'on recherchera à supprimer un élément ; On récupère le pointeur que l'on met en paramètre de la fonction que l'on a rencontrée précédemment.
Par contre, ci on recherche l'ensemble des Utilisateurs ou Canaux qui ont leurs noms qui commencent comme l'élément X, on utilisera plutôt ce principe d'algorithme ci-dessous : (on utilisera une fonction de comparaison et d'inclusion de liste chaînée)
Si le premier élément de la liste de A est supérieur à l'élément X
alors
    On relance la recherche sur le sous fils gauche de A
Sinon
    Si le premier élément de la liste de A est supérieur
    à l'élément X alors
        On relance la recherche sur le sous fils droit de A
    Sinon
        Si le premier élément de la liste de A commence par X
        (X inclus dans A) alors
            Ajouter les éléments de la liste chaînée de A dans la
            liste chaînée des résultats.
            Si le sous fils gauche de A est non nul et si le premier
            élément de sa liste commence par X alors
                Poursuivre la recherche sur le sous fils gauche de A.
            Si le sous fils droit de A est non nul et si le premier
            élément de sa liste commence par X alors
                Poursuivre la recherche sur le sous fils droit de A.

Fonction avancées de la base de données

Dans cette partie, nous allons décrire les différentes fonctions élaborées selon les besoins du server de YooGoo.
int add_canal(canal *new_canal)
:
Cette fonction sert à ajouter un objet " canal "à la base de donnée.
Elle retourne 0 si l'opération s'est correctement effectuée, sinon elle retourne 1 s'il existe déjà, elle retourne 2 si le pointeur sur objet " canal " est à NUL et elle retourne 3 si la base de données est déjà en activité critique (Ajout ou Suppression). Dans les cas où elle retourne une autre valeur que 0, elle se charge d'appeler le destructeur de l'objet.
int del_canal(canal *canal)
:
Cette fonction sert à détruire un objet " canal " dans la base de données. Elle retourne 0 en cas de succès, 2 si le pointeur sur objet " canal " est à NUL et 3 si la base de donnée est en activité critique.
int canal_exists(char *canal_name)
:
Cette fonction sert à tester si un canal du nom désigné existe déjà (en C, l' équivalent du vrai booléen est un entier non nul et nul pour l'équivalent de faut).
t_elt *search_canal(canal mycanal)
:
Cette fonction retourne une liste chaînée dont les éléments sont des objets canaux dont les noms commencent par le nom de celui mis en paramètres.
canal *search_canal_by_name(char *canal_name)
:
Cette fonction retourne un pointeur sur l'objet canal s'il est trouvé dans la base de données, sinon elle retourne NUL (sert principalement à trouver un pointeur pour ensuite relancer la suppression ou bien encore tester si l'objet de ce nom existe déjà).
t_elt *search_canal_by_profil(char *canal_name)
:
Cette fonction retourne une liste chaînée des objets " canal " dont les noms commencent comme le nom mis en paramètres (Cf principe d'algo n°2 de recherche). Elle fait appel à la fonction t_elt *search_canal(canal mycanal).
int add_user(user *new_user)
:
Cette fonction retourne 0 si l' insertion dans la base de données s' est bien passée. Elle retourne 1 si l' objet existe déjà, 2 si le pointeur sur objet " user " mis en paramètre est NUL et elle retourne 3 si la base de données est en activité critique.
int del_user(user *user_todel)
:
Cette fonction supprime l'objet mis en paramètre dans la base de donnée.
int user_exists(char *user_name)
:
Cette fonction retourne 1 si un objet " user " a une propriété Nick égale à la chaîne de caractères mise en paramètres.
t_elt *search_user(user myuser)
:
Cette fonction recherche un utilisateur selon le remplissage des critères de celui mis en paramètre. Si la propriété " Nick " est remplie, alors le moteur de recherche effectue sa tâche sur l'arbre de classement " sort_by_nick ", sinon si la propriété " City " est remplie alors il effectue sa recherche dans " sort_by_city ", sinon si la propriété " Birth " est remplie alors il effectue sa recherche dans " sort_by_birth " et dans l' arbre " sort_by_gender " en dernier ressort (il n'y a que 3 genres définis (0 : homme, 1 : femme, 2 : non défini) donc nous avons affaire à un arbre de 3 noeuds à grandes listes chaînées et la recherche se ferait en partie linéairement et donc plus longuement qu' avec un arbre binaire de recherche). Pour la recherche par " Nick " ou encore par " city ", la comparaison des noms se fait selon si les chaînes de caractères commencent de la même manière. Pour les critères nous utilisons des macros :
  • DB_USER_NICK_EMPTY pour une propriété " Nick " vide.
  • DB_USER_GENDER_EMPTY pour une propriété " Gender " vide.
  • DB_USER_BIRTH_EMPTY pour une propriété " Birth " vide.
  • DB_USER_CITY_EMPTY pour une propriété " City " vide.
int add_user_online(user *new_user)
:
Cette fonction retourne 0 si l' insertion de l'objet " user " mis en paramètre a été correctement effectuée dans l' arbre " sort_by_status " sinon elle retourne 1 si un objet avec la même propriété " Name " existe déjà, retourne 2 si le pointeur de l'objet " user " mis en paramètre est NUL et retourne 3 si la base de donné est en activité critique. Dans le case de retour de valeurs autres que 0, elle se charge d' appeler le destructeur de l'objet mis en paramètre.
int del_user_online(user *user_todel)
:
Cette fonction permet de supprimer un objet " user " de l'arbre " sort_by_status ".
Elle retourne 0 en cas de réussite, 2 si le pointeur mis en paramètre est NUL et 3 si la base de données est en activité critique.
int user_online_exists(char *user_name)
:
Cette fonction test si un objet " user " dont la propriété " name " est celle mise en paramètre existe déjà dans l'arbre " sort_by_status".
t_elt *users_connected_list()
:
Cette fonction retourne une liste chaînée des objets " user " recherchés dans l' arbre " sort_by_nick " et qui ont leur propriétés " Status" différentes de " Offline ".
t_elt *users_list()
:
Cette fonction retourne une liste chaînée de l' ensemble des utilisateurs de la base de données. Elle sert surtout à la sauvegarde de la base de donnée en mémoire morte.
int initialize()
:
Cette fonction fait appel à une fonction de chargement de fichier qui permet de récupérer les différents profils d' utilisateurs sauvegardés en mémoire morte.
i_node *get_node(int i)
:
Cette fonction permet de récupérer les AVL selon un index mis en paramètre. Pour ce dernier, nous utilisons les macros ainsi définies :
  • DB_NAVL_SORT_USER_BY_NICK : permet de récupérer "sort_by_nick"
  • DB_NAVL_SORT_USER_BY_GENDER : permet de récupérer "sort_by_gender"
  • DB_NAVL_SORT_USER_BY_BIRTH : permet de récupérer "sort_by_birth"
  • DB_NAVL_SORT_USER_BY_CITY : permet de récupérer "sort_by_city"
  • DB_NAVL_SORT_USER_CONNECTED : permet de récupérer "sort_by_status"
  • DB_NAVL_SORT_CANAL_BY_NAME : permet de récupérer "channels"

Organisation de la base de données

Voici comment s'organise la base de données dans ses différents fichiers sources :
t_srtings.cpp, t_strings.h
:
Ces fichiers décrivent toutes les fonctions qui on rapport avec les traitement et autres comparaisons de chaîne de caractères.
at_list.cpp, at_list.h
:
Fixent les définitions du type " t_elt " et tout ce qui a rapport à ce type de liste chaînée.
at_avl_comp.cpp, at_avl_comp.h
:
Ces fichiers définissent essentiellement la fonction " compare " utilisée dans les AVL et aussi dans la base de données elle-même.
at_avl.cpp at_avl.h
:
Décrivent le type abstrait AVL utilisé dans la base de données de YooGoo et définit toutes le fonctions de traitement qui y sont relatives.
database.cpp database.h
:
Décrivent la classe d'objet " database " et les différentes fonctions avancées de la base de données de YooGoo.

Réalisation du protocole

Le parseur

Un parseur est une fonction qui permet de divisé une ligne de commande en plusieurs arguments. Généralement le premier argument est le nom de la fonction, tandis que les autres sont ses paramètres. On parle alors de parser un message, afin d'en obtenir les différents arguments qui vont devoir être exploités. Cette définition était nécessaire car le terme parser est employé plusieurs fois au long du rapport.
Lorsque le client ou le serveur reçoit un message, celui ci est une chaîne ascii. Il faut alors pouvoir interpréter ce message, pour cela on va traité ces arguments un à un après l'avoir parsé. Le parseur prend donc en argument une chaîne ascii, et renvoie un tableau d'arguments. Le nombre d'argument de ce tableau est limité à 100 et chaque argument ne peut faire plus de 255 caractères. Ces nombres ont un peu exagéré vu qu'on ne peu recevoir de message de plus de 512 octets. On renvoie aussi le nombre d'arguments dans le tableau.
La fonction parseur se limite à recopier dans le tableau d'arguments la chaîne donnée en argument jusqu'à un espace ou alors s'il y a eu précédemment des guillemets ouvrants, jusqu'à des guillemets fermant. En effet, les seuls délimiteurs possibles sont ' ' et ' "'. Ensuite elle incrémente le nombre d'arguments et continue le même traitement pour l'argument suivant, jusqu'à la fin de la chaîne.

Le protocole

Une fois que l'on a parsé le message, le tableau d'argument obtenu est envoyé à la fonction protocole. Celle-ci effectue une suite de comparaisons de chaînes sur l'argument n°1 pour savoir à quelle commande il correspond. Ensuite, en fonction de la commande, il vérifie le nombre d'arguments. Puis, il vérifie si les users et les canaux mis en arguments existent et récupère leurs pointeurs en faisant appel à la base de données. Puis, pour les fonctions d'administration des canaux, il vérifie les droits de l'utilisateur. Enfin, il effectue divers petits tests en fonction des commandes appelées.
L'implémentation du protocole a été largement simplifié par la conception préliminaire de proto.h. En regardant les valeurs retournés par la fonction, il est facile de voir les tests qui doivent être fait. Le protocole renvoie ensuite un numéro d'erreur au client. 000 indique que tout s'est bien passe. Les numéros inférieurs à 100 indiquent que la commande a été effectuée mais qu'un fait notable s'est produit. Cela arrive si vous voulez par exemple joindre un canal sur lequel vous etes déjà connecté. Les valeurs supérieures à 400 indiquent une erreur. Lorsque le client envoie une commande au serveur, celui-ci lui envoie forcement un unique numéro d'erreur. Le client reçoit aussi les évènements du serveur. Par exemple, lorsqu'un utilisateur se joint au canal, les autres connectés reçoivent le message : join <channel> <nick> pour les informer de l'évènement.
Là aussi, les messages serveur -> client ont été définis au préalable dans proto.h. Néanmoins, un message tel que celui là est assez peu parlant pour un utilisateur. Il faut que son client le parse et lui envoie plutôt un message genre : <Nick> s'est join au canal <channel>. Ou bien, nous pourrions envoyer directement le message sous sa forme "human-readable". Ceci entraînerait des problèmes pour parser le message et des problèmes pour gérer les différentes langues. Nous gardons donc pour le moment la première solution.
Le protocole recevant des arguments de l'extérieur, il doit aussi être capable de gérer les erreurs de l'utilisateur. Nous avons donc fait beaucoup de debugging. Nous testons surtout la taille des buffers et des chaînes.
A ce jour, toutes fonctions 'simples' ont été implémentées. Cela comprend la gestion du profil et la gestion des canaux. Les fonctions évoluées qui demandent l'autorisation d'autres utilisateurs ou bien qui renvoient des informations dont la taille ne permet pas de les envoyer en une seule fois ne sont pas encore gérées.

La classe de log

Dans le monde Unix il est commun d'avoir une trace de ce qui se passe sur un serveur. C'est pourquoi nous avons intégré à YooGoo une classe de log qui permet de logger dans un fichier diverses informations sur l'activité du serveur.
Après l'installation du serveur YooGoo, les logs se trouveront dans le répertoire " ./logs ". Il y a 3 fichiers de log différents. Un pour le log des connexions et des déconnections, un pour l'activité de la base de données, et un pour le debug. Dans les fichiers on à une information par ligne. Pour exploiter ces fichiers sous Windows il faudra des outils spécialisés dans le travail sur les fichiers, par contre sous Unix on peut exploiter ces fichiers facilement grâce à des outils tel que " grep, tail, cut, wc ...". Il y a un fichier par jour. Ainsi si le serveur YooGoo tourne plus de 24h d'affilé, à 0h00 sont créé des nouveaux fichiers de logs. Ceci facilite les statistiques par jour et permet de ne pas avoir des fichiers trop volumineux.

Les différents niveaux

Il y a différent niveau de log choisit par l'utilisateur. C'est une combinaison de flags. Les différents flags sont :
#define CONNECT 0x0002
:
Active le log des connections et déconnections. A chaque connexion on note l'IP, et le nom de la personne qui s'est connecté.
#define DB 0x000C
:
Active le log de l'activité de la base de données. A chaque fois qu'on enregistre, ajoute ou supprime un utilisateur, on écrit une entrée dans le fichier db.log.
#define DB_REQUEST 0x0020
:
Active le log de toutes les recherches effectuées par la base de donnée, avec le résultat obtenu.
#define ERROR 0x00C0
:
Active le log de toutes les erreurs ou les warnings dû à un mauvais fonctionnement d'une fonction. Les erreurs importantes sont aussi affichées à l'écran.
#define FILE_USER 0x0200
:
Active le log de la file de messages des utilisateurs. Tous les messages reçus et émis aux utilisateurs sont loggés.
#define FILE_CANAL 0x0C00
:
Active le log de la file de messages des canaux. Tous les messages des canaux sont loggés.
#define COMMENT 0x2000
:
Active le log des commentaires. Affiche des commentaires sur le déroulement du serveur.
#define DEBUG 0xC000
:
Active le mode debug. Utilisé que pour le debuggage.
Pour sélectionner le droit voulu, il suffit de faire une combinaison de ces modes, séparés par un OU (" || ") .

Les différentes fonctions

Pour écrire une ligne dans un fichier de log il suffit d'utiliser les fonctions :
        // pour tout ce qui concerne les utilisateurs et les canaux
        int Clog::Add_Log(int lvl, const char * msg);

        //pour tout ce qui concerne la base de données
        int Clog ::Add_Db(int lvl, const char * msg);


        // pour le debug et les commentaires
        int Clog::Add_Msg(int lvl, const char * msg);
avec lvl une combinaison des différents modes. Selon la fonction choisit on écrira dans un fichier ou un autre. De plus certains fichiers ne sont pas crées si le niveau de log ne le nécessite pas.
Deux méthodes différentes étaient possible quand à la saisie des messages à écrire dans les fichiers de logs. Soit on faisait beaucoup de fonctions écrivant des messages prédéterminés, ceci permettait de pouvoir gérer dans la classe de log même les différentes langues, et avoir une certaine harmonie entre les différentes entrées. Soit celui qui appelle la fonctions de log est libre de saisir ce qu'il veux. C'est la deuxième solution qui a été retenue. La seule chose qui est commune à toutes les lignes, est la date et l'heure au debut de la ligne, sous forme " [AAAAMMJJ HH:MM:SS] " . La gestions de plusieurs langues est donc remise à plus tard.

Portage sous Unix?

Le portage du serveur sous Unix n'est pas prévu pour cette soutenance, mais il a déjà été pensé. Les seules divergences qui peuvent apparaître entre les deux systèmes d'exploitation viennent de la gestion des Sockets et de la gestion des threads.
En ce qui concerne la gestion des threads nous utilisons une classe qui permet la compatibilité entre les 2 OS. Cette différence intervient surtout dans l'utilisation des mutex car la gestion des threads est presque semblable. La classe utilise des sémaphores assimilables à des Mutex pour la compatibilité.
Pour ce qui est des Sockets, nous nous sommes déjà renseigné et les Sockets de BSD ne diffèrent pas tant que ça avec WinSock étant donné que Microsoft s'est inspiré (une fois de plus) des sockets de BSD.

En attendant la suite

Le serveur presque près a permis à certains membres de s'occuper de tout ce qui devrait faire la réussite de YooGoo : le site web et le client. Ces deux éléments ne sont pas à négliger car ce sera ce que les utilisateurs verront le plus. Pour préparer le terrain, les outils de création d'interface ont été testés afin de choisir celui qui nous sera le plus profitable. Ainsi un gros travail de préparation sur le futur client a été fait. En effet, le client risque de nous prendre plus de temps que prévu donc nous essayons de prendre de l'avance. De plus un client basique pour pouvoir utiliser les nombreuses fonctionnalités du serveur a été fait. En ce qui concerne le site web, il semblerait que déjà pas mal de monde soit attiré par YooGoo, et que notre projet est convoitisé par d'autres car nous avons été victime d'un hack. De plus nous avons voulu bien préparé la soutenance et avons donc pensé à une présentation graphique et structurée avec PowerPoint.

Travail sur le client

MFC (Visual Studio)

Avantages

Afin de développer un premier client graphique, nous nous sommes penché dans un premier temps sur le logiciel qui nous a permis de créer notre serveur, à savoir Visual Studio 6. Le logiciel de Microsoft utilise 2 technologies pour l'interface graphique de ses projets. La première est sans doute la moins connue, c'est la technique ATL (Active Template Library) dont on ne parlera pas. La deuxième la plus connue est la technologie MFC (Microsoft Foundary Library). Dans un premier temps les MFC semblent être très pratiques et très puissantes. On dispose d'une large palette de contrôles à disposés dans la fenêtre de dialogue. De plus nous avons la possibilité d'intégrer des ActiveX disponibles un peu partout. La création graphique des pages est vraiment intuitive. De plus dans la version supérieure de Visual Studio (le .Net) les propriétés des objets sont modifiables beaucoup plus aisément.

Inconvénients

Mais les inconvénients sont importants. Tout d'abord le lien des objets au code du projet est totalement différent de ce que proposait Microsoft avec Visual Basic. Pour pouvoir utilisé un contrôle dans notre code il faut passer obligatoirement par une variable intermédiaire. Celle-ci est obtenue par des appels au ClassWizard, un complément de Visual Studio permettant de gérer les objets (classes) de la fenêtre plus simplement. Seulement l'héritage des classes a été bâclé. Ainsi pour faire des fonctions de base comme griser un bouton à partir du code est un véritable calvaire. Pour faire cela il faut que notre variable m_textbox par exemple lié au contrôle textbox soit utilisée de la manière suivante : m_textbox.SetWindowEnable(False).
On ne voit aucun rapport entre l'appellation Window dans la fonction et notre contrôle textbox. Le vice va encore plus loin dans la gestion des événements. Il a été jusqu'à présent impossible de gérer l'événement OnKeyUp qui réagit à chaque relâchement de touche à l'intérieur d'un textbox. Cet événement n'a marché que pour une fenêtre de dialogue vierge ce qui n'a aucun intérêt.
Nous avons bien essayé de résoudre tous les problèmes rencontrés mais il est impossibles d'obtenir des cours potables sur les MFC sur le net. Les seuls résultats de recherche obtenue étaient pour des cours sur une semaine au prix exorbitant de 2500 frs / jour.
De plus l'intégration de fichier codé purement en C ou C++ tel que le serveur YooGoo n'a pas été une partie de plaisir. En effet lors de la première compilation, le compilateur nous a sorti des erreurs que nous ne supposions pas pouvoir exister. Par exemple une erreur était du genre " fichier trop court ... ". Le problème vient du fait que les projets MFC de Visual C++ utilisent des en-têtes précompilées. C'est-à-dire que le compilateur exécute préalablement les instructions situées dans les en-têtes avant même d'avoir ouvert le corps de notre code en C. Pour pouvoir stopper ce problème il faut modifier des propriétés des units du projet (désactiver l'option en-têtes précompilées dans le menu settings). De plus la compilation avec Visual .Net sort tous les jours de nouvelles erreurs de link qui disparaissent et réapparaissent lorsqu'elles le veulent.
Toutes ces petites choses font que Visual C++ ne sera pas utilisé pour la conception du client et nous utiliserons un logiciel certes moins puissant mais beaucoup moins capricieux comme C++ Builder.

C++ Builder (API Windows)

Avantages

Avec C++ Builder, l'interface graphique est la même que sous Delphi. La prise en main a donc été très rapide. Le principe est le même que pour Visual. On dispose d'un certain nombre de contrôles, qui se placent dans la fenêtre à créer. La différence vient de la gestion de ces objets. Contrairement à Visual, dans la fenêtre des propriétés des objets, il existe un onglet regroupant tous les événements gérés par le contrôle. Et ces événements marchent, de même que pour la modification de propriétés à l'intérieur du code. Ce qui a pris 2 jours avec Visual a mis 15 secondes avec C++ Builder. D'un point de vue global, C++ Builder est assez paramétrable mais moins convivial d'un point de vue de code pur. De plus l'intégration du code de l'ancien client (code C++ pur) n'a posé aucun problème.
Du côté de l'interface du code Borland est assez personnalisable. On peut paramétrer les couleurs des instructions, des strings ... Et pour se rapprocher de Visual il est possible d'utiliser les mêmes raccourcis clavier pour la compilation. Certes l'indentation et la mise en forme sont bien moins performantes que sous Visual mais Borland C++ Builder a l'énorme avantage de ne pas être gourmand en mémoire. Il est alors possible de débuguer le client et le serveur en même temps.

Inconvénients

Tout d'abord, il faut souligner que le compilateur de Borland est moins performant que celui de Visual dans plusieurs domaines. D'une part il est incapable de recompiler un programme en cours de debuging contrairement à son rival. Par ailleurs les fenêtres de debugging sont compliquées à gérer. D'un point de vue visuel C++ Builder n'est pas très beau et on comprend mal pourquoi Borland a décider d'en faire un programme multi-fenêtres ou les autres programmes Windows apparaissent en tâches de fond...
De plus C++ Builder a été fortement déconseillé par Krisboul mais nous n'avons toujours pas vu pourquoi. Aucun problème n'a pour l'instant été constaté.

Client Graphique(Version de base)

Pour cette 2ème soutenance nous voulions présenter un serveur opérationnel pour avoir une bonne avance sur nos prévisions. Et pour tester le serveur nous devions disposer d'un client potable. Pour cela nous avons décidé de créer un premier client graphique avec les fonctionnalités de bases.

Contrôles de la fenêtre

Notre petit client est un programme monofenêtre qui est en fait une boite de dialogue. Celle-ci se décompose en deux parties.
La première est une partie dédiée à la connexion au serveur. Elle est composée de :
*
Un textbox pour entrer le pseudo de l'utilisateur
*
Un textbox pour entrer le mot de passe de l'utilisateur
*
Un bouton de connexion
*
Un bouton de déconnexion
*
Des quelques étiquettes destinées à faciliter la compréhension
La deuxième est dédiée à la communication. Elle est composée ainsi :
*
Un textbox pour envoyer des messages au serveur
*
Un listbox qui contient tous les messages renvoyés par le serveur. C'est en quelque sorte l'écran du client mode texte
*
Un bouton pour envoyer les données au serveur.

Fonctions implémentées

Les fonctions implémentées ne vont pas très loin. Il ne s'agit que des fonctions de base. Elles regroupent les fonctions déjà disponibles dans le client texte remis à jour et quelque nouveautés :
*
Connexion avec Pseudo et Mot de Passe
*
Déconnexion
*
Dialogue à travers le serveur
*
Obtention d'information telle que le profil d'un utilisateur
La liste précédente est non contractuelle, car le rapport a été préparé bien avant la soutenance. Il est donc possible que d'autres fonctions aient été rajoutées par la suite.

Passage en non-bloquant

Pour pouvoir utiliser notre client avec la nouvelle version du serveur, il a fallu faire quelque modification, notamment dans la manière de communiquer. Effectivement, le serveur fonctionne désormais en non-bloquant, il semble donc évident que le client doit être basé sur le même principe. Pour passer le client en non-bloquant, nous avons déjà passé le client texte en non-bloquant mais il était très difficile de le tester ainsi. Le code a donc été implémenté dans le client. Il a alors fallu travailler sur la fonction select de winsock qui surveille l'activité sur les sockets.
La fonction select prend dans son dernier paramètre une valeur qui détermine son mode de fonctionnement. Ce paramètre désigne en quelque sorte le temps d'attente du select avant de continuer l'exécution de la boucle. Une valeur 0 correspond à un mode bloquant, ici le 0 équivaut en fait à une valeur infinie. Dans notre cas, il a fallu recréer un timer du type Winsock. Ce timer est une structure regroupant un champs pour les secondes et un autre pour les millisecondes. Pour le client nous avons choisi une valeur très courte : 1 ms.
Dès lors, le client fonctionnait en mode non-bloquant.

Utilisation de Thread

Le passage en mode non-bloquant a causé une remise en question de la structure de notre premier client graphique, car une fois entrée dans la boucle de communication le reste du programme perdait la main.
Nous avons donc eu recours à l'exécution en parallèle. Nous avons donc créé un thread supplémentaire dédié exclusivement aux communication entrantes et sortantes. Ce thread est créé lors de la connexion au serveur.
Avec ce thread, le reste du programme continue de tourner normalement, et il devient possible de cliquer sur des contrôles de la fenêtre. On comprend alors tout l'intérêt des threads.
Par ailleurs, la déconnexion a également du être repensée. En effet celle-ci est totalement différente que l'on soit en bloquant ou non-bloquant. Pour effectuer une déconnexion proprement il faut s'assurer que les buffers d'émission et de réception soient bien vides. Or avec deux threads il est impossible d'en être sûr à 100%. Nous avons donc créer une variable de déconnexion qui prends l'état 1 lorsqu'on clique sur le bouton déconnexion ou lorsqu'on détruit la fenêtre. Cette variable agit directement sur le thread de communication qu'elle conditionne. Lorsqu'elle est à 1 la boucle de communication vide les buffers s'il y a lieu d'être sinon elle quitte directement la boucle et détruit le thread proprement. Il ne reste alors plus qu'à détruire le socket correspondant et la déconnexion s'est effectuée proprement.

Le site web

Le Site web n'a pas subit de grande refonte. On peut noté un petit changement dans la galerie photos : celle-ci permet de gérer des vidéos maintenant. Comme nous l'avions prévu, l'entretient est assez simple grace aux scripts PHP. Le travail a surtout consisté à entretenir les nouvelles versions en téléchargement, la galerie photo et les news.

Le piratage du site web

Il y a eu un fait notable sur le site web le 24 Janvier à 18:40. Alors que Hargos allait faire l'entretient quotidien du site web, il a vu que la première page avait été complètement modifiée. Il a tout de suite compris ce qui venait de se passer. Un pirate venait de passer par-là. L'interface était complètement modifiée. Elle redirigeait au bout de quelques secondes vers www.disney.com. A la place du logo habituel, il y avait une photo de Lvg (aussi appelé Jésus). Il y avait de grande chance que se soit lui qui ai fait le coup. Il faut prendre des décisions rapidement. Tout d'abord avertir le reste du groupe. Ensuite, on a redirigé www.yooGoo.com vers le serveur de Danao qui avait par chance une version du site à peu près à jour. Nous avons ensuite vérifié que le mot de passe du site n'avait pas été modifié. Par chance non. Nous avons donc regardé s'il y avait eu des modifications dans la base SQL. En cherchant un peu, nous avons trouvé une news un peu spéciale: elle contenait du java script. En effet, sur la page d'index, le script recherche les 10 dernières news dans la base SQL et les affiches. En mettant du Java script dans les news, le pirate à redirigé le site vers www.disney.com. Nous avons ensuite regardé les fichiers du site pour voir si aucun n'avait été modifié. Il y en avait effectivement. Nous avons donc remplacé les fichiers qui avaient été modifiés pendant la journée par ceux que nous avions en sauvegarde sur nos disques. Le site était redevenu normal. Nous avons remis le lien www.YooGoo.com vers yoogoo.multimania.com. Reste à savoir qui? quand? comment? Jésus a revendiqué le hack de notre site dès le lendemain (aucune surprise). Il nous dit avoir mis en place le hack du site vers 18:30 ce qui concorde avec la date de modifications des fichiers. Il a utilisé une faille de download.php lui permettant de télécharger n'importe quels fichiers du site. Il a ainsi pu étudier le source PHP du site en détail. Il a ensuite découvert une faille dans counter.php (un script permettant de savoir la fréquentation du site). Celle-ci lui permettait d'envoyer une n'importe quelle requête SQL. Grâce à ça, il a pu modifier la base SQL et ajouter la news qui contenait du Java script. A partir de là, il a uploadé des fichiers sur le serveur web. Il a donc changé notre logo et l'interface du site. Nous avons réparé les failles du site. Le script counter.php ne nous servait plus depuis quelques temps déjà puisque nous utilisons Xiti. Le script counter.php a donc été supprimé.

Impacts

Xiti nous a permis de voir l'impact du piratage du site. A 18:50 le site a été redirigé et il paraissait saint à toutes les personnes qui y allaient. Le site est donc resté seulement 20min dans sa version piratée. Xiti nous informe qu'il y a eu seulement 5 visites entre 18h et 19h. Toujours d'après Xiti, la redirection chez Danao a permit à 15 visiteurs de ne pas se retrouver sur la page hackée. Mais surtout, on remarque 1349 pages vues 102 visites et 72 visiteurs. C'est le tiers de la fréquentation mensuelle en une journée. Effectivement, quand Jésus a piraté le site, il a averti beaucoup de monde. Le bouche à oreille aidant, il a fait de la pub qui se ressent encore une semaine plus tard (voir évolution du mois de janvier). Bilan du piratage du site : quasiment personne n'a vu le site piraté et on enregistre une augmentation de 400% du nombre de visites par rapport au mois de décembre. L'impact de ce hack est donc très positif.

Présentation de la soutenance (PowerPoint)

Pour les soutenances à venir nous avons décidé de présenter notre projet de manière plus professionnel et plus attrayante. Pour cela nous avons donc utiliser un programme destiné principalement aux professionnels dans le but de créer des présentations graphiques. Il s'agit du logiciel PowerPoint de Microsoft.

Description

Ce logiciel permet de créer assez rapidement des présentations graphiques avec possibilité d'animation. Il est possible de créer plusieurs types d'organigrammes, du standard aux plus sophistiqués. Ce logiciel permet en plus d'ajouter quelques séquences sonores qui sont liées à des événements.
Tous les programmes que nous voulons montrer sur le PC principal seront exécutés à partir de PowerPoint. Il en est de même pour le site. PowerPoint possède en plus quelques fonctions pense-bête dans le but d'éviter les trous de mémoire. Il est d'ailleurs possible d'ajouter une voix narrateur pour commenter toute la soutenance mais cela n'a pas beaucoup d'intérêt dans notre cas. Cependant ceci pourra être utile pour expliquer le fonctionnement du logiciel Client YooGoo dans l'aide qui sera fournie avec le CD d'installation.

Explications

Le contenu des pages qui seront présentées sous PowerPoint regroupera toutes les idées de notre plan de soutenance. Le contenu est pour l'instant assez flou car le plan n'a pas encore été décidé.
PowerPoint pourra nous aider à améliorer la clarté de notre soutenance, d'appuyer nos explications de planifier, à temporiser notre soutenance (réglage de l'intervalle de temps entre deux pages) et à l'accélérer car lorsqu'on passe par du visuel on gagne toujours du temps (ex : regarder un match à la télé et l'écouter à la radio !!!)

Conclusion

L'opinion de chacun

Danao

Tout d'abord j'ai appris différentes choses totalement nouvelles dans plusieurs domaines. Je me suis familiarisé avec les threads et comprends presque totalement leur fonctionnement (j'ai lu de grandes parties de différents livres sur ce sujet, plus des tutoriaux et des cours sur le net). J'ai également appris pourquoi les MFC découragent une grande partie de leurs utilisateurs et comment faire sans elles. Par conséquent je connais un autre logiciel de programmation, C++ Builder.
Je me suis également plongé dans la programmation événementielle que ce soit en mode graphique ou en mode texte (en mode console DOS). De plus j'ai fait quelque chose qui me tenait à coeur, redonner un peu de vie à la console DOS en y mettant des couleurs et en recodant une fonction gotoxy.
En travaillant avec Marco, j'ai amélioré ma compréhension de Winsock et compris tout le code qui se trouve dans la partie client.
Enfin c'est la première fois que j'attache autant d'importance à la préparation d'une soutenance et à mener à bien le projet jusqu'à son terme.

Folken

La base de données de YooGoo est terminée pour la seconde soutenance. A l'origine, elle devait l'être pour la troisième soutenance. En bref, cela est dû aux commandes des autres membres du projet qui en avaient besoins dans leurs parties. Au final, j'ai conservé mon avance depuis la dernière soutenance.
Le développement de cette base de données m'a permis d'apprendre à faire évoluer un algorithme en fonction de certains besoins. Cela m'a permis de rendre mon code plus évolutif et d'être efficace pour répondre plus rapidement aux besoins des autres membres du groupe de projet. J'ai également appris à être plus rigoureux dans mon code en m'imposant une grande partie de la norme EPITA (déclarations et syntaxes) et en compilant ma partie avec différents environnements (Visual Studio et GCC). Cela abouti à une grande portabilité du code de la base de données.

Hargos

Pour cette soutenance, pas de mauvaises surprises. Nous avions fait assez attention à ce que les codes des différentes parties soient compatibles. Contrairement à ce que j'aurais cru, la fusion des parties s'est bien déroulée. A part quelques petites fonctions qui n'avaient pas été codées comme tout le monde le souhaitait, dans l'ensemble, nos codes étaient compatibles.
La conception du protocole pour la première soutenance nous a beaucoup aidé. L'implémentation du protocole a été relativement simple. En regardant les valeurs retournées, il est facile de voir l'algorithme à utiliser. Avec le protocole sous la main, j'ai codé sans avoir besoin de beaucoup réfléchir (ça va vite et ça fatigue pas trop).
De même, l'entretien du site web est devenu très automatisé grâce aux scripts PHP. Nous remarquons maintenant que le temps que nous avions pris pour préparer l'architecture de YooGoo porte ses fruits.

Zapata

Cette soutenance, contrairement à la dernière, a été pour moi moins riche en nouveautés. Le C/C++ devient mon langage naturel, les objets deviennent des outils familiers. D'un point de vue algorithmique, cette soutenance ne m'a pas apportée grand chose. De plus mon travail a été très ciblé : il s'agissait de finir le serveur pour cette soutenance. Mission accomplie!
Mais tout n'est pas si simple. J'ai surtout appris l'importance de la phase de conception d'un projet. En effet notre architecture du serveur a dû être modifiée à cause d'une conception trop rapide, et surtout d'une mauvaise connaissance des outils à utiliser lors de la phase de conception. Ainsi j'ai dû me replongé dans Winsock, et dans des fonctions plus avancées que celles étudiées précédemment. Afin de compléter mes connaissances réseaux (et Unix), un portage sous Unix serait intéressant. Un autre point à ne pas négliger est la répartition des tâches, car si celle ci est mal faite, certain membre du groupe risque d'être retardé par les autres.
Un autre point important de cette soutenance était le travail d'équipe. Il a fallu regrouper le code de chacun. On constate alors une grande différence de style de programmation, mais au final si la conception est bien faite, cette différence n'est pas importante car tout s'emboîte à merveille.
Pour finir, je dois dire que cette expérience annuelle de projet libre est pour moi capitale dans mes études car nous acquerons une grande expérience de conduite de projet qui pour moi se révèle capitale pour notre future vie professionnelle.

Le mot de la fin

D'après le planning que nous nous étions fixé, le serveur est tout à fait dans les temps, voire même en avance. Il ne reste plus qu'à l'améliorer pour la prochaine soutenance. Une ou deux personnes seulement vont continuer à travailler dessus. Là où nous avons pris une grande avance c'est sur le client. Grande avance sur le planning peut être, mais il reste encore énormément de choses à faire et c'est toujours le temps qui manque. Le reste du groupe va donc travailler sur le client afin de pouvoir exploiter toutes les fonctionnalités du serveur et peut être pouvoir commencé à le distribuer sur Internet afin de tester YooGoo en grandeur nature.
 
 

 
 
Documentation

FAQ
Conception du Protocole
SDK en Ligne
Télécharger SDK
Cahiers des Charges
Soutenance 1
Soutenance 2
Soutenance 3
Soutenance Finale

Comment marche...
Les Canaux
Le Profile
L'administration
La Base de Données
La Cryptographie
Winsock
Le Multithreading
 
 

 
 
Liens

Epita
EpiTarget
Hallucinetik - Zone 42
poupouill.fr.st Minosis (Spé C2)
La Spé C1
La Sup C1
 
 


 
 
made in Epita Powered by ApachePHP Scripting Language

All logos and trademarks in this site are property of their respective owner. The comments are property of their posters, all the rest © 2000 by YooGoo Team
Des remarques, des question, des choses pas claires? Hargos@ifrance.com