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 Soutenance Finale

BOUHELIER Stéphane
POUILLER Jérôme
TESSARI Marco
TETZLAFF Franck

EPITA - Juin 2002


Table des matières

Introduction

Cadre du projet

Lors de notre cursus de deuxième année préparatoire d'EPITA (Ecole Pour l' Informatique et les Techniques Avancées), il nous est tenu de réaliser un projet informatique. Il a pour but de mettre en oeuvre les connaissances acquises en cours, TD et TP, ajoutés aux acquis personnels nécessaires pour le projet choisi et qui ne peuvent être délivrées lors des cours.

Le projet est réalisé en groupe de quatre personnes. Il est d'une durée d'environ cinq mois (de janvier à juin). Le sujet ainsi que la plate forme de développement sont libres, mais le projet est obligatoirement développé en C ou C++.

Le projet doit être plus que l'application de nos compétences techniques, une expérience de travail en groupe similaire à ce que nous pourrions rencontrer dans la vie professionnelle. Pour cela il nous est demandé un cahier des charges décrivant les différents aspects du projet (description, fonctionnalités, planning...). Durant le développement du projet nous sommes suivis par un jury, lors de soutenances, afin d'évaluer notre progression par rapport au cahier des charges. Chaque soutenance est accompagnée d'un rapport. A la fin des cinq mois, nous devons présenter le résultat de notre travail, et nous sommes alors évalués sur la qualité du travail. Cette soutenance finale accompagnée d'un rapport final fait état de bilan sur le développement du projet. Afin de respecter jusqu'au bout cette expérience, nous devons fournir un dossier d'exploitation (manuel d'utilisation) et un site web à propos de nos réalisations.

Présentation de la "YooGoo Team"

Les membres

Nous sommes tous les quatre en deuxième année du cycle préparatoire de l'Epita. Franck et Jérôme avaient déjà travaillé ensemble. Marco les connaissait et avait envie de travailler avec eux. Stéphane les a rejoint par la suite, une fois le sujet choisit.

Au départ, nous avions tous une expérience de programmation en Pascal acquise lors de notre projet de première année. Nous avions tous utilisé DirectX et l'API Win32 lors de cette première expérience. Seul Stéphane avait déjà fait un projet en C/C++.

Les motivations

Le groupe s'est d'abord formé autour d'un but commun : nous ne voulions pas réaliser uniquement une application, mais avoir une véritable logique d'entreprise dans la conduite de notre projet. Nous voulions donner autant d'importance à tout ce qui entoure l'application elle-même. C'est un aspect qui est souvent négligé par les autres étudiants qui se concentrent sur l'aspect technique sans prendre en compte tout ce qui entoure l'application en elle-même. C'est d'ailleurs souvent la qualité de présentation et de documentation d'un projet qui fait la différence. Nous avions pris conscience de cela et nous nous sommes efforcés de travailler dans ce sens.

De plus nous voulions faire un effort sur la phase de conception, afin que l' implémentation ne soit plus qu'une façon d'expliquer à la machine ses idées. Pour atteindre notre but, il nous fallait trouver quelque chose d'assez complexe pour développer nos compétences en conception et en organisation, mais pas trop spécialisée pour éviter de n'avoir à résoudre uniquement des problèmes techniques. Le but était de réaliser un projet complet et professionnel dans sa présentation.

Le choix du projet

Une fois la direction du projet établie, il nous fallait trouver le sujet. Pour le choix c'est plutôt les domaines techniques que nous voulions aborder qui ont étés déterminants. Tout d'abord, suite aux maladresses que nous avions commises lors de nos précédents projets (trop de graphisme) il nous tenait à coeur de faire un programme axé sur le réseau. Ensuite, le but du projet d'un point de vue pédagogique, a été d'appliquer ce que nous avons appris en cours. Les cours d'algorithmique nous ont présenté les types de données abstraits les plus importants, il nous paraissait intéressant de les mettre en oeuvre dans notre projet.

Après longue réflexion, et de nombreuses idées, nous avons choisit de faire un logiciel de communication. Les caractéristiques n'étaient pas encore bien déterminées, mais, comme nous le verrons plus loin, il nous permettait de répondre à toutes nos attentes. Pour commencer, il nous manquait juste le nom. Après s'être appelé EpIRC, Felix le Chat, c'est YooGoo qui sera retenu pour sa simplicité et la facilitée avec laquelle on retient le nom. Le projet pouvait démarrer.

Do You YooGoo ?

Description

YooGoo a pour but de créer un outil de communication sur internet. Le projet est articulé autour du protocole, qui pour but d'établir les règles de communication, et nécessite une architecture client / serveur. A partir du protocole, il est possible de concevoir un logiciel capable d'interagir avec les autres logiciels YooGoo. YooGoo se veut sécurisé, il existe donc une couche de cryptage qui permet la confidentialité des communications. Cette particularité rend le sniffing (écoute des trames réseau) impraticable sur YooGoo. YooGoo se veut simple d'utilisation, lors de sa conception nous avons tiré partie de notre expérience des chats pour faire un protocole clair, avec des commandes simples et aux noms explicites. Le protocole est suffisamment intuitif pour que l'on puisse se connecter sur un serveur YooGoo à l'aide d'un simple client texte. Toutes les commandes retournent un message d'information suffisamment explicite pour que l'utilisateur sache d'où vient l'erreur. Néanmoins, l'utilisation d'un client texte étant peu attrayante, nous avons aussi créé un client graphique. Celui-ci est suffisamment intuitif pour être utilisé sans avoir besoin de lire l'aide. Mais, il est suffisamment puissant par tirer pleinement parti des fonctions du serveur YooGoo. Nous voulons aussi un client entièrement personnalisable, car YooGoo se veut capable de s'adapter. YooGoo est en priorité basé sur des salons de discussion (appelés aussi canaux). Il permet l'administration des canaux grâce à un système de hiérarchie totalement invisible pour l'utilisateur normal. YooGoo gère aussi une liste de contacts qui permet de savoir qui est connecté en même temps que soi sur YooGoo. Nous avons aussi ajouté la possibilité à l'utilisateur de s'inscrire sur YooGoo et d'y avoir un profil. Tout ceci est géré par une base de données. Nous avons aussi pensé à l'administration du serveur en incluant des commandes d'administration. L'administrateur a ainsi la possibilité de loguer (enregistrer dans un fichier) tous les événements du serveur. Il est ainsi possible de tirer des statistiques sur l'utilisation du serveur.

YooGoo se veut sécurisé, simple d'utilisation et capable de s'adapter.

Le cahier des charges

Le centre du projet est le protocole. C'est lui qui représente le plus la dimension que nous voulions donner au projet. En effet la rédaction du protocole demande un grand effort de conception, car toute erreur peut faire perdre énormément de temps par la suite. C'est aussi grâce à lui que notre projet peut être réutilisable car il établit une norme entre tous les logiciels YooGoo (clients et serveurs).

Pour montrer la puissance du protocole il fallait réaliser des applications qui l'utilisent. Le serveur devait gérer toutes les connexions, rediriger les messages et traiter les diverses commandes des utilisateurs. Pour cela il nous fallait réaliser une base de donnée qui puisse traiter une grande quantité d'informations et le plus rapidement possible. Toutes les communications étant sécurisées, il fallait gérer la couche de cryptage et de décryptage. Le client quant à lui devait permettre de réaliser toutes les opérations du protocole uniquement avec la souris, de plus il devait avoir une interface agréable et permettre de s'adapter aux utilisateurs.

De plus, nous voulions faire un effort sur la présentation du projet. Pour cela il fallait réaliser un site web, agréable qui regroupe toutes les informations sur le projet. Un effort devait aussi être fait sur la documentation du projet.

Déroulement du projet

Dans un premier temps nous avons conçu le protocole, que ce soit du point de vue des différentes commandes que de la couche sécurité. Un serveur de base gérant les connexions multiples et le cryptage des messages a été réalisé. Les squelettes des différents objets, utiles pour la suite, ont été développés. Les types abstraits de la base de données ont étés créés.

Ensuite l'implémentation du protocole a démarré et l'intégration de la base de données a été faite. A la fin de cette deuxième étape nous avions un serveur qui permettait d'effectuer les commandes de base, et permettait à plusieurs utilisateurs de discuter. Le client d'abord en mode texte, commençait à avoir un début d'interface graphique.

Ensuite le serveur a été achevé en finissant d'implémenter toutes les fonctions du protocole. Tandis que le client se dotait d'une interface graphique avancée mais pas encore fonctionnelle.

Enfin les fonctionnalités du client ont été implémentées et les différentes documentations ont été rédigées.

Au final nous avons :

  • Un serveur qui gère toutes les fonctions du protocole.
  • Un client graphique personnalisable et simple d'emploi.
  • Un protocole et sa SDK pour permettre à d'éventuels développeurs de réaliser soit un serveur soit un client YooGoo.
  • Une librairie de communication cryptée facilitant le développement d'un client YooGoo.
  • Un site web : www.yoogoo.com.
  • Un manuel d'aide et d'installation.
  • Un cédérom contenant les exécutables, les sources, les rapports, le site web et toute la documentation.

Les rôles de chacun

Jérôme
: Il s'est occupé de la conception du protocole, et de la rédaction son SDK (Software Development Kit). Il a aussi implémenté le protocole (les utilisateurs, les canaux, les fonctions d'administration) sur le serveur. C'est aussi le webmaster du site.
Marco
(chef de projet): En temps que chef de projet il a dirigé l'organisation de l'équipe. Il s'est occupé de la partie réseau et cryptage que ce soit sur le serveur que sur les clients. Il s'est occupé de la gestion des messages sur le serveur (récupération et parsage) Il a aidé Jérôme à implémenter le protocole (liste de contacts, gestion des utilisateurs). Il a aidé Franck sur le client (liste de contact, options, threads).
Franck
: Il a réalisé les objets de la base de donnée (canal et utilisateur). Il est aussi le programmeur principal du client. Il s'est occupé notamment de l'interface, de la réalisation d'une librairie de communication, ainsi que de la plupart des fonctionnalités du client.
Stéphane
: Il a d'abord implémenté le type abstrait d'AVL servant de base pour la base de donnée. Il a ensuite créé l'objet base de donnée du serveur. Il a ensuite aidé Franck sur le client (parsage du texte, peer to peer, recherche). Il a rédigé l'aide du client et le manuel d'installation.

Nous pensons que le projet est un travail d'équipe. Donc pour ne marquer de différences entre les différents membres, le rapport est écris avec un ton impersonnel afin que le plus important soit ce qui a été réalisé et comment cela a été fait plutôt que qui l'a réalisé. Les réalisations de personnes différentes peuvent donc très bien être mélangés dans un même paragraphe.

Découpage du rapport

Tout au long du rapport nous nous sommes efforcés d'expliquer d'abord l'architecture générale avant de rentrer dans les détails techniques. Nous essayons d'expliquer nos choix à chaque fois que possible. Mais surtout nous vous prouverons que YooGoo est sécurisé, simple d'utilisation et capable de s'adapter.

Le rapport se compose de trois grandes parties. Tout d'abord nous allons parler du protocole "YooGoo" pour présenter les nombreuses fonctionnalités qu'il propose ainsi que les différentes règles établies. Nous expliquerons les concepts, tels que les canaux, qui seront utilisés par la suite. Nous verrons aussi les trois grandes classes de fonctions, celles portant sur les utilisateurs, celles portant sur les canaux, et celles des commandes d'administrations. Nous expliquerons aussi les différents niveaux d'utilisations que l'on peut avoir.

Ensuite nous parlerons du serveur. D'abord en expliquant son architecture interne et l'interaction entre les différents objets qui le composent. Puis en regardant comment ont été réalisés les différentes pièces de cette architecture. Nous parlerons alors de cryptographie, de programmation réseau, de la base de données (le contenu et le contenant), et de l'implémentation du protocole.

Enfin nous nous intéresserons au client. Comme pour le serveur nous commencerons par expliquer l'architecture de celui-ci. Puis nous passerons à l'interface en expliquant comment elle a été réalisée et surtout comment elle interagie avec toutes les autres pièces. Chaque partie du client sera ensuite abordée afin de décrire son fonctionnement.

Pour conclure nous ferons le bilan de ce qui a été réalisé, du déroulement du projet, et des connaissances abordées et acquises. Chacun d'entre nous exprimera son opinion personnelle. Mais surtout nous jugerons si nos buts ont étés atteints.

Le Protocole

Conception

Le protocole est le langage qui permet à deux applications réseau de communiquer. Nous avons donc écrit un protocole de communication YooGoo. Si une application respecte le protocole, elle est en mesure de communiquer soit avec le client, soit avec le serveur de YooGoo. De plus, le protocole nous a permis de coder le serveur et le client indépendamment l'un de l'autre. Il suffit qu'ils respectent tous les deux le protocole pour qu'ils soient compatibles.

Lors de la conception du protocole, nous avons fait bien attention à ce que celui-ci soit "logique". Il ne faut pas qu'il y ait de fonctions incompatibles, ni de fonction double. Dans un protocole tel que le FTP et HTTP, il n'y a pas besoin de faire passer de l'information entre les clients. Ce n'est pas le cas dans Yoogoo. Chaque connexion au serveur est totalement indépendante. Comme YooGoo ressemble pour la plupart de ses fonctionnalités à l'IRC, nous nous sommes beaucoup inspiré du protocole de l'IRC (cf. RFC 1412 disponible sur www.YooGoo.com).

Principes

Nous avons fait en sorte que le protocole soit assez intuitif par le nom des fonctions et les paramètres. Le protocole n'est pas sensible à la case des caratères. Un utilisateur anglophone ne devrait pas avoir de problèmes pour comprendre le fonctionnement de Yoogoo seuleument en regardant le nom des fonctions. Nous nous sommes conformé à certaines règles qui permettent au protocole d'être logique. Par exemple, toutes les commande Commencant par CH permettent de changer un champ de son profile et les commandes commencant par CHCHAN permettent de changer un champ du profile d'un canal.

Lorsqu'une commande a besoin de renvoyer une information au client, le nom de ces fonction commence toujours par RPL_. Parfois, la commande a besoin de renvoyer plusieurs information au client (comme le resultat d'une recherche). On utilise alors une commande RPL_ pour chaque message d'information, puis, on termine avec RPL_END. Les commande commençant par RPL_END servent donc de balise de fin de transmition.

Il arrive que le serveur ait besoin d'envoyer un évènement au client. Le message est alors le même que la commande qui l'a provoqué.

Par exemple :

            Hargos -> msgChan Epita "Salut"
            Hargos <- msgChan Hargos Epita "Salut"
            Danao <- msgChan Hargos Epita "Salut"
            Zapata <- msgChan Hargos Epita "Salut"
            Folken <- msgChan Hargos Epita "Salut"
Nous avons hésité au début à envoyer des message plus évolué du genre "HARGOS" S'EST JOIN AU CANAL "EPITA". Ce genre de message offrait l'avantage d'être plus compréhensible pour l'utilisateur. Mais, le message aurait été plus difficile à parser pour le client. De plus, le message aurait été dans une seule langue et nous aurions eu des problèmes pour le traduire.

Retours d'erreurs

Pour chaque fonction un code d'erreur est renvoyé au client. Pour chaque commande envoyées, il y a un unique message d'erreur. Tant que le client n'a pas recu ce code, la commande est en cours d'execution. La valeur 00 signifie que tout s'est bien passé. Tous les codes $<$ 100 signifient que ce ne sont pas des erreurs fatales. Les erreurs comprises entre 400 et 499 signifient que l'erreur est fatale et que la fonction s'est terminée anormalement. Dans le protocole, les codes $<$ 100 commencent par INF_ et les code d'erreurs commence par ERR_. Ces codes d'erreur sont important car ils seront aussi utilisés en interne dans le serveur. Nous avons décidé de ne pas renvoyer d'erreur de la part du client. Effectivement, nous pensons qu'il n'est pas utile au serveur de savoir si la donnée est bien reconnue (il ne peut rien y faire s'il y a une erreur...). De plus, les messages envoyés par le serveur sont souvent des messages d'informations, il y a donc que très peu d'erreur possible. Je rappelle que TCP/IP possède un code de correction d'erreur et qu'une donnée arrive forcement à destination sans erreurs.

Le cryptage des données

YooGoo est crypté, donc, toute la première phase de la connexion se compose de la génération et de l'échange des clés de cryptage. Dans la suite, le cryptage est totalement transparent. Lorsque nous appelons la fonction permettant l'envoie de donnée, celle-ci crypte les données avant de les envoyer.

Récapitulatif

Il existe plusieurs algorithmes de cryptage. Les plus simples et les plus anciens sont les algorithmes secrets. Ce sont des algorithmes dont on ne connaît pas le procédé de cryptage. Il est tenu secret. Cette méthode ne nous convient pas, car les sources de notre projet sont disponibles pour tous les utilisateurs, donc l'algorithme de cryptage ne sera pas secret. Il existe alors des algorithmes à clé. L'algorithme est alors connu de tous mais la clé doit rester secrète car c'est d'elle que dépend la sécurité du message. Ceci permet de pouvoir utiliser un même algorithme autant de fois que l'on veut, car la sécurité ne dépend pas du procédé mais de la clé. C'est donc cette méthode qui a été retenue pour YooGoo. On distingue alors deux types de cryptage à clé. Les algorithmes symétriques et asymétriques.

Le cryptage symétrique, et en particulier le DES (Data Encryption Standard) que nous utiliserons, a besoin d'une seule clé pourcrypter et décrypter le message. Cette clé doit alors être tenue secrète et les deux parties communicantes doivent derenir la clé. L'avantage est que ces algorithmes sont assez rapides et permettent le cryptage d'un grand nombre de données. Mais l'inconvénient est que les deux parties doivent detenir la clé. Dans notre cas cela implique l'échange de la clé entre le client et le serveur. Il faut donc trouver une autre méthode pour cet échange. De plus le DES est devenu peu sûr à cause des machines très puissantes actuelles. Nous utiliserons alors un triple-DES qui revient à crypter 3 fois le message (et donc avoir 2 ou 3 clés). Dans YooGoo on utilisera donc le DES pour crypter les messages entre les clients et le serveur.

Le cryptage asymétrique, et en particulier le RSA, fonctionne sur un principe de clé publique et de clé privée. On appelle clé publique la clé qui est accessible à tout le monde et qui permet de crypter les messages. La clé privée, qui elle doit être tenue secrète, sert à décrypter le message. On s'aperçoit alors que tout le monde peut crypter un message et que seulement une personne peut le décrypter. Ainsi, il est parfois impossible de savoir de qui provient le message. De plus le RSA se base sur des propriétés arithmétiques qui exigent des calculs sur de très grands nombres (pour que l'algorithme soit suffisamment sûr), ce qui fait que le temps de cryptage est beaucoup plus long que des algorithmes tels que le DES. Par conséquand le RSA sera seulement utilisé pour l'échange de la clé entre le serveur et les clients.

Le DES

Principe:

DES est un chiffrement par bloc, ce qui signifie qu'il traite les données en sections de taille fixe, appelées blocs. La taille d'un bloc DES est de 64 bits ; si le volume de données à chiffrer n'est pas un multiple pair de 64 bits, les données sont complétées d'une façon dépendante de l'application (par des '$\backslash$0' dans YooGoo).

La sécurité du DES repose sur le même principe qu'un écran de fumée ou, en jargon cryptographique, sur les principes de confusion et de diffusion. Le but de la confusion est de cacher toute relation existante entre le texte clair, le texte chiffré et la clé. Le but de la diffusion est de répartir les effets conjugués du texte clair et de la clé sur la plus grande longueur possible de texte chiffré. Ces deux principes rendent l'analyse cryptographique très difficile.

Avec DES, on chiffre un bloc de texte clair en réalisant une série de permutations et de substitutions sur celui-ci. Leur conséquence sur le texte clair est essentiellement fonction des 16 sous-clés, $K_1$, $K_2$, ... , $K_{16}$, dérivées d'une clé de départ, $K_0$ qui est celle que l'on fournit. Pour chiffrer un bloc de texte clair, les sous clés sont appliquées tour à tour ($K_1$, $K_2$, ..., $K_{16}$) sur les données à l'aide d'une suite d'opérations répétées 16 fois par clé. Chaque itération s'appelle un round. Le déchiffrement d'un bloc crypté s'effectue de la même façon, mais en appliquant les clés en sens inverse ($K_{16}$, $K_{15}$, ..., $K_1$).

Tous les tableaux utilisés lors des décalages, permutations, transformations et rotations, sont établis par une norme DES pour que l'algorithme ai une confusion et une dispersion maximum. La description de l'algorithme est très fastidieuse car les opérations effectuées peuvent sembler sorties de nulle part (c'est d'ailleurs un peu le cas).

Calcul des sous-clés

La première étape consiste à calculer les sous-clés à partir de la clé de départ. DES utilise une clé de 56 bits, mais nous en fournissons une de 64 bits car, car les 8 bits restants sont utilisés dans les implémentations matérielles du DES. Pour obtenir la clé de 56 bits, on effectue une transformation de clé selon le tableau Des_Transform[].

        // Correspondances pour la transformation de la clé
        static const int Des_Transform[56] =
        {
            57, 49, 41, 33, 25, 17,  9,  1, 58, 50, 42, 34, 26, 18,
            10,  2, 59, 51, 43, 35, 27, 19, 11,  3, 60, 52, 44, 36,
            63, 55, 47, 39, 31, 23, 15,  7, 62, 54, 46, 38, 30, 22,
            14,  6, 61, 53, 45, 37, 29, 21, 13,  5, 28, 20, 12,  4
        };

Chaque position $p$ du tableau contient la position du bit de la clé de départ occupant la position $p$ de la clé transformée. Le bit 57 de la clé de départ, devient par exemple le bit 1 de la clé transformée ; de même, le bit 49 devient le bit 2, et ainsi de suite. La convention est de numéroter les bits de gauche à droite, en commençant à 1.

Après avoir transformé la clé en 56 bits, on calcule les sous-clés en divisant d'abord la clé en deux blocs de 28 bits. Puis, pour chaque sous-clé, on effectue une rotation des deux blocs d'un nombre de fois dépendant du round dans lequel on utilisera la sous-clé selon le tableau Des_Rotations[], puis on réunit les blocs.

        // Nombre de rotations pour le calcul des sous-clés
        static const int Des_Rotations[16] =
        {
        1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
        };

Après cela on réduit les sous-clés de 56 bits, formées des blocs réunis, à 48 bits en les permutant selon le tableau Des_Permute[].

        // Correspondances pour le choix permuté des sous-clés
        static const int Des_Permute[48] =
        {
        14, 17, 11, 24,  1,  5,  3, 28, 15,  6, 21, 10,
        23, 19, 12,  4, 26,  8, 16,  7, 27, 20, 13,  2,
        41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48,
        44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32
        };
Cette permutation s'appelle "choix permuté". Ce traitement est répété pour chacune des 16 sous-clés. Le but de tout cela est de s'assurer que l'ont peut appliquer, dans chaque round, les différents bits de la clé de départ aux données.

Pour gagner en rapidité d'exécution, lorsqu'on ne passe pas de clé en paramètre à la fonction de cryptage, c'est alors la clé utilisée lors de l'appel précédent qui est utilisée. Les sous-clés sont déclarées en "static", et ne sont alors pas recalculées.

Chiffrement et déchiffrement des blocs de données

Lorsque l'on a préparé les sous-clés, on est prêt à chiffrer ou déchiffrer des blocs de données. On commence par permuter les blocs de données de 64 bits selon le tableau Des_Initial[].

        // Correspondances pour la permutation initiale des blocs
        static const int Des_Initial[64] =
        {
        58, 50, 42, 34, 26, 18, 10,  2, 60, 52, 44, 36, 28, 20, 12, 4,
        62, 54, 46, 38, 30, 22, 14,  6, 64, 56, 48, 40, 32, 24, 16, 8,
        57, 49, 41, 33, 25, 17,  9,  1, 59, 51, 43, 35, 27, 19, 11, 3,
        61, 53, 45, 37, 29, 21, 13,  5, 63, 55, 47, 39, 31, 23, 15, 7
        };
Cette permutation porte, fort à propos, le nom de permutation initiale. Elle n'améliore pas la sécurité du DES, mais elle doit être effectuée pour être conforme à la norme DES. Après la permutation initiale, le bloc de 64 bits de données est divisé en deux blocs de 32 bits, $L_0$ et $R_0$.

Après la permutation initiale le bloc de données passe par une série d'opérations répétées pendant 16 rounds. Le but de chaque round $i$ est de calculer $L_i$ et $R_i$, utilisés par le round suivant, jusqu'à obtenir finalement le bloc de données $R_{16}L_{16}$. On commence chaque round avec $L_{i-1}$ et $R_{i-1}$ et on étend $R_{i-1}$ de 32 à 48 bits à l'aide d'une fonction d'expansion, selon le tableau Des_Expansion[].

        // Correspondances pour la fonction d'expansion des blocs
        static const int Des_Expansion[48] =
        {
            32,  1,  2,  3,  4,  5,  4,  5,  6,  7,  8,  9,
            8,  9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17,
            16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25,
            24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32,  1
        };
Le but principal de cette fonction est de créer un effet d'avalanche lors du chiffrement des données : un seul bit du bloc de données affecte plus de bits à l'étape suivante, ce qui provoque donc une diffusion. Après cette étape, on calcule le OU exclusif (note $\oplus$) du résultat de 48 bits et de $K_i$, la sous-clé du round : cela donne un résultat intermédiaire de 48 bits, appelé $R_{int}$. Soit $E$, la fonction d'expansion, les opérations réalisées jusqu'à maintenant dans le round peuvent s'exprimer comme suit:

\begin{displaymath}R_{int} = E(R_{i-1}) \oplus K_i\end{displaymath}


Puis, $R{int}$ subit huit substitutions réalisées a l'aide de huit boîte-S. Chaque boîte-S j prend un bloc de 6 bits, de la position $6j$ à $6j+6$ dans $R_{int}$ et recherche une valeur de 4 bits pour lui dans la table Des_SBox[]. Cette valeur est écrite dans le tampon à la position $4j$.

        // Tableaux pour les substitutions par boîtes-S sur les blocs
        static const int Des_SBox[8][4][16] =
        {
        {
        {14,  4, 13,  1,  2, 15, 11,  8,  3, 10,  6, 12,  5,  9,  0,  7},
        { 0, 15,  7,  4, 14,  2, 13,  1, 10,  6, 12, 11,  9,  5,  3,  8},
        { 4,  1, 14,  8, 13,  6,  2, 11, 15, 12,  9,  7,  3, 10,  5,  0},
        {15, 12,  8,  2,  4,  9,  1,  7,  5, 11,  3, 14, 10,  0,  6, 13},
        },
                ...
        {
        {13,  2,  8,  4,  6, 15, 11,  1, 10,  9,  3, 14,  5,  0, 12,  7},
        { 1, 15, 13,  8, 10,  3,  7,  4, 12,  5,  6, 11,  0, 14,  9,  2},
        { 7, 11,  4,  1,  9, 12, 14,  2,  0,  6, 10, 13, 15,  3,  5,  8},
        { 2,  1, 14,  7,  4, 10,  8, 13, 15, 12,  9,  0,  3,  5,  6, 11},
        },
        };
Pour lire la table Des_SBox[], on trouve la boîte-S $j$, on recherche le numéro de ligne ayant la valeur de deux bits, formée par le premier et le dernier bit du bloc de six bits, et le numéro de colonne ayant la valeur de quatre bits formés des bits du milieu de ce même bloc (les numéros de lignes et colonnes commencent à 0). Les boîtes-S ajoutent de la confusion aux données, et font, plus que tout autre chose, la sécurité de DES : elles ont donc été longtemps l'objet d'examens minutieux.

Lorsque les substitutions par boîtes-S sont terminées, le résultat est une valeur de 32 bits que l'on permute à l'aide d'une boîte-P, selon le tableau Des_PBox[].

        // Correspondances pour la permutation-P
        static const int Des_Pbox[32] =
        {
        16,  7, 20, 21, 29, 12, 28, 17,  1, 15, 23, 26,  5, 18, 31, 10,
        2,  8, 24, 14, 32, 27,  3,  9, 19, 13, 30,  6, 22, 11,  4, 25
        };

A cette étape, il est pratique de considérer les opérations du round comme une fonction $f$. Si $b_j$ est $j^e$ bloc de six bits de $R_{int}$, $S_j$ la $j^e$ boîte-S, et P la permutation-P, cette fonction se définit par :

\begin{displaymath}f = P(S_1(b_1),S_2(b_2),\cdot,S_8(b_8))\end{displaymath}

La dernière opération de chaque round consiste à calculer le OU exclusif du résultat de $f$, sur 32 bits, et du bloc gauche original passé au round, $L_{i-1}$. Lorsque cela est fait, on échange les blocs gauche et droit et on commence le round suivant. Au dernier round, cependant, on n'effectue pas cet échange. Mis ensembles, les calculs des $L_i$ et $R_i$ de chaque round peuvent être ainsi énoncés:

\begin{displaymath}L_i = R_{i-1}\end{displaymath}


\begin{displaymath}R_i = L_{i-1} \oplus f(R_{i-1},K_1)\end{displaymath}

Lorsque les 16 rounds sont terminés, on concatène le bloc droit final, $R_{16}$, avec le bloc gauche final, $L_{16}$, pour produire le bloc de 64 bits, $R_{16}L_{16}$ (le bloc droit et le bloc gauche ne sont pas échangés dans le round final : le dernier bloc droit est donc à gauche et le dernier bloc gauche à droite). L'étape finale consiste à permuter $R_{16}L_{16}$ selon le tableau Des_Final[].

        // Correspondances pour la permutation finale des blocs
        static const int Des_Final[64] =
        {
        40,  8, 48, 16, 56, 24, 64, 32, 39,  7, 47, 15, 55, 23, 63, 31,
        38,  6, 46, 14, 54, 22, 62, 30, 37,  5, 45, 13, 53, 21, 61, 29,
        36,  4, 44, 12, 52, 20, 60, 28, 35,  3, 43, 11, 51, 19, 59, 27,
        34,  2, 42, 10, 50, 18, 58, 26, 33,  1, 41,  9, 49, 17, 57, 25
        };
Cette permutation s'appelle évidemment "permutation finale"; elle annule simplement ce que la permutation initiale avait fait plus tôt. Lorsque l'on chiffre les données, le résultat est un bloc de 64 bits de texte chiffré ; lorsque l'on déchiffre, c'est le bloc de 64 bits de texte clair.

Chiffrement par blocs

Comme nous avons vu, le DES chiffre par blocs de 64 bits. Mais dans la plupart des cas l'information à chiffrer n'est pas égale à 64 bits. Si elle est inférieure on complète de façon à aboutir à une donnée de 64 bits. Si elle est supérieure a 64 bits comme dans la plupart des cas, on découpe la donnée en bloc de 64 bits. On invoque alors le DES plusieurs fois sur chacun de ces blocs, on parle alors de mode de chiffrement par blocs.

La façon la plus simple de traiter plusieurs blocs de données consiste à ajouter chaque bloc de texte chiffré généré à ceux qui ont été générés avant lui. Cette approche primitive s'appelle ECB, ou " Electronic Code Book ". Sa simplicité la rend très populaire, mais elle est relativement peu sûre : son problème principal est que, pour une clé donnée, un bloc de texte clair sera toujours chiffré par le même bloc de texte chiffré, où qu'il apparaisse dans les données. Cela signifie que si un quelqu'un arrive à briser ne serait-ce qu'une partie des données, il peut commencer à mettre en place un décodeur pour également briser les autres parties. CBC, ou " Cipher Block Chaining " constitue une meilleure approche. C'est donc cette méthode que nous avons décidé d'utiliser.

CBC évite les problèmes de ECB en augmentant un bloc chiffré à l'aide d'opérations simples et d'une rétroaction. La rétroaction fait que chaque bloc chiffré dépend, d'une certaine façon, d'actions réalisées auparavant. Dans le mode CBC, les blocs chiffrés précédents servent à alimenter la suite, afin q'un bloc de texte clair identique ait toutes les chances d'être chiffré différemment à chaque apparition.

Pour que les blocs chiffrés précédemment puissent servir à la rétroaction, avant de chiffrer un bloc de texte clair , on fait un OU exclusif de celui-ci avec le bloc chiffré généré avant lui. Lorsque l'on déchiffre le texte, on fait un OU exclusif de chaque bloc déchiffré avec le bloc chiffré suivant. Exprimé simplement on a :

\begin{displaymath}C_i = E_K(P_i \oplus C_{i-1}\end{displaymath}


\begin{displaymath}P_i = C_{i-1} \oplus D_K(C_i)\end{displaymath}

$C_i$ et $P_i$ et $E_K$ et $D_K$ sont, respectivement, les $i^e$ blocs de texte chiffré et clair des tampons $C$ et $P$, et $E_K$ et $D_K$ sont les opérations de chiffrement et de déchiffrement utilisant la clé $K$.

Le RSA

Principe

Comme le DES, le RSA est un chiffrement par bloc, mais la taille du bloc varie selon celle des clés. Dans YooGoo nous utilisons des clés de 256 bits, ce qui nous permet dans le cas d'un triple-DES d'envoyer 3 clés DES ($3*64 < 256$).

RSA est considéré très sûr, mais il est considérablement plus lent que DES. Comme pour ce dernier, la sécurité du RSA n'a jamais été prouvée, mais elle est liée au problème difficile de factoriser des grands nombres (des nombres contenant au moins 200 chiffres). Comme on ne connaît aucune solution efficace à ce problème, on suppose qu'il n'existe pas de moyen efficace de briser RSA.

RSA est fondé sur des principes moins obscurs que les permutations numériques et les substitutions utilisées par le DES. Le chiffrement et le déchiffrement des données dépendent d'une exponentiation modulaire, une opération de l'arithmétique des modulos.

Calcul des clés publiques et privées

Avec RSA, la clé publique et la clé privée fonctionnent ensemble comme une paire. La première sert à chiffrer un bloc de données, après quoi seule la clé privée correspondante peut permettre de le déchiffrer. Lorsque l'on génère les clés, on suit plusieurs étapes afin d'assurer que ce mariage fonctionne. Ces étapes vérifient également qu'il n'y a pas moyen de déterminer une des clés à partir de l'autre.

Pour commencer, on choisit deux grands nombres premiers, appelés $p$ et $q$. Etant donné les techniques de factorisation actuelles, ces nombres doivent posséder au moins 200 chiffres pour être considérés comme sûrs. Puis, on calcule $n$, le produit de ces nombres :

\begin{displaymath}n = pq\end{displaymath}

On choisit alors un petit entier impair, $e$, qui fera partie de la clé publique. Le critère le plus important dans le choix de $e$ est qu'il ne doit pas avoir de facteur commun avec $(p - 1)(q -
1)$ : en d'autres termes, $e$ et $(p - 1)(q -
1)$ sont premiers entre eux. Nous utilisons 17, car c'est un nombre premier, nous sommes alors sûr qu'il n'est pas de facteur commun. L'utilisation d'une valeur précise pour $e$ ne compromet pas la sécurité de RSA car le déchiffrement des données est fonction de la clé privée.

Ensuite, on calcule une valeur correspondante $d$ qui fera partie de la clé privée. Pour cela, on calcule l'inverse de $e$, modulo $(p - 1)(q -
1)$, de la façon suivante :


\begin{displaymath}d = e^{-1} mod(p - 1)(q - 1)\end{displaymath}

Maintenant que nous avons des valeurs pour $e$ et $d$, on publie $(e,n)$ comme étant la clé publique $P$ et on garde $(d,n)$ secret comme clé privée $S$ :


\begin{displaymath}P = (e,n)\end{displaymath}


\begin{displaymath}S = (d,n)\end{displaymath}

Ceux qui chiffrent les données utilisent $P$ et ceux qui déchiffrent, $S$. Pour s'assurer que quelqu'un connaissant $P$ ne puisse calculer $S$, les valeurs utilisées pour $p$ et $q$ ne doivent jamais être révélées.

La sécurité offerte par le couple $P$ et $S$ vient du fait que la multiplication est une fonction à sens unique, ce qui est fondamental en cryptographie. Exprimé simplement, une fonction à sens unique est une fonction relativement simple à calculer dans un sens, mais impossible à trouver dans l'autre. Dans RSA, par exemple, la multiplication de $p$ et $q$ est facile, mais la factorisation de $n$ en $p$ et $q$ est extrêmement lente si les valeurs choisis pour $p$ et $q$ sont suffisamment grandes.

Chiffrement et déchiffrement des blocs de données

Pour chiffrer et déchiffrer des données avec RSA, il faut d'abord choisir une taille de bloc en s'assurant que la plus grande taille que pourra contenir ce bloc sera inférieure à $n$. Si, par exemple, $p$ et $q$ sont des nombres premiers de 200 chiffres, n fera à peine moins de 400 chiffres. On doit donc choisir une taille de bloc suffisamment petite pour ne stocker que les nombres ayant moins de chiffres que cela. En pratique, on prend souvent une taille de bloc d'un nombre de bits égal à la plus grande puissance de 2 inférieure à $n$. Si $n = 209$, par exemple, on choisirait une taille de bloc de 7 bits car $2^7 = 128$ est inférieur à 209 et $2^8 = 256$ est supérieur.

Pour chiffrer un bloc de texte clair $M_i$, le ième bloc de données d'un tampon $M$, on utilise la clé publique $(e,n)$ pour prendre la valeur numérique de $M_i$, l'élever à la puissance $e$, et prendre le résultat modulo $n$. Cela donne un bloc de texte chiffré $C_i$. Le modulo $n$ garantit que $C_i$ tiendra dans la même taille de bloc que celle du texte clair. Ainsi, pour chiffrer un bloc de texte clair, on a :


\begin{displaymath}C_i = M_i^e mod n \end{displaymath}

Pour obtenir de nouveau le texte clair $M_i$ à partir de $C_i$, ième bloc de texte chiffré d'un tampon $C$, on utilise la clé privée $(d,n)$ pour prendre la valeur numérique de $C_i$, l'élever à la puissance $d$ et prendre le résultat modulo $n$. Ainsi pour déchiffrer un bloc de texte chiffré, on a :


\begin{displaymath}M_i = C_i^e mod n\end{displaymath}

Nombres infinis

Comme nous l'avons vu, RSA est sûr que si l'on travaille sur des nombres très grands (200 chiffres au moins). Il faut alors créer une classe de nombres infinis qui permet de réaliser toutes les opérations classiques (addition, soustraction, division, multiplication, modulo, exponentielle).

La première méthode qui a été réalisée est d'utilisé des chaînes de caractères pour stocker les nombres. Cette méthode permet d'afficher et de saisir des nombres très facilement. Mais que très peu de caractères sont utilisés ('0'-'9') et on gâche alors beaucoup d'espace. Les opérations sont effectuées comme si on les faisaient à la main, c'est à dire que pour chaque chiffre on effectue l'opération et on garde une éventuelle retenue pour l'inclure dans le calcul du chiffre suivant. La classe ainsi réalisée marche très bien mais est malheureusement très lente.

Nous avons alors décidé de reprendre une classe déjà réalisée, qui reprend un peu la même méthode mais au lieu d'utiliser les caractères '0'-'9', utilise des unsigned long int. On met alors côte à côte tous ces nombres et on obtient alors un nombre de la taille que l'on veut (dans notre cas 7). Le calcul des opérations est plus rapide car on calcul alors par paquet de 32 bits. Il suffit alors comme précédemment de garder la retenue à chaque fois.

Génération de nombres infinis

Les nombres $p$ et $q$ doivent être premiers. Pour pouvoir générer une clé aléatoirement il faut donc pouvoir générer un nombre premier aléatoirement. La méthode la plus évidente serait de générer un nombre quelconque et puis de tester s'il n'est pas multiple d'un nombre premier inférieur ou égal à sa racine carré. Cette méthode est très lente surtout lorsqu'on travaille sur des grands nombres, de plus il faut avoir précédemment calculé tous les nombres premiers plus petits que le nombre aléatoire. C'est quand même la seule méthode sûr à 100%.

Mais pour accélérer le test, on à mis au point des méthodes probabilistes qui sont fiables. La méthode de Rabin-Miller est considérée actuellement comme la plus efficace. Il faut l'effectuer 4 fois si on veut avoir une réponse quasiment exacte.

Le test de Miller-Rabin prend en entrée un entier impair $n > 1$ à tester. Il renvoie en sortie FAUX ou VRAI.

  1. Calculer $b$$b$ est le nombre de fois que 2 divise $n - 1$
  2. Poser $2^b * r = n - 1$$r$ est un entier impair.
  3. (A répéter 4 fois)
                A = rand(n-1)       // 1 < a < n-1
                y = a^r mod n
                Si ( (y != 1) et (y != n - 1) ) alors
                    j := 1
                    Tant que ( (j < b) et (y != n - 1) ) faire
                        y := y^2 mod n
                        Si (y = 1) alors retourne (FAUX)
                        j := j + 1
                        Si (y != n - 1) alors retourne (FAUX)
                    Fin fant que
                Sinon retourne  (VRAI)
    

L'utilisateur

Un serveur de chat a pour but de permettre à plusieurs utilisateurs de dialoguer entre eux. Chaque utilisateur a la possibilité d'échanger des messages avec les autres. De plus pour permettre aux utilisateurs de se regrouper, certaines informations peuvent être publiées aux autres, et éventuellement, enregistrées sur le serveur.

Deux types d'utilisateur.

Non-enregistré

Un utilisateur qui se connecte ponctuellement sur YooGoo, peut ne pas vouloir fournir d'informations le concernant. Il est alors un peu comme un invité. Aucune des informations qu'il fournit ne sont conservées sur le serveur. A peine il sera déconnecté que son profil n'existera plus, il est obligé d'envoyer les informations à chaque fois. Un utilisateur à tout de même accès à toutes les fonctions de YooGoo.

Enregistré

Un utilisateur régulier de YooGoo a fortement intérêt à s'enregistrer. Toutes les informations qu'il fournit sont alors enregistrées sur le serveur. Ainsi à chaque connexion il retrouve toutes les informations qu'il a pu rentrer dans ses précédentes sessions.

Enregistrement

La commande REG PASSWORD permet d'enregistrer le pseudo sous lequel on est connecté. Par la suite cet utilisateur devra taper son mot de passe à la connexion pour pouvoir accéder au serveur. Ainsi on est sûr que ce soit toujours la même personne qui utilise ce nom. Pour se désenregistrer, on utilise la commande UNREG (sans paramètre). Chaque enregistrement crée une entrée dans une base de données d'utilisateur. Lorsqu'on se désinscrit l'entrée est supprimée, et il ne reste plus de trace de l'utilisateur.

Informations.

Les utilisateurs ont donc la possibilité d'enregistrer des informations les concernant afin de les rendre disponibles aux autres utilisateurs. Chaque utilisateur, enregistré ou pas, à un mini profil contenant son pseudo, sa ville, sa date de naissance et son genre. Un utilisateur enregistré a, quant à lui, un profil complet avec nom, prénom, adresse etc.

Pour modifier les valeurs des champs de son profil, on utilise les fonctions CHNICK, CHGENDER,... et ainsi de suite pour tous les champs du profil. Ces fonctions testent les arguments passés en paramètre pour voir s'il est acceptable. Si aucune valeur est passée en paramètre à la fonction, on efface le contenu du champ.

Toutes ces informations sont accessibles aux autres utilisateurs grâce à deux fonctions. La première est ASV USER qui renvoie RPL_ASV NICK STATUS GENRE VILLE. On obtient aussi tout le mini profil. Cette fonction est utilisable aussi bien sur les utilisateurs enregistrés que sur ceux non enregistrés. La seule restriction est qu'ils doivent être connectés. La deuxième fonction est WHO USER. Elle renvoie plusieurs réponses RPL_WHO. La fonction ne renvoie que les champs qui ont une valeur non nulle. Les différentes réponses sont :

  • RPL_WHO n°1 = Pseudo + genre + âge + ville
  • RPL_WHO n°2 = Nom + Prénom + Adresse + Code Postal + Langue + Pays
  • RPL_WHO n°3 = Téléphone 1 + Téléphone 2 + Mail 1 + Mail 2
  • RPL_WHO n°4 = Photo + Description
La fin de la transmission est marquée par le message RPL_ENDWHO. La valeur du champ est précédée par un identifiant par exemple :
        Rpl_who NOM= Angeline SEXE= FEMALE AGE= 19821006 VILLE= Paris
La commande WHO ne marche que sur les utilisateurs enregistrés, connectés ou non.

Mais le plus grand intérêt du profil est de pouvoir effectuer des recherches dessus. Il existe une fonction de recherche qui renvoie la liste des utilisateurs correspondant aux critères que l'on a défini. Il est donc possible de faire une recherche multicritères. La fonction de recherche se prototype ainsi :

Search [Nick=Pseudo] [Age=Age][Birth=Date_Naissance] [Town=Ville]
       [Sex={Male|Female}][Status={OnLine|OffLine}]

On renvoie une réponse RPL_SEARCH PSEUDO pour chaque utilisateur trouvé.

Liste de contacts.

Principes.

La liste de contacts est une liste de personnes que l'on connaît et qui ont des droits spéciaux. Il y a deux types de personnes que l'on rajoute dans sa liste, des personnes que l'on aime bien avec qui on va partager certaines informations. D'autres qu'on essaye d'éviter, on pourra alors leur interdire certaines choses.

Les différentes fonctions.

La fonction permettant de gérer toute la liste de contacts est :

    contactlist add|get|del|change|accept|denie|status

Voici le détail de chaque sous fonction :

ADD USER [DROIT]
:
ajoute un utilisateur à la liste de contacts. Si droit n'est pas indiqué c'est la valeur par défaut qui sera donné comme droit. Si c'est un droit "mauvais" on envoie à l'utilisateur ajouté soit CL_BLACKLISTEDBY (l'utilisateur a été mis en liste noire) soit CL_IGNOREDBY (l'utilisateur est ignoré) soit CL_MUTEBY (l'utilisateur ne peut plus parler avec la personne). Si le droit est "ami" , on envoie alors une requête d'ajout à l'utilisateur concerné, sauf si celui-ci a aussi donné un droit "ami" à l'appelant.

DEL USER
:
supprime un utilisateur de la liste de contacts. Si l'utilisateur était un ami il est prévenu par CL_REMOVEDBY. Si l'utilisateur supprimé était un ami, on supprime le nom de l'appelant dans la liste des utilisateurs dont il est ami.

CHANGE USER [DROIT]
:
change les droits d'un utilisateur de la liste de contacts. Il faut faire attention aux cas où un "ennemi" devient un "ami" ou vice-versa. Si pas de droit la valeur par défaut est mise.

GET [USER]
:
permet de recevoir la liste de contacts d'un utilisateur. Sans paramètre la fonction renvoie la liste de contact de l'appelant. Sinon il faut que l'utilisateur ait les bons droits chez l'utilisateur dont il désire avoir la liste de contacts.
Renvoie RPL_CL PSEUDO DROIT (avec dix couples (pseudo, droit)maximum par réponse).

ACCEPT USER
:
permet d'accepter la demande d'ajout d'un ami.

DENIE USER
:
refuse la demande d'ajout d'un ami.

STATUS
:
renvoie le statut de tous les utilisateurs amis présents sur la liste de contacts.

Les droits.

Comme nous l'avons vu, il est possible de donner des droits "ami" comme des droits "mauvais" aux utilisateurs présents dans la liste de contacts.

Les différents droits sont :

CL_BLACKLIST
:
L'utilisateur cible est mis sur liste noire. Il n'a plus accès aux informations de l'utilisateur appelant (la fonction WHO et ASV lui sont inaccessibles). Il lui est impossible de parler avec l'utilisateur appelant. Si l'utilisateur cible essaye d'ajouter l'appelant à sa liste de contact, il obtiendra un refus automatiquement. De plus l'appelant ne reçoit plus les messages de la cible même lorsqu'il est connecté sur le même canal.

CL_NOREP
:
L'utilisateur appelant ne donne plus accès à ces informations (WHO et ASV inaccessibles). Un ajout sur la liste de contacts sera automatiquement refusé.

CL_MUTE
:
L'utilisateur appelant ne reçoit plus aucun message de la part de la cible, ni directement ni des canaux.

CL_NOINOF
:
L'utilisateur cache ces informations (WHO et ASV inaccessibles).

CL_ASVONLY
:
Le WHO est inaccessible à la cible.

CL_AMI1
:
L'utilisateur cible est un ami. La cible prévient de ses changements de statut, de ses connexions et déconnexions.

CL_AMI2
:
L'utilisateur cible est un ami. La cible prévient de ses changements de statut, de ses connections et déconnections. La cible peut avoir accès à la liste de contacts de l'appelant.

Le Statut.

Le statut permet d'indiquer dans quel état on est. Souvent certains utilisateurs restent connectés toute la journée, mais ils ne sont pas forcément devant leur ordinateur. Pour prévenir les autres dans un tel cas, il suffit de mettre son statut sur AWAY, pour indiquer que l'on est parti. Comme état il existe :

  • ONLINE, on est connecté et devant son ordinateur.
  • CHAT, on est disponible pour discuter.
  • AWAY, on n'est pas devant son écran.
  • DND, Do Not Disturb, on ne veut pas être dérangé.
  • OCCUPIED, on est occupé.
  • BRB, Be right Back, on revient tout de suite.
  • OFFLINE, non connecté.

A chaque changement de statut il faut prévenir toutes les personnes qui sont nos amis. Il faut aussi prévenir tous les utilisateurs qui sont connectés au même canal, ceci peut permettre à un client YooGoo de suivre l'évolution du statut des utilisateurs connectés sur un canal. Il faut aussi prévenir ses amis lorsqu'on se connecte et quand on se déconnecte. A la connexion il faut aussi recevoir les statuts de tous les utilisateurs amis présents dans la liste de contacts. Ceci se fait grâce à la fonction contactlist status. Cela est optionnel, donc ce n'est pas le serveur qui s'en occupe, c'est le client qui doit envoyer cette commande pour gérer une liste d'amis.

Communication avec les autres.

Que serait un serveur de chat sans commande pour dialoguer entre utilisateurs ? Dans YooGoo il existe deux façons de parler entre utilisateur, soit en passant par des canaux, soit en communiquant directement avec un utilisateur.

Pour une discussion directe il y a deux possibilités, soit en passant par le serveur, soit en connexion directe entre les deux personnes. Pour une connexion directe c'est la commande QUERY USER PORT qui sera utilisée. La commande query envoie à l'utilisateur cible l'adresse IP et le port où il doit se connecter pour parler avec l'autre utilisateur. C'est alors aux client de s'occuper d'établir la connexion et de communiquer. Le serveur ne s'occupe que de fournir l'adresse où se connecter. L'inconvénient de cette méthode est d'une part que chaque utilisateur connaît l'IP de l'autre, ce qui dans certains cas peut être dangereux (selon la personne avec qui on parle), d'autre part, on ne passe plus par le serveur, donc la communication n'est pas forcement cryptée. L'avantage est que le

trafic sur le réseau est beaucoupmoins important et plus rapide.

Si on communique en passant par le serveur, c'est la commande MSGUSER USER MSG qui sera utilisée. C'est le serveur qui s'occupe d'envoyer le message d'un utilisateur à l'autre. Le message est comme tous les autres, cryptés. De plus on n'a pas besoin de connaître l'IP de l'autre. Un autre grand avantage de cette méthode est que si l'utilisateur avec qui on parle est enregistré, on peut aussi lui envoyer des messages même s'il n'est pas connecté. Lors de sa prochaine connexion il recevra tous les messages qu'on lui a envoyés. Ceci est extrêmement pratique car souvent on ne peut pas être connecté en même temps, et il est difficile de croiser tout le monde.

L'autre possibilité de discussion est de passer par des canaux. Les canaux sont vus plus en détails dans la partie suivante.

Les canaux

Qu'est-ce qu'un canal, un salon et un chatroom?

Le principal but de YooGoo est d'accueillir ses utilisateurs sur des canaux. Un canal peut aussi être appelé Chatroom ou salon. Un salon est un "lieu" (ici, virtuel) où les utilisateurs discutent entre eux. Chaque phrase de chaque utilisateur est retransmise à tous les autres utilisateurs connectés sur le même salon.

Principes d'un Canal.

Un canal correspond en gros à une liste d'utilisateurs. Effectivement, toutes les fonctions basiques d'un canal peuvent se faire sur une liste. Lorsqu'un utilisateur envoie un message à un canal (MSGCHAN CANAL MESSAGE), on envoie un message à chaque utilisateur de la liste. Le message renvoyé a la structure suivante:

        msgChan Canal Pseudo_de_l_expéditeur Message

Lorsqu'un utilisateur arrive ou part du canal, un message informe les autres utilisateurs de l'évènement. Lorsqu'un utilisateur essaye de joindre un canal qui n'existe pas celui-ci est automatiquement créé. De même, lorsque le dernier utilisateur d'un canal s'en va, ce dernier est automatiquement détruit. Ceci pourra être modifié en changeant l'option KEEP que nous verrons plus loin.

L'utilisateur aura aussi besoin de savoir qui est connecté avec lui sur le canal. La fonction GETCONNECTED renvoie la liste des utilisateurs d'un canal. Pour cette fonction, nous utilisons la méthodes des RPL_ ... RPL_END. Un message RPL_GETCONNECTED est envoyé pour chaque utilisateur. La fin de la liste est signalée par le message RPL_ENDGETCONNECTEDS. GETCONNECTEDS renvoie aussi le statut et le grade des utilisateurs. Nous verrons tout a l'heure la hiérarchie sur les canaux. Ces information seront utiles au client pour faire la liste des connectés.

Par exemple :

            <- getconnecteds Epita
            -> Rpl_GetConnecteds Epita Danao ONLINE ChanRoot
            -> Rpl_GetConnecteds Epita Zapata AWAY User
            -> Rpl_GetConnecteds Epita Folken DND User
            -> Rpl_GetConnecteds Epita Hargos ONLINE Op
            -> Rpl_EndGetConnecteds
            -> 0 Inf_Ok

Hiérarchie et droits sur les canaux.

Les fonctions précédentes permettent d'avoir un canal de base. YooGoo permet une gestion avancée des canaux en ajoutant des droits à certains utilisateurs. Pour un utilisateur novice, cette partie administrative est totalement transparente. Son client lui affiche la liste des canaux, il fait un double click sur l'un d'entre eux pour le joindre puis, il peut discuter. Il existe quatre grades sur YooGoo. Tout d'abord l'utilisateur normal sans droit particulier. Il peut créer un canal (c'est-à-dire joindre un canal qui n'éxiste pas). A ce moment, il est considèré fondateur du canal et obtient tous les droits sur le canal. Il peut nommer une autre personne comme fondateur du canal pour par exemple l'administrer à deux ou bien avoir un utilisateur avec pouvoir de secours en cas de problème. Ces utilisateurs sont appelés ChanRoots. Il est possible sur des gros canaux d'avoir besoin de sous fifres pour gérer le canal. Mais, il peut être dangereux de leurs donner tous les droits. Ces utilisateur, avec des droits moins importants que le ChanRoot, sont des Opérateurs (on dira "Op" (prononcer Opé) dans la pratique). Chanroot peut décider des droits des Op. Enfin, tout en haut de l'échelle se trouve Root, c'est à dire l'administrateur du serveur. Il a le droits de tout faire, il n'a aucune barrière. Nous parlerons plus longuement de ses fonctions dans la partie "Fonctions d'administrations". Signalons enfin, qu'un utilisateur peut être banni d'un canal. Lorsqu'un utilisateur se joint à un canal, on vérifie tout d'abord qu'il n'est pas banni, si c'est le cas, sa demande est refusée et il ne peut joindre le canal.

Pour savoir quel grade a chaque utilisateur sur un canal, nous utilisons trois listes de pseudonymes (ou NickName, en anglais). Vu que les pseudo(nymes) sont protégés par mot de passe, on est sûr de l'identité d'un utilisateur. Il n'est donc pas possible de devenir ChanRoot sur un canal en usurpant l'identité de celui-ci. Les fonctions suivantes permettent l'ajout et la suppression dans les listes des ChanRoot/Op/bannis :

  • CHANROOT CANAL PSEUDO (nomme un ChanRoot)
  • DECHANROOT CANAL PSEUDO (supprime un utilisateur de la liste des ChanRoot)
  • OP CANAL PSEUDO (nomme un Op)
  • DEOP CANAL PSEUDO (supprime un utilisateur de la liste des Op)
  • BAN CANAL PSEUDO (bannit un utilisateur)
  • DEBAN CANAL PSEUDO (supprime un utilisateur de la liste des Bannis)

Pour gérer les droit des op, nous utilisons donc un entier où chaque bit correspond à une fonction que les op peuvent utiliser. Si le bit est à 0, ils n'y ont pas accès, sinon, ils peuvent utiliser la commande. ChanRoot a accès à la commande CHMOD CANAL COMMANDE {ONOFF} qui permet aux ops d'utiliser ou non $<$Commande$>$. La fonction CHMOD utilise un masque de bit pour mettre à jour la variable Droit. Si ChanRoot décide de mettre toutes les commandes sur OFF, l'OP n'a alors pas plus de droits d'un utilisateur normal. Si ChanRoot décide de mettre toutes les commandes sur ON, les OP ont alors presque autant de pouvoir que lui. Lors de la création du canal, les Ops ont par défaut tous les droits. Tous les champs sont donc à 1. Les seules commandes qui ne peuvent jamais devenir accessible aux OP sont CHANROOT (Nommer un ChanRoot), DECHANROOT (supprimer un utilisateur de la liste des ChanRoot), CHMOD (Modifier les droits des Ops) et CHKEEP et CHPRIV (que nous verrons plus bas). Ainsi, pour chaque commande d'administration du serveur, on vérifie que le demandeur est ChanRoot OU (Op ET A_le_Droit).

Voici un exemple de ce qui peut se passer sur un canal Epita où Hargos est ChanRoot, Danao est Op et Lamer est un utilisateur normal:

            Hargos -> ChMod Epita Ban OFF
            Hargos <- Inf_Ok
            **** Si l'op n'a pas le droit de bannir ***
            Danao -> ban Epita Lamer
            Danao <- Err_Privileges
            *** On modifie la variable Droit ***
            Hargos -> ChMod Epita Ban On
            Hargos <- Inf_Ok
            *** Ca marche ***
            Danao -> ban Epita Lamer
            *** Tout le canal est averti de l'évènement ***
            Danao <- ban Danao Epita Lamer
            Hargos <- ban Danao Epita Lamer
            Lamer <- ban Danao Epita Lamer
            Danao <- Inf_Ok

Aux fonctions permettant de gérer la hiérarchie sur le canal s'ajoute les fonctions permettant de gérer les petits détails du canal. Tout d'abord, la fonction KICK CANAL NICK. Quand un utilisateur ne respecte pas les règles sur un canal, un op peut décider de "kicker" celui ci, c'est-à-dire, le forcer à partir du canal. Ceci n'empêchera pas l'utilisateur de revenir tout de suite après, mais, elle sert d'avertissement au bannissement (généralement accompagné d'un kick). Un canal possède aussi des champs pour le nom du canal, son sujet, son mot de passe et sa catégorie. Ces champs peuvent être respectivement modifiées par CHCHANNAME, CHCHANTOPIC, CHCHANPASS et CHCHANCAT. Le sujet permet de donner un aperçu du sujet de discussion du canal. Le mot de passe empêche de joindre le canal si on ne le possède pas. Si la variable est vide, cela signifie qu'il n'y a pas de mot passe. Lorsqu'un utilisateur joint un canal protégé par mot de passe, il doit passer celui-ci en paramètre de la commande Join. On compare ce mot de passe avec celui du canal. S'ils sont différents, l'accès est refusé. Enfin, à la catégorie correspond un nombre qui permet de savoir sur quel sujet est le canal (A la manière catégories Yahoo : Informatique, Voyage, Informations, etc...)

Informations sur un canal.

Toutes ces informations peuvent être récupérées grâce à la commande WHAT CANAL. Cette commande renvoie diverses info sur le canal sous la forme suivante: RPL_WHAT NOM SUJET DROITS_DES_OP CATEGORIE NOMBRE_DE_CONNECTES NOMBRE_D'OP NOMBRE_DE_CHANROOT NOMBRE_DE_BANNIS.

Un utilisateur peut aussi recevoir la liste des ChanRoot/Op/bannis du canal grâce aux commande GETCHANROOTS, GETOPS, GETBANS. Ces commandes renvoient une liste de nom de la même façon que GETCONNECTEDS. C'est à dire RPL_GETOPS... RPL_ENDGETOPS... etc

Enfin, pour que les utilisateurs puissent parfaitement utiliser les canaux, il faut une commande permettant de donner la liste de ceux-ci. GETCHANS effectue cette tâche. Le message renvoyé est basé sur le même modèle que GETCONNECTED, GETBANS, etc...

Canal permanent, privé et invitations.

Au début de cette partie, nous avons dit que lorsque le dernier connecté sur un canal s'en allait, le canal était détruit. La variable Droits possède un bit supplémentaire permettant d'empêcher cela lorsque ce bit est à 1. CHKEEP permet de modifier la valeur de ce bit. CHKEEP n'est utilisable que pas ChanRoot. Ainsi, lorsque le dernier utilisateur s'en va, le canal et toute sa configuration sont conservés.

Nous savons qu'il existe deux sortes d'utilisateurs dans YooGoo, les enregistrés et les non-enregistrés. De plus les diverses listes utilisées pour l'administration d'un canal sauvegardent les noms des utilisateurs. Ces raisons font qu'un administrateur de canal peut désirer qu'il n'y ait que des utilisateurs enregistrés sur son canal. Pour cela il existe un fanion PRIVÉ. Celui-ci fonctionne comme le fanion KEEP. Il n'est utilisable que par le ChanRoot. Une fois activé, seuls les utilisateurs enregistrés pourront joindre le canal.

Nous avons vu qu'il est possible de protéger l'accès à un canal par un mot de passe. Ainsi pour permettre à quelqu'un de rejoindre le canal il faut lui donner le mot de passe. Mais cette méthode peut être dangereuse, le mot de passe peut rapidement tomber entre de mauvaises mains. Les ops et chanroot ont alors une autre méthode pour permettre à un utilisateur lambda d'accéder au canal : ils peuvent l'inviter. La commande INVITE USER et DEINVITE USER permet la gestion des invités. Ces fonctions marchent comme les OP/DEOP, grâce à une liste. Lorsqu'un utilisateur essaye de joindre un canal qui est protégé par un mot de passe on regarde s'il est invité. Si oui, on le supprime de la liste d'invité et il peut rejoindre le canal. Ainsi une invitation n'est valable qu'une fois. De plus la liste d'invité n'est pas sauvegardée. Ces deux points peuvent sembler fastidieux mais permettent aux ops d'avoir un maximum de contrôle des allées et venues dans leur canal.

L'administration.

Pour l'administration du serveur, nous nous sommes beaucoup inspirés d'Unix, référence en la matière. Le serveur peut être administré à distance. Cela rend l'administration très puissante (à la manière du Shell Unix). Comme nous l'avons dit plus haut, YooGoo possède une hiérarchie. Tout en haut de cette hiérarchie se trouve l'administrateur ou encore Root. A l'image des systèmes UN*X, on considère Root comme un homme de confiance absolue. Comme les connections sont sécurisés, il n'y a aucun problème.

Tout comme pour les ChanRoot, l'administrateur possède les commandes ROOT PSEUDO et DEROOT PSEUDO pour ajouter et supprimer des roots.

Privilèges.

Le premier privilège du Root est de ne jamais recevoir de message "Err_Privileges". Dans toutes les fonctions qui demandent d'être ChanRoot ou Op, ou toute autre barrière, on teste si l'utilisateur est root. Si c'est le cas, on le laisse faire.

Root possède aussi la commande KILL USER. Elle permet à Root de déconnecter un utilisateur du canal.

La commande KICKALL CANAL permet de vider un canal. Tous les utilisateurs sont " kickés " (vulgairement, jetés à coups de pied dehors) et le canal est détruit si le fanion KEEP n'est pas actif. (Cf. partie "Les canaux") Si le root met un mot de passe sur le canal avant de faire cette manip, il empêche quiconque (puisque personne ne connaît le mot de passe) de se joindre au canal. Cela peut être utile pour des canaux illégaux.

Il peut être intéressant pour le root d'envoyer un message à tous les connectés pour par exemple signaler une maintenance. La commande ToAll $<$Message$>$ le permet. Par exemple :

    root -> ToAll "Attention : Fermeture du serveur dans 5 minutes"
    user1 <- ToAll "Attention : Fermeture du serveur dans 5 minutes"
    user2 <- ToAll "Attention : Fermeture du serveur dans 5 minutes"
    user3 <- ToAll "Attention : Fermeture du serveur dans 5 minutes"
    .....
    root <- Inf_Ok

Enfin, la plus puissante des commandes est peut-être la commande IAM USER COMMAND. Elle permet d'exécuter une commande comme si on était cet utilisateur. Pour effectuer ceci, on appelle le parseur avec la commande et l'utilisateur en question en paramètre.

Le Serveur

Architecture

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.

Les threads du serveur

Pour développer le serveur YooGoo nous devions être capables de gérer un grand nombre d'utilisateurs en même temps. Nous avions alors deux choix possibles. Premièrement le séquentiel: le programme doit être structuré de telle manière qu'il passe d'un utilisateur à un autre sans ralentir l'exécution du programme et en exécutant bien toutes les requêtes envoyées par le client. Deuxièmement les threads, ce sont des chemins d'exécution autonomes au sein d'un processus (ici le programme YooGoo) et qui partagent leur espace d'adressage, leur codage et leurs données globales. Chacun dispose de son propre jeu de registres, de sa propre pile, de ses propres mécanismes d'entrée, y compris d'une file d'attente de messages.

Au départ, nous devions, utiliser une thread par utilisateur, couplé avec des sockets bloquants, puis, une thread pour la gestions des canaux. Chaque nouvelle connection devait lancer un nouveau thread qui devait gérer l'échange de données entre l'utilisateur connecté et le serveur. Le tout devait communiquer en interne par l'intermediaire d'un système de files de messages.

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. Ainsi, nous avons un tread Winsock pour chaque 64-utilisateurs qui s'occupe de répartir les messages dans les bonnes files utilisateurs. Ensuite, chaque tread utilisateur possède un compteur indiquant le nombre de messages dans ses files. Quand ce compteur tombe à zéro, le tread se met en pause. Il est réveillé dès que le compteur est incrémenté. Nous avons aussi 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).

La Base de données

Choix de la Base de données

Pour la base de données, deux possibilités s'offraient à nous. La première était d'utiliser MySQL ou un serveur SQL. L'utilisation de SQL nous aurait permis de gérer les profils utilisateurs ainsi que ceux des canaux en toute sécurité et avec une vitesse optimale (SQL est l'une des bases de données les plus performantes). De plus, l'apprentissage du langage SQL est relativement simple et aurait pu nous être utile pour le développement du site Web, pour notre culture, et nous aurions pu être invités à l'utiliser plus tard dans notre vie. Le deuxième possibilité était de coder nous même notre base de données avec tous les algorithmes de recherche que nous avons vus en Sup et en Spé. Cette solution permet d'adapter notre base aux besoins de YooGoo.
Nous avons opté pour la deuxième solution pour plusieurs raisons. Etant donné le peu de requêtes de notre projet, utiliser SQL serait un peu éccessif. Cela reviendrait à écrasé une mouche à l'aide d'un marteau-pilon. De plus, coder par soit même un algorithme de recherche est beaucoup plus passionnant que d'en reprendre un déjà tout fait. Cela nous permit par ailleurs d'appliquer notre cours dans un cas pratique, chose qui arrive trop peu souvent, malheureusement. Notre base de données est donc adaptée à nos exigences. Par ailleurs, nous voulions que dans YooGoo toutes les grandes parties soient recréées par nous et dépendent donc le moins possible de programmes extérieurs. L'utilisation de SQL nous aurait contraint à installer un autre serveur (SQL) à côté du notre.

Place de la base de données dans le serveur

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 threads). 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 avons donc souvent besoin de la base de données pour récupèrer le pointeur associé. Elle est 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.

Winsock

Stratégie d'entrées/sorties :

La stratégie d'entrées/sorties est la manière dont les informations reçus et envoyées sont traitées. Un bon choix est déterminant car tout le code réseau repose sur celui ci. Pour rappel les différentes méthodes possibles sont :

Sockets Bloquants
: Par défaut, un socket est bloquant, c'est à dire qu'il ne rend pas la main à l'application tant qu'il n'as pas finis d'effectuer ces tâches ou qu'il y a eu un problème.

Sockets purement non-bloquants
: Un appel avec un socket non-bloquant rend la main immédiatement si il ne réussit pas à terminer sa tâche immédiatement. Ceci permet au programme d'effectuer d'autres tâches pendant que les opérations du réseau finissent. Par contre il faut continuellement tester le socket pour savoir si son opération est finie.

Sockets Asynchrones
: Ce sont des sockets non-bloquants qui renvoient automatiquement un message quand il se passe quelque chose d'intéressant sur le socket.

Select()
: La fonction select() permet à un thread de s'occuper d'un groupe de sockets intéressants, c'est à dire qui reçoivent ou émettent des données. Elle est généralement utilisée pour éviter d'effectuer des test continus sur les sockets non-bloquants.

Objets événement
: utilisés avec WSAEventSelect(), ce mécanisme rassemble à la fonction select(), mais un peu plus efficace. Par contre elle ne fonctionne que sous Windows, tandis que select() fonctionne aussi avec les sockets BSD.

Overlapped I/O
: une des nouvelles fonctions de Winsock 2. C'est la méthode la plus efficace car elle se base directement sur la méthode de Windows pour gérer les entrées/sorties. C'est ce que nous utilisons dabs le serveur YooGoo.

Si on dresse un tableau récapitulatif des différentes méthodes utilisables pour chaque système d'exploitation on obtient :

Win9x WinCE Win NT4+ Win NT 3.x Win16 Unix
Sockets Bloquants oui oui oui oui oui oui
Sockets non-bloquants oui oui oui oui oui oui
Sockets asynchrones oui non oui oui oui non
Objects événement oui non oui non non non
Overlapped I/O oui non oui non non non
Threads oui oui oui oui non oui

Fonctionnement de Winsock

Winsock se présente sous forme d'une dll, il a donc fallu apprendre a insérer une dll dans un projet Visual Studio. En effet, ceci ne se présente pas sous la même forme que sous Delphi ou il suffit d'inclure le .h, il faut utiliser inclure au projet un .lib et le .h correspondant. De plus la librairie Winsock doit être initialisé, ceci se fait par l'appel de la fonction WSAStartup() tandis que la fermeture se faite par WSACleanup() (il est à noter que lors de la fermeture des connections et de la librairie il faut vérifier s'il y a ou nom des messages en attente). Une fois ces opérations réglées on peut accéder à toutes les fonctions externes de la librairies : celles qui sont déclarées dans Winsock.h.

Le choix du non-bloquant

Nou utilisons dans YooGoo 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é.

Il est important de noter que toutes ces fonctions ont une syntaxe très proche de la version sous Unix des socket, ainsi que la création de thread qui est à peu de choses près pareil. De plus les fonctions utilisant Winsock ont toutes été regroupées dans un même fichier afin de faciliter le portage du code sous une autre plateforme.

La mise en oeuvre

Architecture du serveur

Comme indiqué dans la section architecture, nous avons un thread Winsock 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. 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.

Méthode d'envoi et de réception 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 faut reprendre plus tard au bon endroit. Pour cela nous avons dans la classe user 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.

Connexion / deconnexion

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. On attend une connexion.

Connexion

Lors de la connexion d'un client au serveur, il faut échanger la clé DES afin que le client et le serveur puissent par la suite communiquer. Cette fonction est lancée dès que le serveur reçoit une connexion TCP/IP. Si toute la procédure se déroule bien l'utilisateur sera connecté au serveur et pourra alors interagir avec lui. En cas de mauvais fonctionnement il sera déconnecté. La fonction n'effectue pas les mêmes opérations sur le serveur et sur le client, elles dépendent l'une de l'autre. Mais dans les deux cas, la fonction renvoie la clé DES, et NULL si la fonction s'est mal déroulé. Les différentes étapes de la connexion sont :

  1. Le serveur envoie la clé RSA publique en clair au client. Les clés RSA sont générées au lancement du serveur.

  2. Le client reçoit la clé publique. Il génère une clé DES et la crypte avec la clé RSA publique du serveur. Il envoie le message crypté au serveur.

  3. Le serveur reçoit le message crypté du client. Il le décrypte avec sa clé RSA privée. Il obtient alors la clé DES du client en clair.

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 NICK [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.

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 décrypté et se retrouve sous la forme d'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.

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. 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.

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

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é...

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'ait rien à faire, qu'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.

Il faut également savoir qu'il est possible 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.

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().

La classe user

Introduction

La classe User à pour but de gérer tout ce qui touche à l'utilisateur. C'est-à-dire qu'elle contient toutes les fonctions ayant rapport au profil, à la contact list et à la file des messages. Elle est composée de fonctions publiques qui seront appelées par le programme, et de fonctions privées qui seront cachées du reste du programme mais appelées en interne à la classe.

Chaque utilisateur est représenté en mémoire sur le serveur par un objet. On parlera alors d'un objet utilisateur (ou user). Celui-ci regroupe toutes les informations relatives à un utilisateur. Celles-ci peuvent être d'ordre technique comme le socket, le tampon d'envoi (voir la partie winsock), tout comme des informations propres à l'utilisateur comme la ville où il habite, sa date de naissance... L'objet utilisateur contient aussi deux files, une pour les messages qu'il a reçus qui attendent d'être traités, l'autre la liste des canaux auxquels il est connecté.

Pour mettre de l'ordre dans tous ces objets utilisateurs, ils sont classés dans une base de données. Souvent il sera nécessaire de retrouver l'objet utilisateur correspondant à un nom. Il existe donc cinq arbres de classement, en fonction du nom, de la ville, de la date de naissance et enfin une liste des connectés triés par nom. La structure interne est un arbre de type AVL. Ces differents arbres permettent de faire des recherches multicritères.

Type User

Chaque objet de la classe est chargé en mémoire. Nous avons donc alléger au maximum la structure de l'objet user. Or l'élément prenant le plus de place est bien évidemment le profil. Nous avons donc garder que les éléments indispensables à un fonctionnement rapide du programme. Le type User se décompose en trois structures, la structure profil, la structure info et une structure dédiée à la file de messages. La structure profil contient les critères de recherches (nick, sexe, ville, date de naissance) mais également le mot de passe et deux 'pointeurs' vers les fichiers Profil et Contact désignant un numéro de ligne. La structure info contient elle des informations temporaires utiles pour la procédure de la thread, telles que l'adresse IP, le port, la clé DES, (le statut). Enfin la structure file de messages est composée d'un tableau de char et d'un pointeur vers un autre message.

Profil

Il existe donc deux profils. Un profil lite, le plus petit possible, destiné à la mémoire principale et un profil complet destiné à la mémoire secondaire, regroupant toutes les infos disponibles sur un utilisateur. Les informations sont celles qu'on trouve régulièrement sur les chats (ICQ, Caramail, IRC ...). Le profil complet regroupe les champs du profil lite, plus des champs complémentaires tels que le Password, le nom, le prénom, l'adresse, le code postal, la langue, le pays, le numéro de téléphone, l'adresse mail, un " pointeur " vers le numéro de ligne dans le fichier contact, une chaîne contenant le chemin vers une image, et les intérêts.

Fichier Utilisateurs

Le profil d'un utilisateur est conservé dans un fichier (par défaut "user.yoo"). Quand l'utilisateur s'enregistre, on lui attribue une ligne dans le fichier (la dernière). Ainsi à chaque fois qu'on aura besoin d'accéder à ses informations on ira directement à cette ligne du fichier. Ceci est d'autant plus facile que chaque ligne du fichier à la même taille.

Cette ligne contient toutes les informations du profil d'un utilisateur. Chaque champ est séparé par un caractère spécial. Les champs ne doivent donc pas contenir ce caractère. Les différents champs du profil sont (dans l'ordre): pseudo, genre, date de naissance, ville, mot de passe, nom, prénom, adresse, code postal, langue, pays, téléphone n°1, téléphone n°2, e-mail n°1, e-mail n°2, photo, description. Dans le fichier la taille de chaque champ est prédéfinie. Si la valeur fournie ne fait pas la bonne taille, les trous sont comblés par des espaces.

Lors d'un désenregistrement on supprime la ligne de l'utilisateur du fichier. Pour cela, on copie la dernière ligne du fichier, on la met à la place de celle à supprimer, puis on réduit la taille du fichier de la taille d'une ligne. Il ne faut pas oublier de mettre à jour la ligne de l'utilisateur qui a été déplacé.

Au démarrage du serveur, le fichier base de données est parcouru, et on crée en mémoire un objet pour chaque utilisateur enregistré. Ceci permet de pouvoir effectuer des opérations sur les utilisateurs enregistrés même si ceux-ci ne sont pas connectés.

Le profil complet n'existe pas en mémoire. Il faut donc pour les fonctions qui modifient ces valeurs écrire directement dans le fichier. On accède au fichier à la ligne de l'utilisateur, on va jusqu'au champ que l'on veut modifier en comptant le nombre de caractères délimiteurs rencontrés et on modifie le champ (toujours en complétant la taille du champ avec des espaces). Ainsi la base de données est toujours à jour.

Les valeurs du mini profil sont aussi stockées en mémoire car on y accède beaucoup plus souvent. Nous avons vu que les objets utilisateurs sont triés dans des arbres selon leur pseudo, leur ville, leur âge et leur genre. Lors d'une modification d'une de ces valeurs il ne faut pas oublier de mettre à jour les arbres, pour cela on supprime l'ancien utilisateur et on ajoute le nouveau.

Pour gérer la contact list, nous avons aussi une sauvegarde dans un fichier, tout comme le profil. Mais vu qu'on ne connaît pas par avance le nombre de contacts que l'utilisateur va avoir, on ne peut avoir une ligne de taille fixe par utilisateur. Il y a donc un fichier par personne. Ce fichier contient une première ligne avec la première liste, une suite de noms et d'entiers séparés par un caractère spécial. A chaque connexion d'un utilisateur enregistré on charge sa liste en mémoire. Il faut alors faire attention car il peut avoir eu des changements alors qu'il était déconnecté, par exemple quelqu'un l'a supprimé de sa liste, il faut donc mettre à jour la liste des personnes dont on est ami pour cesser de le prévenir lors de changements. Lors de la déconnexion on sauvegarde les listes dans le fichier.

Les Canaux

Gestion des canaux par la base de données.

Chaque canal est symbolisé par un objet que nous verrons en détail un peu plus loin. Nous utilisons la base de données pour gèrer les canaux. Les canaux sont triés dans la base de données selon leur nom. Effectivement, les canaux sont repérés par un nom unique. La base de données nous permet de faire la conversion entre le nom et l'adresse de l'objet de façon très rapide (cf. le fonctionnement interne de la base de données).

Par exemple lorsqu'un utilisateur envoie la commande:

           MsgChan Epita "Bonjour tout le monde"
(commande permettant d'envoyer le message "Bonjour tout le monde" sur le canal "Epita").

La première opération à faire sera de retrouver l'adresse du canal Epita. Ensuite, nous pourrons appellé la fonction associé au canal en question pour traiter le reste de la commande.

Pour la commande GetChans il faut cette fois parcourir l'arbre des canaux pour récupèrer les noms de tous les canaux.

L'objet Canal

Chaque canal est représenté en mémoire sur le serveur par un objet. On parlera alors d'un objet canal. Celui-ci regroupe toutes les informations relatives à un canal.

D'un point de vu de la gestion " interne " au serveur, chaque canal est associe a une instance de l'objet canal. N'importe quelle thread utilisateur peut appellé une fonction d'un objet canal.

Constructeur et destructeur

Pour pouvoir créer et détruire proprement un canal il faut adapter le constructeur et le destructeur à nos besoins.

Constructeur :
canal::canal();

Ce constructeur est le constructeur par défaut. Il existe trois sortes de constructeurs, le constructeur par défaut qui ne prend aucun paramètre, le constructeur de copie qui prend un certain nombre de paramètres servant à initialiser l'objet, et le constructeur par référence qui permet de copier des structures plus complexes impliquant des pointeurs. Notre constructeur initialise tout les champs à 0, vide les chaînes et fait pointer les pointeurs vers NULL. De plus il incrémente la variable statistiques nbcanal, désignant le nombre de canaux créés. Quand un utilisateur join un canal qui n'existe pas, on crée un objet canal vide et on l'ajoute dans la base de données.

Destructeur :
canal:: canal();

Le destructeur a pour but de détruire proprement l'objet. Notre destructeur vide dans un premier temps toutes les listes par l'appel d'une fonction privée parcourant les listes et supprimant les entrées. Enfin on décrémente la variable statistique nbcanal.

Le profile d'un Canal

Pour gérer les utilisateur connectés d'un canal, nous avons utilisé une liste chainée sur pointeur de user. Effectivement, quand un utilisateur est connecté au canal, il existe forcement une instance de l'utilisateur sur laquelle pointer. Cette méthode nous permet d'avoir directement le pointeur sur l'objet user pour envoyer un message par exemple. Lorsqu'un utilisateur joint le canal (JOIN CANAL [PASSWORD], il suffit de l'ajouter dans la liste. De même lorsqu'il part d'un canal (PART CANAL), il suffit de faire une suppression. Lorsque l'on veut envoyer un message a tout un canal, on parcourt cette liste envoyant le message à chaque connecté.

Pour gérer les utilisateurs spéciaux (ChanRoot, Op, Bannis et Invités), nous utilisons des liste chainée sur chaines. Effectivement, ces utilisateurs peuvent ne pas être connectés, donc, nous sommes oblige de pointer sur leurs Pseudo.

Notre type classe est aussi composé de 5 champs

  1. name : char[20] définissant le nom du canal
  2. cat : int définissant le thème ou catégorie du canal (sport, cinéma ...)
  3. pass : char[10] définissant le mot de passe. S'il existe le canal est considéré comme privé
  4. description : char[50] donnant une description sommaire du contenu du canal
  5. droits : long stockant la variable droit qui permet de définir les droits des Ops. Cette variable stocke aussi le fanion pour savoir si le canal doit être gardé après le départ du dernier utilisateur
Il existe des commandes pour modifier ou lire chacun de ces champs. Lorsque l'on change le nom d'un canal, on modifie la variable et surtout, on met à jour la base de données des canaux.

Lorsqu'une fonction de l'objet canal est appellé, elle subit tout d'abord une batterie de test (droits, taille des arguments, existences des utilisateurs, etc...) avant d'être executée.

Les canaux permanent

Lors de la fermeture du serveur la liste des canaux permanents est sauvegardée dans un fichier. Pour chaque canal on sauvegarde aussi toutes les informations qui lui sont relatives : nom, catégorie, mot de passe s'il y en a, les droits et la description. Les différentes listes sont aussi sauvegardées : les ops, les chanroots, et les bannis. Lors du lancement du serveur, le fichier est chargé et on recrée tous les canaux dans la base de données tels qu'ils étaient avant la fermeture du serveur.

La Base de Données

Dans chaque programme qui gère une grande liste d'utilisateurs, la base de données joue un rôle primordial. C'est pourquoi nous avons longtemps réfléchi sur la méthode à employer pour gérer cette base. Notre but est d'en avoir une rapide et occupant peu de place en mémoire principale (moins de 2 Mo). Nous avions le choix entre les arbres, les tableaux statiques, les méthodes de hachages. Nous avons finalement opté pour les arbres. Les tableaux statiques sont incompatibles avec nos besoins. Il nous est impossible de savoir le nombre de personnes qui feront partie de notre base de données. Les méthodes de hachage auraient pu nous convenir : elles sont très rapides pour la recherche, mais elles ne sont pas compatibles avec des recherches par tranches car les données ne sont pas ordonnées selon leur valeur mais selon la valeur de la fonction de hachage. Par ailleurs, le calcul de la fonction de hachage n'est pas simple dans notre cas (pour les mêmes raisons que pour les tableaux statiques la gestion des collisions n'est pas évidente). Les arbres binaires constituent donc une bonne alternative entre simplicité et rapidité. Le principal défaut des arbres binaires est le déséquilibre qui peut engendrer un arbre dégénéré. L'avantage de la représentation en AVL est que la durée de n' importe quelle opération, que ce soit ajout, insertion, suppression ou recherche, et au plus $log(n)$ où n est le nombre d'informations stockées. En effet, cela est dû à la présence de fonctions de rééquilibrage qui opèrent lors d'une insertion ou d'une suppression et maintiennent ainsi un déséquilibre au maximum de 1 en valeur absolue (le déséquilibre d'un arbre est calculé en faisant la différence entre la hauteur du fils droit de la racine et la hauteur de son fils gauche ; la hauteur étant le nombre de noeud que l'on rencontre en parcours descendant jusqu' à atteindre une feuille). Nous utilisons donc l'AVL et toutes les fonctions qui l'accompagnent (calcul du déséquilibre, rotations ...) pour contrer ce problème.

AVL

  • Une structure ordinaire d'arbre binaire avec des pointeurs sur Type Noeud en fils gauche et fils droit :

                typedef struct s_node
                {
                void *data;
                int balance;
                struct s_node *lc;
                struct s_node *rc;
                } t_node;
    

  • Les différentes fonctions disponibles pour l'utilisateur de la librairie (l'utilisateur n'utilisera jamais les fonctions de rééquilibrages) :

    *
    Une fonction qui teste si l'arbre est une feuille (joke).

              int     at_vlt_isleaf(t_node *root);
    

    La fonction retourne si l'arbre est à NULL.

    *
    Une fonction qui insert un nouvel élément.

              int     at_vlt_insert(t_node **root,void *data,int type);
    

    Parcours l'arbre selon les critères de comparaisons définis dans at_avl_comp.c et qui varient selon la valeur de "type" qui permet de désigner le type de donnée à insèrer.

    Les AVL étant des arbres binaires de recherche, on peut utiliser les méthodes pour rechercher, ajouter ou supprimer un élément. D'après la propriété, la recherche dans un AVL contenant n éléments nécessite toujours $\theta(log n)$ comparaisons. Cependant une adjonction ou une suppression dans un AVL peuvent déséquilibrer l'arbre. Ainsi, après avoir ajouté (aux feuilles) ou supprimé un élément dans un AVL, il faut éventuellement le rééquilibrer, tout en conservant la structure d'arbre binaire de recherche.

    Voici le principe de rééquilibrage :
    Soit $T = <r,G,D>$ un AVL; supposons que l'adjonction de l'élément x a lieu sur une feuille de G et qu'elle fait augmenter de 1 la hauteur de G, et que G reste un AVL (donc avant l'adjonction, le déséquilibre de G vaut 0).

    1. Si le déséquilibre de T valait 0 avant l'adjonction, il vaut 1 après; T reste un AVL et sa hauteur a augmenté de 1.

    2. Si le déséquilibre de T valait -1 avant l'adjonction, il vaut 0 après, T reste un AVL et sa hauteur n'est pas modifiée.

    3. Si le déséquilibre de T valait +1 avant l' adjonction, il vaut +2 après : T n' est plus H-équilibré, il faut donc le restructurer en AVL.

    Dans cette troisième hypothèse, il n'y a que deux cas possibles, selon que l'adjonction a lieu dans le sous-arbre gauche ou droit de G. Dans le premier cas, le déséquilibre de G passe de 0 à 1, et on rééquilibre T par une rotation droite. Dans le deuxième cas, le déséquilibre de G passe de 0 à - 1, et on rééquilibre T par une double rotation gauche-droite. Dans les deux cas, l'arbre T obtenu après le rééquilibrage est bien un AVL (arbre binaire de recherche H-équilibré), et il retrouve exactement la hauteur qu'il avait avant l'adjonction de x.

    *
    Une fonction qui supprime tout l'arbre et optionnellement la donnée en mémoire(...).

              void    at_vlt_delall(t_node **root, int deldata);
    

    Parcours l'arbre et libère la mémoire pour chaque feuille (donc tout l'arbre, au final).

    *
    Une autre fonction, qui supprime un élément en fonction du critère de comparaison Type.

              int at_vlt_delete(t_node **root,void *data, int type, int deldata);
    

    Le principe de la suppression d'un élément dans un AVL est le même que pour les arbres binaires de recherche : remplacer l'élément à supprimer par l'élément de l'arbre qui lui est immédiatement inférieur. Mais l'arbre résultant d'une telle suppression peut ne plus être H-équilibré et il faut alors le réorganiser en arbre AVL. Le rééquilibrage après suppression fait intervenir les mêmes techniques que le rééquilibrage après adjonction; mais dans le cas d'une suppression, la réorganisation de l'arbre peut nécessiter plusieurs rotations successives, sur le chemin de la feuille supprimée jusqu'à la racine.

    La réorganisation d'un AVL après une suppression peut entraîner des rotations en cascade sur le chemin allant de la feuille supprimée jusqu'à la racine de l'arbre; en fait les rotations se propagent de bas en haut tant que la hauteur du sous-arbre réorganisé après suppression est diminuée de 1.

    L'implémentation de l'algorithme nécessite donc de mémoriser complètement un chemin de la racine à une feuille, cela peut être fait soit de façon explicite en utilisant une pile dans une version proche de celle de l'adjonction, soit de façon implicite dans une version récursive proche de la spécification formelle (attention : après la suppression d'un élément contenu dans le noeud v, il faut examiner la hauteur h du sous-arbre de racine v; si h a diminué de 1, il faut éventuellement faire une rotation au niveau du père de v).

    Une suppression dans un AVL peut entraîner jusqu'à 1.5 Log2n rotations mais la complexité reste toujours en O(log n). Comme pour l'adjonction, l'analyse en moyenne reste un problème ouvert; les résultats expérimentaux montrent cependant qu'il y a seulement en moyenne une rotation pour cinq suppressions, ce qui va donc à l'encontre du sentiment intuitif qu'une suppression est plus coûteuse qu'une adjonction!

Définition de l'objet Database

Pour " simplifier les choses ", nous avons choisi de représenter l'objet [base de donnée] (tout du moins, celui représenté en mémoire) directement par sa propre classe d'objet et ses propres méthodes qui lui seront appliquées.

La classe Base De Donnée (DB) comprend essentiellement une liste chaînée et un AVL pour chaque critère selon lesquels on peut rechercher un utilisateur.
Nous avons 4 critères principaux (Nickname, genre, date de naissance et ville). Ceci signifie que les opérations d'insertion et de suppression seront de l'ordre de $4*log(n)$.

L'objet DB comporte deux opérations classiques : le constructeur qui initialise la liste et les arbres à NULL et le destructeur qui se charge de libérer la mémoire proprement.
Il comporte également une fonction d'insertion (celle qui stock et insert dans les AVL) et une fonction de recherche qui retourne une liste chaînée d'objets utilisateurs en fonction d'un objet utilisateur passé en paramètre (critères plus ou moins vides selon ce que l'on recherche : c'est la fonction qui gère).

Bien entendu, dans le but d'être propre, les utilisateurs ne sont représentés qu'une seule fois en mémoire et les arbres s'entrecroisent sur les pointeurs "data" stockés 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.

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.

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"

Administration du serveur

Gestion des roots.

YooGoo permet de gérer plusieurs Root sur un serveur. Nous utilisons, pour gérer les root, la même liste chaînée que pour la gestion des ChanRoot, Op, Invités et Bannis. Les seules différences sont que cette liste est globale au serveur et quelle est sauvegardée dans un fichier. Pour la structure du fichier, nous utilisons une ligne par root. Pour l'ajout d'un root dans le fichier, on écrit une ligne à la fin. Pour la suppression, nous avons décidé de réécrire tout le fichier plutôt que de faire un algorithme compliqué. Le fichier étant court et la commande rarement appelée, ça ne pose pas de problème de charge d'accès au disque. Le fichier est ensuite lu à chaque démarrage.

La Fonction iam

Comme décrit dans le protocole, la fonction iam permet a un root de prendre l'identité d'un utilisaeurs. Nous avons des problèmes pour renvoyer les retours de messages au root. Effectivement, une fois l'appel du parseur fait, il n'y a plus moyen de savoir si c'est une commande détournée ou non. Les messages de retours sont donc envoyés à l'utilisateur. Nous avions au début pensé à changer le pointeur du tampon d'envoi de l'utilisateur de façon à ce qu'il pointe sur le tampon d'envoi du root. Cela aurait sûrement fonctionné avec un serveur "Normal", mais Yoogoo est un serveur sécurisé. Il faut donc aussi prendre en compte les clés DES, et ce, avant d'envoyer le message à root. Bref cette solution aurait sûrement fonctionné, mais, elle était trop compliquée à mettre en oeuvre. Nous avons donc ajouté une variable de retour de type user * dans l'objet user. Dans la fonction d'envoi, si cette variable et non NULL, on rappelle la fonction d'envoi avec pour destinataire l'utilisateur pointé par cette variable et le tour est joué.

Les 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 MSG 0x0020
:
Message d'un utilisateur à un canal ou à un autre utilisateur (MsgUser et MsgChan).

#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 USER 0x0200
:
Active le log des commandes utilisateurs (Who, Asv, etc...).

#define CANAL 0x0C00
:
Active le log des commandes d'administration des canaux (Kick, Ban, chChanTopic, etc...).

#define ADMIN 0x2000
:
Active le log commandes d'administration du Serveur (Kill, Halt, etc...)

#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.

Le fichier de configuration.

Au démarrage du serveur, on essaye de lire le fichier de configuration ygd.conf. Si il existe, il permet de faire passer certaine option au serveur. Pour lire ce fichier, on le lit ligne par ligne. A chaque ligne, on recopie la ligne dans une autre string en éliminant les caractères blancs (espaces, tabulations, etc...) et en s'arrêtant si on trouve le caractère ';' (signifiant un commentaire). Ensuite, on recherche le caractère '$=$' qui sépare le mot-clé de l'argument. On compare le mot-clé avec la liste des mots-clés reconnu. Enfin, on exécute la commande demandée. Le fichier de configuration par défaut se présente ainsi :

;\\
; ygd.conf Fichier de configuration pour YooGoo daemon\\
;\\

;logDb          ; Log les modifications dans la Base de Données\\
;logMsg         ; Log les messages des utilisateurs (cette option\\
                ;       est une atteinte à la vie privée)\\
;logUser        ; Log les événements utilisateurs\\
logChan         ; Log les événements administratifs sur les canaux\\
logAdmin        ; Log les fonctions d'administration du serveur\\
;logDebug       ; Log diverses informations de Debug\\
logErr          ; Log les erreurs fatales du serveur (très
conseille)\\
logConnections  ; Log les connections et les déconnections\\

Client Graphique

Structure du client

Introduction

Le client tout comme le serveur repose sur une structure, les éléments et objets sont disposés selon une certaine logique. Comme pour le serveur nous avons réfléchi avant d'agir afin d'éviter les retours en arrière et de délimiter ce qui est envisageable et ce qui ne l'est pas, au niveau du temps principalement.

Donc dans la partie "code" du client on distingue plusieurs objets majeurs. Nous avons tout d'abord le parseur, ou plutôt les parseurs car ils sont au nombre de deux. Leur but est d'analyser des messages et d'appeler la fonction nécessaire. Ils jouent le rôle d'aiguilleurs. Le deuxième gros objet est l'ASV. Il est en quelque sorte une base de données temporaire qui est constamment modifiée. Elle contient toutes les personnes qui ont un lien direct avec l'utilisateur (personnes connectées sur le même canal, personnes en session privée ...). L'objet canal fait également partie de cette structure. Cet objet est fondamental car il permet la gestion des canaux au niveau du client. Il s'occupe de gérer tous les messages liés aux canaux provenant du serveur. Le dernier objet est l'objet onglet. Ce dernier s'occupe de la gestion des onglets de la fenêtre principale.

Enfin pour communiquer en crypté avec le serveur, nous allons utiliser une DLL de communication qui servira de standard a quiconque désirera développer son propre client.

Parseurs

Ils sont donc au nombre de deux. Le premier le parseur de texte s'occupe du traitement des messages saisis par l'utilisateur dans la boite de saisie ainsi que des messages en provenance du réseau et devant être affichés à l'écran. Le deuxième parseur s'occupe de tous les messages en provenance du réseau. Il doit appeler les bonnes fonctions qui sont en rapport avec le message reçu.

Parseur de texte

Au sein du parseur de texte on peut également compter deux autres parseurs. Le premier vise à convertir un message provenant du serveur et devant être affiché dans une boite de texte. Et le deuxième vise à convertir le texte saisie en forme en texte suivant un certain protocole pour pouvoir circuler sur le réseau.

Premier parseur:

Dans le cadre du client, nous nous sommes mis d'accord pour permettre aux utilisateurs de définir les caractéristiques de leur texte. Ils auront ainsi le choix entre différentes polices, le choix de la graisse des caractères de même que la couleur.

L'objectif est de permettre une plus grande liberté aux utilisateurs ainsi qu'un attrait supplémentaire pour les Clients YooGoo. Il est également possible d'insérer des images (style smileys).

Exemple de résultat désiré :
danao (01:56)$>$ Salut, ça va?

Bien entendu, il n'est pas possible de transférer des images à travers net à cause de la taille des messages limitée. De plus il est impossible de transférer des couleurs a travers le réseau (techniquement, on ne peut transférer que des bits). Pour cela, on a défini un protocole qui code dans le message les différentes caractéristiques du texte, ainsi que des insertions d'images.

Le principe est simple, tout texte qui suit un code particulier suit les caractéristiques définies par celui-ci. Pour caractériser la couleur, le code commence par un caractère dièse (#) et il est suivi d'une chaîne de 6 octets définissant la couleur en hexadécimal selon le principe RGB (Red Green Blue). En effet, une couleur est composée de ces trois couleurs de bases et leur dosage varie de 0 a 255.

Voici un exemple de codage que l'utilisateur peut rentrer : #ff0000[Coucou !
Cela aura pour résultat : danao (02:00)$>$Coucou !
Ou bien encore : #ff0000[Cou#0000ff[cou #00ff00[!
Donne : danao (02:01)$>$Coucou !

Pour caractériser les différentes graisses et polices, il faut utiliser un code plus complexe. Le code est préfixé du caractère spécial % et est suivi d'un chiffre qui désigne la police. Ce client offre un choix de 10 polices (ce qui semble suffisant, l'objectif est la communication et non pas un traitement de texte). Parmi ces polices on note : Arial, Comic sans MS, Times New Roman ...

Après le chiffre qui désigne la police se trouve un nombre de deux chiffres qui désignent la taille de la police. Cependant, en une seule commande, ce serait dommage de sans arrêt rappeler la taille que l'on veut conserver ou bien la police lorsque l'on veut juste changer un des deux paramètres. Pour cela, il suffit de mettre dans la commande le caractère * (joker) qui permet a la fonction de parsage de conserver les attributs précédents du texte.

Pour mettre en gras, il faut utiliser les balises "" pour activer la fonction et "$\backslash$b0" pour la désactiver. Il en va de même pour l'italique avec "$\backslash$i" et "$\backslash$i0". Pour le soulignement, il faut utiliser les balises "$\backslash$ul" et "$\backslash$ulnone". Attention, ces balises doivent être suivies d'un espace.

Insertion des images

Pour l'insertion des images nous avions deux solutions. La première était l'utilisation des objets OLE, et la deuxième était le format RTF.

L'utilisation des Objets OLE

Les grands défauts de cette technique sont qu'elle nécessite beaucoup de ressources et qu'elle est relativement lente : en effet, pour chaque insertion d'image, si petite soit-elle, cela revient à lancer une application qui supporte le format de fichier à insérer. Ici, on utilisait les objets Paint.Picture : pour chaque image à insérer, on appelait une instance de l'application Paint de Microsoft (qui se trouve normalement insérée par défaut sur tous les Windows à partir du 95).

Quand on regarde les logiciels de chat les plus utilisés, on peut remarquer les habitudes des utilisateurs à insérer de nombreux smileys dans leur texte, que ce soit pour montrer leur humeur. On ne pouvait donc se permettre d'employer une technique si gourmande en ressources et qui serait appelée très souvent par les utilisateurs. Le but du "chat" en informatique est la communication rapide, et si possible, aussi rapide qu'à l'oral. (Les colorisations, les graisses et les polices servent à simuler les intonations, voire faciliter les techniques implicites).

Cette méthode était également valable pour insérer du texte formaté (coloré avec police ...). Le problème rencontré de ce côté là, était principalement d'ordre logique : est-il vraiment normal de créer une instance OLE pour insérer du texte formaté (ici, on utilisait des instances WordPad) dans un objet qui le supporte déjà en natif. Il est bien évident que la réponse est non. Nous allons voir dans la partie suivante, quelle a été la première solution trouvée à ce problème (qui ne résolvait en rien le problème des insertions d'image).

Format RTF

Au final, nous avons opté pour une méthode plus radicale et qui est mieux adaptée à l'objet avec lequel nous traitons : le format RTF.

Il s'agit d'un standard de Microsoft qui permet de traiter du texte formaté et qui est traité en natif par l'objet RichTextBox.

Le format RTF repose sur un système de balise comme pour l'HTML. Le cartouche du document contient toutes les fontes, couleurs, [...], contenus dans le corps du document. Du point de vue du programme le composant RichTextBox nous permet par le biais de propriétés adaptées de réaliser exactement ce que nous désirions.

Notre client étant limité à 10 polices le cartouche ne change donc pas du point de vue des fontes. C'est pourquoi il existe une fonction du parseur de texte qui crée une cartouche au moment de l'initialisation.

De plus le RTF nous a grandement aidé pour l'insertion des images. Pour insérer notre image il nous suffit d'écrire dans la boite de texte, le texte au format RTF correspondant à l'image que nous souhaitons ajouter (en RTF, toutes les données sont en caractères).

Tous ces avantages ont fait du RTF notre choix pour ce client. Enfin dans le futur nous pourrons encore plus l'exploiter : gestion des indices, exposants ...

Deuxième parseur

Pour réaliser un parseur à partir du cadre de saisie il faut comprendre le principe de fonctionnement du RichTextBox et le format d'arrivée YooGoo. L'implémentation de la barre des polices nous a grandement aidé dans la compréhension du RichTextBox.

Le principe de l'algorithme de ce parseur est de sélectionner les caractères un par un dans le cadre et de vérifier les propriétés relatives aux polices. On insère des balises au format YooGoo lorsqu'on note un changement d'une de ces propriétés.

Pour pouvoir apprécier les changements il faut passer par des variables temporaires de différents types. Par exemple en type booléen nous avons des variables du type currentbold qui définissent si le caractère précédent est en gras.

Il faut cependant gérer le cas particulier du premier caractère car pour le premier caractère quoiqu'il arrive il est nécessaire d'entrer au moins une balise, celle de définition de la police (nom de la police, plus taille de la police). La boucle commence donc au deuxième caractère. Pour sélectionner uniquement un caractère et incrémenter le pointeur de sélection il faut initialiser SelLength à 1 et SelStart à la variable compteur i.

Après avoir parser le message de cette manière il ne reste plus qu'à l'envoyer sur le réseau. Et de le traiter avec le parseur vu précédemment.

Parseur de messages

ASV

L'ASV est une classe qui a des allures de base de données. Elle regroupe toutes les personnes étant liées à l'utilisateur. Elle contient toutes les personnes de tous les canaux sur lesquels l'utilisateur est connecté, toutes les personnes en session privée, ainsi que lui-même. Les informations enregistrées sont uniquement l'ASV de la personne. L'utilité de cette base de donnée est très importante. Elle ne saute pas aux yeux et pourtant elle est primordiale. En effet, lorsque l'on veut implémenter une Canal List par exemple il est très utile d'avoir directement sur la machine les informations de toutes de ce canal sans avoir à repasser par le serveur.

Etant donné le peu de personnes qui y sont stockées, l'ASV a une structure de liste simplement chaînée. Une seule chose distingue l'ASV d'une liste classique, c'est le compteur d'occurrence. En effet, lorsqu'une personne est présente plus d'une fois dans la liste ce qui est presque toujours le cas de l'utilisateur, il est beaucoup plus simple d'incrémenter ce compteur, plutôt que d'ajouter des doublons qui seront plus difficiles à enlever et surtout à modifier.

La structure de l'ASV est donc la suivante :

            typedef struct TList
            {
                TList * next; // élément suivant
                Tasv * asv;   // Pointeur sur la structure ASV
                int occurrence;   // nombre de fois où cette personne apparaît dans le client
            }TList;

Les fonctions disponibles avec cette classe sont les suivantes :

  • AddOcc : cherche juste incrémenter la valeur de occurrence si la personne existe dans l'ASV.
  • Add : ajoute tout un ASV en mémoire. Il faut fournir les informations en paramètre.
  • Mod : permet de modifier les informations d'une personne.
  • Del : supprime une occurrence de la personne ou l'efface si l'occurrence était à zéro.
  • GetSex : permet simplement d'obtenir le sexe de la personne donnée en paramètre.

L'unique objet de cette classe est global au projet pour pouvoir être utilisé n'importe où.

Canaux

Pour pouvoir utiliser les canaux sur le client, il faut une compréhension totale des messages en provenance du serveur. On pourrait très bien parler sur un canal en mode texte, mais la réception des messages bien qu'explicites ne permet pas de tout comprendre étant donné le nombre de message reçus sur une courte durée.

L'implémentation des canaux sur le client est donc grandement liée à l'interface graphique. La partie code du canal a donc besoin d'un onglet avec une boite pour afficher les messages de sortie, ainsi qu'une Canal List qui doit afficher les personnes présentes sur le canal (elle sera étudiée plus tard).

Cette partie code est représentée dans une classe, la classe canal qui n'a rien à voir avec celle du serveur. Contrairement à l'ASV, il existe une multitude d'instances de la classe. Tous les canaux sont gérés par la classe par le biais de fonctions statiques qui permettent un appel n'importe où dans le programme. Tous les canaux créés sont stockés dans une liste chaînée. C'est la fonction create qui initialise toute la liste des connectés et qui s'occupe d'enchaîner le canal.

D'un point de vue de la structure, la classe canal est donc composée d'une liste statique qui regroupe tous les canaux et d'une liste liée au canal regroupant toutes les personnes ayant rapport au canal (connectés, bannis, ...). Cette dernière contient l'asv de la personne ainsi que son statut sur le canal. C'est ce statut qui définit si la personne est bannie, opérateur ... Pour ce qui est des fonctions de la classe elles sont relativement nombreuses et sont, pour une grande partie, statiques.

Création d'un canal au niveau du client

Au moment de la création il faut créer une instance de la classe canal - new canal - et appeler la fonction create. Le canal sera alors lié à la liste, et la fonction envoie un message au serveur afin d'obtenir la liste des connectés. La fonction crée également un nouvel onglet et rafraîchit la Canal List.

Arrivée d'une personne sur le canal

L'arrivée d'une personne est signalée par l'événement JOIN + nom du canal + nom de la personne + info... Pour ajouter cette personne au canal, il faut alors appeler la fonction join avec le nom du canal, de la personne ainsi que ses informations. Cette fonction est statique, c'est pourquoi il est indispensable de spécifier le nom du canal. On ajoute alors cette personne dans l'asv et on rafraîchit la Canal List.

Départ d'une personne

Il faut appeler la fonction part. Cette fonction fait exactement le contraire de la fonction join. Et si la personne en question est l'utilisateur alors on appelle la fonction kick. C'est comme si on s'éjectait tout seul. Enfin on remet la Canal List à jour.

Ejection d'une personne

Représentée par l'événement KICK + nom du canal + nom de la personne + nom de l'opérateur, elle intervient lorsqu'un opérateur décide que cette personne n'a plus rien à faire sur ce canal. On appelle donc la fonction part car sur le principe c'est comme si la personne décidait de partir de son plein gré. Si il s'agit de l'utilisateur on détruit l'onglet du canal ainsi que le canal, on décrémente sa valeur dans l'ASV, et on efface la Canal List.

Changement du nom du canal

Un chanroot peut à tout moment changer le monde de son canal, sous réserve de disponibilité du nouveau nom. Un message d'avertissement est envoyé par le serveur à toutes les personnes connectées au canal. Chaque client concerné modifie le nom de l'onglet et rafraîchit sa Canal List.

Modification d'information d'un connecté

Si une personne connectée au canal modifie une de ses informations, le serveur informe tous les clients en fonction des droits de chacun (les personnes bannis par cette utilisateur ne recevront rien). Le client modifie donc l'information dans l'ASV par le biais de la fonction de modification.

Informations sur le canal

Afin d'obtenir des informations sur un canal l'utilisateur doit saisir la commande what plus le nom du canal désiré ou cliquer sur le canal puis sur information. Le client envoie alors une requête au serveur dans le but de recevoir les informations. Une fois toutes les informations récoltées on crée une nouvelle fenêtre de type what détaillée plus tard qui affichera toutes ces informations.

Onglets

A la base YooGoo se veut personnalisable, simple d'utilisation mais monobloc à la différence d'ICQ. La manière la plus simple de réaliser cela à été d'utiliser des onglets pour chaque session ouverte : canaux, sessions privées, peer to peer.

Cependant l'utilisation d'onglets nécessite une structure au niveau du code. On peut se demander pourquoi. Et pourtant la réponse est évidente. Premièrement, il faut pouvoir retrouver rapidement un onglet lorsqu'on veut ajouter un message en provenance du réseau. Deuxièmement, à chaque onglet on associe aussi une boite de texte contenant tous les messages. Il faut donc une structure pour stocker ce pointeur. Et enfin, les onglets peuvent représenter plusieurs types différents (canaux, sessions privées ...).

A l'inverse de l'ASV et des canaux, les onglets sont gérés par une structure dans une unit et non une classe. Cette structure est encore une liste enchaînée, mais elle contient également le type de l'onglet, représenté par un entier, un pointeur vers la page de l'onglet, un pointeur vers la boite de texte, et un booléen qui désigne si l'onglet est "allumé".

Les onglets sont donc différenciables au niveau du programme mais pour qu'ils soient différenciables par l'utilisateur, les textes des onglets sont colorés et respect cette norme : rouge pour un canal, bleu pour une session privé et bleu foncé pour une connexion peer to peer. La structure onglet étant la seule à posséder le pointeur de la boite de texte, c'est l'unit onglet qui contient la fonction d'ajout dans cette boite.

Le client doit créer un onglet

Un onglet doit être créé lorsque l'utilisateur ouvre une session. Le client appelle donc la fonction d'ajout d'un onglet. Celle-ci créé un onglet et la page qui est associé puis l'ajoute dans la liste des onglets. Enfin elle affiche l'onglet.

Le client reçoit des messages textes

Le parseur appelle la fonction d'ajout de l'unit onglet qui met à jour la boite de texte de l'onglet correspondant. Avant cela la fonction modifie l'entête du message en mettant la bonne couleur pour la personne ayant envoyée le message, c'est-à-dire en fonction de son sexe.

Le client doit envoyer un message

Lorsque l'utilisateur envoie un message, le client cherche la valeur du type de l'onglet sélectionné et envoie un message sur le réseau. L'entête de ce message dépend du type de l'onglet. Ceci est réalisé par la fonction d'envoi de l'unit onglet.

L'onglet doit être redessiné

Afin de colorer les textes des onglets, il faut gérer soi-même l'affichage de ce dernier. L'onglet peut donc être amené à être redessiné. Ceci ce produit lorsqu'on sélectionne un onglet on lorsqu'on le demande. Ce rafraîchissement génère un événement qui appelle la fonction de coloration de l'unit onglet. Celle-ci colorie l'onglet on fonction de son type mais teste également si l'onglet est "allumé" dans quel cas elle dessine une forme significative.

Le client doit détruire un onglet

Un onglet doit être détruit à chaque fermeture de section. Pour cela le client appelle la fonction de fermeture qui efface l'onglet en détruisant tous les composants qu'il contenait et en le supprimant de la liste.

DLL de communication

Afin de communiquer avec le serveur il fallait quelque chose dans la structure du client pour pouvoir et crypter/décrypter les messages et les envoyer. Le cryptage est indispensable pour répondre à la norme du protocole YooGoo.

Cette structure assez complexe a été placée dans une DLL. Ce choix a plusieurs avantages. D'une part le code du cryptage étant composé d'une multitude de fichiers et étant assez complexe, le fait de le mettre à l'écart du programme principal rend le client plus léger. Mais le principal avantage est de pouvoir distribuer cette DLL, ce qui permettrait à d'autres utilisateurs de développer assez facilement leur propre client. Le dernier avantage est que si des modifications au niveau de cette DLL étaient nécessaires il suffirait juste de télécharger ce fichier sans rien installer.

Les fonctions publiques de la DLL ne concernent pas la cryptographie, seulement le Winsock. Elle ne s'occupe que de la communication entre le client et le serveur.

Pour connecter le client au serveur

La première étape du client est de se connecter sur le serveur. La phase de connexion est intégralement gérée par la fonction connexion de la DLL. Il suffit juste de passer les paramètres indispensables, tels que le pseudo, le mot de passe, l'IP du serveur, le port ...
Cette fonction va initialiser Winsock, créer la clé DES, procéder à l'échange de la clé en passant par l'algorithme RSA. Elle retourne un entier qui désigne le résultat de la connexion.

Pour envoyer un message au serveur

Une fois connecté, le client envoie pratiquement tout le temps des messages. Pour cela il appelle la fonction Send de la DLL en passant juste la chaîne de caractères en paramètre.

Fonction getmsg

La fonction getmsg lit la file de message en attente. Les messages sont mis dans la file dans la fonction communicate, lors de la mise à jour des buffers.

Fonction communicate

La fonction communicate construit et/ou vérifie si le socket connecté au serveur est encore capable de recevoir des données (c'est a dire que son "buffer" n'est pas plein) et s'il est en train d'envoyer des données au serveur (c'est a dire que son buffer d'envoie n'est pas vide). Puis elle regarde si le socket est actif, c'est-à-dire s'il se passe quelque chose sur le réseau (envoie, réception ou les deux). Si c'est le cas elle met à jour les buffers.

La mise à jour consiste à l'envoie ou la réception des données. Si le message est entier elle le traite c'est-à-dire qu'elle le décrypte puis elle l'enfile.

Déconnexion du client

Une fois l'utilisation du client terminée, il faut se déconnecter. Pour cela, il faut appeler la fonction déconnexion de la DLL, qui termine le thread en cours, et arrête Winsock.

Thread

Peer to peer

Une connexion Peer-to-peer est une connexion TCP - IP entre deux clients dont chacun d'eux a ouvert une thread d'écoute sur un port particulier. Ce type de connexion n'a aucun lien avec le protocole YooGoo et, comme vous l'avez compris, n'à aucune relation avec aucun serveur que ce soit.

La connexion Peer-to-peer est utilisée dans le client YooGoo pour envoyer et recevoir des messages privés et ainsi décharger le serveur de dialogues en tête-à-tête.

Pour établir une connexion Peer-to-peer, il faut créer un objet "t_p2p_server". La librairie Peer-to-peer codée pour le client a été codée en C++ et comprend de l'objet pour simplifier son utilisation dans le client. A la création de l'objet (new *), on définit un numéro de port ainsi qu'une fonction de traitement des messages. Le constructeur de l'objet lance ensuite un " thread " qui écoutera les messages parvenant au port dédié et les traitera avec la fonction dédiée au traitement des messages.

Pour envoyer un message en Peer-to-peer, on utilise une autre fonction dédiée à l'envoi de message simple. Elle se charge de se connecter à un " thread " Peer-to-peer, de lui communiquer le message et ensuite de se déconnecter. (Il est donc nécessaire que l'interlocuteur ait un telle " thread " de lancée).

Le lien avec le serveur YooGoo se fait grâce à la fonction "Query" (Query <Pseudo> <Port>) du protocole. Lorsqu'un utilisateur veut se dialoguer avec un autre, il lui envoie un "Query" avec un numéro de port. Il lance alors une "thread" peer-to-peer et attends les messages de l'autre. Celui ci est alors libre d'envoyer (ou pas) un message (et donc de rendre visible son ip) au premier.

Il existait une autre méthode d'implémentation qui impliquait que l'un des deux clients soit désigné comme serveur et que l'autre se connecte en tant que simple client, avec une connexion permanente (alors qu'avec la version présente, on se connecte à l'interlocuteur désiré, on lui transmet le message puis on se déconnecte). Cette dernière version n'a pas été retenue d'une part pour sa complexité d'implémentation, d'autre part parce qu'il fallait décider lequel des deux clients serait serveur, et ensuite qu'il nous semblait inutile de conserver des connexions permanentes pour quelques messages à petit débit et ainsi monopoliser un port des sockets.

De plus le peer-to-peer n'étant aucunement lié avec le processus de communication vers le serveur (et notamment la dll de communication) il est possible d'ajouter des fonctionnalités très facilement (tel que le transfert de fichier ou la vidéo conférence), à l'image des logiciels de messagerie instantanée (icq, aol instant messager, msn messager..). Mais cela ne rentre plus dans le cadre du protocole YooGoo.

Classe User

Dans le serveur YooGoo il existe une classe user, qui permet de créer des instances utilisateurs (autant qu'il y en a dans la base de données). Cette structure regroupe toutes les informations "légères" de la personne, ainsi que des infos complémentaire.

Pour ce qui est du client, c'est à peu près la même chose, à l'exception du fait qu'il n'y a qu'un seul utilisateur. Pour réaliser cette classe nous nous sommes donc inspirer de la classe du serveur en modifiant quelques champs. Par exemple, maintenant les informations de bases sont stockées dans une structure asv pour être compatible avec la base de données ASV. Une structure p2p à été rajoutée pour savoir le port à utiliser, et savoir si l'utilisateur les accepte. Une structure interface a également été rajoutée pour pouvoir gérer les fenêtres principales du client : Canal List, Contact List ... Les informations de connexion n'ont quant à elles pas changées.

Cette classe regroupe toutes les informations qu'il est possible de saisir dans la fenêtre options. Elle est créé au lancement du client et est détruite juste avant la fermeture du client.

Interface graphique

Introduction

Du côté utilisateur le client YooGoo doit être agréable à regarder. Il doit être simple d'utilisation, c'est-à-dire qui doit être intuitif. L'utilisateur ne devrait donc pas avoir besoin d'aide pour s'en sortir. Cela dit l'utilisateur se verra proposer un fichier d'aide complet qui doit être interactif avec son problème. C'est-à-dire que s'il appuie sur F1 en étant sur la Canal List, l'aide s'ouvrira directement sur la bonne page.

YooGoo doit également être personnalisable pour se fondre totalement dans l'idée de l'utilisateur. Il aura donc une Contact List pour garder tous ses liens proches, mais également une Canal List pour voir toutes les personnes de ses canaux préférés. Toutes les fenêtres seront détaillées par la suite.

Choix de Borland

On peut se demander pourquoi avoir choisit Borland pour développer le client et non pas Visual C++ comme tout le monde le fait.

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 semblent mieux fonctionner, de même que pour la modification de propriétés à l'intérieur du code. Un travail graphique qui a pris 2 jours avec Visual a été réalisé en 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 avec un Borland bien configuré on peut pallier ce problème.

Au niveau des inconvénients, 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 debuging 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...

Borland a donc été choisi pour sa simplicité en terme graphique, à un Visual certes très puissant mais avec un temps d'adaptation aux MFC trop long.

Barre des polices

Le client intègre également une barre des polices. Son but est d'être facile d'utilisation et d'être performante. Pour cela nous avons copié le modèle de Word, référence en la matière. Pour sélectionner le nom de la police nous avons pensé à une liste déroulante qui ne sera pas éditable, c'est-à-dire que l'utilisateur aura la choix entre les polices proposées. Ces polices sont au nombre de 10 pour l'instant. Ce nombre fini vient du fait que tout le monde ne possède pas les mêmes polices. Des tests faits sur ICQ ont prouvés que ce dernier réagi très mal dans ces cas là.

Pour ce qui est de la taille de la police, nous avons également pensé à une liste déroulante mais qui peut être éditable. Cependant il faut vérifier que le champ édité par l'utilisateur est valide. Pour cela nous vérifions cette valeur à la sortie de cette liste qui déclenche l'événement OnExit. Cette valeur doit être comprise entre 4 et 22 sinon l'ancienne valeur sera reprise.

L'utilisateur doit pouvoir mettre de la couleur dans ses textes. Pour cela nous avons ajouté un bouton dans la barre des polices. De plus nous avons ajouté un contrôle qui correspond à une boîte de dialogue de couleur comme celle de Windows. Le fait de cliquer sur cette police provoque son exécution par l'appel à la fonction BOITEDCOULEUR->EXECUTE();

Une fois cette instruction terminée (quand l'utilisateur à cliquer sur OK) nous récupérons la couleur saisie par l'appel à la fonction BOITEDCOULEUR->COLOR;

Ensuite il faut faire passer la couleur au cadre d'édition. Nous passons par la propriété SelColor qui permet de donner notre couleur au texte sélectionné. Si aucun texte n'est sélectionné il s'agira du dernier caractère de la boîte et des futurs caractères qui seront entrés. Pour embellir l'aspect visuel du client, nous avons pensé à donner de la couleur au bouton. Cette couleur est bien évidemment la couleur choisie dans la boite de dialogue couleur. Pour pouvoir mettre de la couleur, nous passons par le "canvas" du bouton. Il faut alors définir la taille de cette zone, hauteur et largeur, définir la couleur que nous voulons donner en passant par Brush->Color, et enfin il faut placer notre surface sur le bouton, la positionner.

Une remarque qui aura son importance par la suite est que le format des couleurs n'est pas un RGB (Red Green Blue) mais un BGR.

La barre des polices est également composée de boutons Gras, Italique, et Souligné comme pour la barre des polices de Word. La gestion du "GIS" est à peu près la même que celle des couleurs à la différences près que les propriétés à modifier sont : SelBold - SelItalic - SelUnderline. Ces propriétés sont des booléens ce qui permet de gérer facilement les boutons. En effet quand on désire mettre un texte en gras le bouton gras reste enfoncé pour faire comprendre que les prochains caractères saisis seront au format gras.

Les deux derniers boutons de la barre des polices sont des boutons personnalisables. Le premier est programmé initialement pour faire apparaître la fenêtre des smileys. Le deuxième n'est pas programmé par défaut. Ces boutons peuvent être très utiles pour gérer plus facilement le texte envoyé.

Enfin la barre des polices est interactive. C'est-à-dire que lorsque l'utilisateur déplace le curseur dans le cadre de saisie, la barre des polices se met à jour automatiquement. Par exemple si vous positionnez le curseur sur un texte gras, le bouton gras va s'enfoncer jusqu'à ce que le curseur se remette sur un texte non gras. Pour arriver à ce résultat il nous a fallu passer par l'événement ONSELCHANGE qui est appelé lorsque le curseur est déplacé dans le cadre de saisie.

Canal List

Pour pouvoir utiliser la structure canal du client. Il faut pouvoir afficher les sorties de cette structure à l'écran. C'est-à-dire qu'il faut gérer l'onglet correspondant et mais également la Canal List.

Le but de cette dernière est de permettre à l'utilisateur de voir les personnes connectées au canal. De plus l'utilisateur possède des informations supplémentaires pour chaque personne, comme son sexe, son statut sur YooGoo (online, offline, away, ...), ou son statut sur le canal (chanroot, opérateur, ...). En plus de l'affichage, la Canal List permet d'exécuter diverses actions sur les personnes du canal ou tout simplement sur le canal.

Ces actions apparaissent dans un popup lorsque l'utilisateur clique droit sur une autre personne. Il peut alors avoir accès au profil, à l'ajout dans sa CL, à une session privée avec cette personne, à le bannir (ne plus recevoir de message d'elle). Si l'utilisateur est chanroot ou opérateur de ce canal, il possède plus de droit. Il y a donc beaucoup plus d'options dans le menu popup. L'opérateur peut alors kicker une personne, en inviter une autre, nommer une personne ... tout cela en fonction de ses droits.

Au niveau du canal, un utilisateur standard ne peut que voir les informations de canal. Pour les opérateurs et chanroot, il existe multiples actions pour gérer le canal : renommer, changer le sujet ...

Enfin dans un canal, on trouve souvent des conversations croisées, des utilisateurs qui causent par groupe. Pour faire comprendre à un utilisateur qu'on s'adresse directement à lui, il suffit de cliquer gauche sur son nom dans la Canal List. Ceci ajoute une entête au message. La personne en question comprendra que ce message est pour elle. De plus si elle parle au même moment sur un autre onglet, l'onglet du canal se mettra en surbrillance.

Contact List

Différentes fenêtres

Le client YooGoo comporte également d'autres fenêtres importantes. Il existe une fenêtre qui recense toutes les informations ou erreurs en provenances du serveur. Il est possible de loguer toutes ces informations dans un fichier. Chaque message contient 3 informations : la date, le niveau (erreur, note ...), le corps du message.

Pour pouvoir trouver des personnes, il faut pouvoir les rechercher. Il existe donc une fenêtre qui affiche les résultats des recherches de l'utilisateur. De cette fenêtre il a la possibilité d'effectuer des actions, comme dans la Canal List. L'utilisateur pour sa recherche dispose de plusieurs critères de recherche, comme le pseudo, l'âge, le sexe, la ville ...

Il existe également des fenêtres d'information comme les fenêtres Who et What pour obtenir les informations d'un utilisateur, ou d'un canal. L'utilisateur peut en ouvrir autant qu'il le souhaite, jusqu'à saturation de la mémoire.

De plus pour écrire des textes joyeux, l'utilisateur, s'il ne connaît pas les raccourcis clavier, peut utiliser la fenêtre de smileys. Pour l'ouvrir, il suffit de cliquer sur le bouton perso1 de la barre des polices.

Enfin la fenêtre peut-être la plus importante est celle d'options. L'utilisateur peut paramétrer la configuration réseau, il peut choisir les fenêtres à afficher, modifier les informations de son asv ...

Personnalisation

Une de nos plus grandes préoccupations était de faire un client qui soit agréable à regarder mais qui soit le plus simple possible à utiliser. Notre client doit donc être personnalisable, car chacun utilise un ordinateur à sa façon. Pour adapter le client au goût de l'utilisateur nous utilisons deux modes de personnalisation. Une personnalisation à l'aide de skin qui permettra de poser des images un peu partout sur le client, à l'image de WinAmp. Les emplacements pour ces images sont déjà positionnés cependant la création de ces images ne sera effectuée que pour la deuxième soutenance, car la création d'éléments graphiques demande un temps énorme.

La deuxième personnalisation vient du "dockage" des fenêtres. Le "dockage" est une fonctionnalité du "drag and drop". Il consiste à intégrer une fenêtre à l'intérieur d'une autre. Pour cela, il suffit de cliquer sur une fenêtre et de la déplacer sur un élément qui accueille ces fenêtres. Pour qu'un contrôle de la fenêtre puisse accueillir il faut que la propriété "DockSite" soit à vrai. Cependant nous avons voulu faire mieux encore. A l'instar de l'interface de Microsoft Visual C++, nous avons voulu faire en sorte que certains éléments "encastrables" puissent en accueillir d'autres. Pour cela il ne suffisait pas seulement de mettre la propriété DockSite à vrai car les contrôles se seraient mis n'importe comment.

Pour qu'une fenêtre puisse en accueillir une autre, il a fallu créer un système de PageControl permettant la gestion des onglets. Ainsi on peut docker autant de fenêtres qu'on le désire dans la fenêtre CanalList ou ContactList. La gestion de ces onglets et de toute cette personnalisation a pris la plus grande partie de cette 3ème soutenance. Il a dans un premier temps fallu comprendre le fonctionnement du "drag and drop", car nous n'y connaissions rien. Nous ne pensions d'ailleurs pas pouvoir réussir à faire quelque chose d'aussi personnalisable que cela. Le "drag and drop" marche en deux modes. Le mode manuel où pour draguer ou docker les contrôles il faut obligatoirement passer par le code, et le mode automatique où l'utilisateur par le biais de la souris peut également déplacer les objets.

Ainsi il est possible de positionner les fenêtres pour réaliser un client monobloc. Pour positionner les fenêtres à l'intérieur de la fenêtre mère (client) nous avons créé quatre emplacements qui se situent sur les côtés du client. Pour comprendre comment se dockent les contrôles il faut étudier la composition du client. En effet ce dernier est composé de multiples panels aligner différemment. Un panel est un contrôle qui contient d'autres contrôles. L'alignement de ces panels peut être soit à gauche, soit à droite, soit en haut, soit en bas, soit sur toute la surface. Ce dernier se positionne donc entre tous les alignements et se retrouve au centre de la fenêtre. C'est l'alignement que nous avons choisi pour le panel principal où se trouvent les textbox pour dialoguer. Le client est donc constitué de 5 panels. Les 4 autres panels qui sont sensés accueillir les fenêtres dockées ont la propriété AUTOSIZE à vrai ce qui leur permet de prendre la taille de la fenêtre qu'ils contiennent. Lorsqu'il n'a aucune fenêtre sa largeur ou sa hauteur est à zéro ce qui fait que nous ne voyons pas ce contrôle.

De plus l'utilisation des panels permet l'utilisation des Splitter qui sont des barres permettant de redimensionner deux contrôles. Ce dimensionnement n'est possible que si les contrôles sont alignés par rapport à un autre contrôle ce qui est notre cas ici (par exemple dans l'explorateur Windows entre la partie gauche et la partie droite de la fenêtre). Seulement dans notre cas la propriété AUTOSIZE étant activée il est impossible de redimensionner. Pour pouvoir contourner ce problème il a donc fallu désactiver cette propriété en passant par l'événement de la Splitter ONCANRESIZE(appelé lorsqu'on clique sur la splitter) puis la réactiver par l'événement ONPAINT (appelé lorsque la splitter est redessinée).

Il est donc possible de faire tout ce que l'on veut avec les fenêtres du client qui devient du même coup plus personnalisable qu'aucun autre client de chat.

Enfin la dernière méthode de personnalisation est celle des options. En effet l'utilisateur pourra adapter le client en modifiant les options qui lui sont données. Cette personnalisation n'est pas encore très poussée mais elle devrait l'être pour la dernière soutenance.

Skin de base

Pour pouvoir personnaliser YooGoo nous avons pensé à l'utilisation de skin. Une skin est en fait une image ou plusieurs images qui se placeraient à certains endroits du client pour faire ressortir un thème général, et le rendre plus agréable à regarder.

Pour cette année, nous ne voulions pas nous pencher sur le graphisme, donc nous avons juste fait en sorte que des utilisateurs puisse développer leur propre skin et les poster à l'avenir sur le site.

La gestion des skins se fera dans le menu option à l'aide d'une liste déroulante. Des zones images sont placées un peu partout sur le client. Le rendu n'est pas encore terrible sur cette version du client car nous ne maîtrisons pas encore la transparence des composants et tout ce qui est graphisme en général, mais l'accent sera mis sur l'aspect graphique pour la prochaine version.

Divers

Fichier d'aide

Fichier d'installation

Mais YooGoo c'est aussi

Un site web

But du Site web

Le site web présente le projet. On y trouve toutes les informations du projet. Pour cela, il nous avons une page de news que nous avons tenue à jour tout au long du dévelloppement du projet. Les différentes versions du projet ainsi que les sources sont accessibles. Le site possède aussi une partie documentation qui permet à n'importe qui de programmer son propre Serveur/Client. La documentation se compose prioritairement de la SDK mais aussi des rapport des differente soutenances et du cahier des charges. La documentation est disponible sous différents formats (Latex, Dvi, Html, Pdf...). Pour complèter la documentation, nous avons mis en place une FAQ. Le site web possède aussi un espace photos pour garder quelques souvenir du projet. Enfin, nous avons mis nos CVs en ligne. Un site tel que celui-ci est ingerable en HTML. Nous utilisons donc le couple PHP/SQL pour mettre en place ce site.

HTML/PHP/SQL

PHP est un langage qui permet de générer du code HTML. Seul le serveur interprète le code. Pour le client, il n'y a aucune différence, il reçoit du code HTML. Une page comportant du code PHP doit comporter l'extension .php (.php3) pour que le serveur web l'interprète. Pour insérer du code PHP dans une page, on utilise la balise $<$? ... /$>$. On utilise alors la commande pour renvoyer du code HTML.

ex: echo $<$H1$>$ $\backslash$$titre $<$/H1$>$

Les variables en PHP commencent toutes avec un '$'. Il est possible d'initialiser certaines variables en appelant la page avec la syntaxe suivante.

mapage.php?var1=42;var2=53

Il est ainsi possible de transmettre des informations à d'autres pages. PHP possède un add-on qui permet de se connecter à une base SQL. SQL est un protocole permettant d'envoyer des requêtes à une base de donnée (les plus utilisées : MySQl, Oracle et MS SQL). L'instruction pour récupérer des données est:

SELECT $<$champs$>$ FROM $<$base$>$ WHERE $<$condition$>$

Par exemple si on veut récupérer le Nom et la profession de toutes les personnes de 18 ans depuis la base user, on envoiera l'instruction:

SELECT Name,Job FROM user WHERE age=18

En créant les bonnes bases, on peut rendre le couple PHP/SQL très puissant. Les principaux scripts du site web sont :

-
Les news
-
Les téléchargements
-
La Documentation
-
Les CVs

Ils ont à peu près tous le même fonctionnement. La base SQL contient les différents éléments (news/téléchargement/documentation/CVs) avec les champs:

id title text date .....

On récupère ensuite l'ensemble des éléments et on les affiche avec une boucle while toute simple. Alors qui aurait fallu recopier le code à chaque fois si on avait utilise du HTML normal (vive PHP).

Outils utilisés pour la création du site web

Pour gérer la base SQL, la plupart des hébergeur possèdent un outil qui s'appelle PHPMyAdmin.

PHPMyAdmin est un outil sécurise qui permet d'avoir une vue graphique d'une base SQL. Il permet d'avoir accès facilement au commande de base de SQL ajout, suppression et recherche. Il permet aussi à l'utilisateur d'envoyer ses propres requêtes.

Pour le site web, nous ne voulions pas avoir trop de travail graphique à faire. Ce n'est effectivement pas le but du projet. Bien que PHP nous aide dans cette tache, il nous semblait difficile de faire un site web sans beaucoup toucher aux logiciels graphiques. Et puis, nous avons trouvé PHPNuke. PHPNuke est un ensemble de scripts PHP qui permettent de crée un site web PHP sans quasiment toucher une ligne de code. PHPNuke nous a surtout été utile pour les fonctions pour créer l'interface graphique.

Nous avons utilise PHPNuke 5.0 pour la création de YooGoo.com. Les plus utiles des fonctions sont celles qui permettent de créer des cadres. Ca parait bête mais pour cela, il faut faire un tableau 3x3 où les cellules du bord contiennent les images des bordures. Et surtout, il faut avoir un ensemble d'images dont les dimensions sont calculées pour qu'elles correspondent avec les dimensions du tableau. Bref, ces fonctions nous ont épargné tout ce calcul laborieux.

PHPNuke possède aussi une interface pour l'administration du site web. Néanmoins, nous n'utilisons pas cette interface. Tout d'abord parce que nous avons beaucoup modifié PHPNuke pour nos besoins et que les scripts d'administrations ne sont plus compatibles. Ensuite, nous préférons utilise PHPMyAdmin qui demande une meilleure compréhension du code mais offre plus de possibilités.

Nous avons aussi utilisé MIG, un script permettant de créé des galeries de Photos. MIG nous permet de mettre en ligne rapidement les photos numériques de Danao. Nous avons modifié ce script pour pouvoir ajouter des video dans notre gallerie et avoir la possibilité d'ajouter des commentaires. La partie Photo est la plus visitée du site. Nous avons donc ouvert un second site (poupouill.free.fr) qui utilise le meme script mais qui ne contient que des photos. De plus, cela nous permet de ne plus nous limiter aux photos de YooGoo.

Par curiosité, nous voulions savoir qui visite yoogoo.com. Nous utilisons pour cela le très célèbre Xiti. Xiti s'utilise en ajoutant simplement un petit morceau de JavaScript dans son code HTML. Xiti permet de savoir de nombreuses informations sur les internautes qui visitent le site.

Il est vrai que Xiti emmène un petit cote ludique au pauvre webmaster. YooGoo a connu une croissance supérieure a 1000% entre Novembre et Décembre. Au mois de Décembre, nous affichions 55 visiteurs mensuels.

Choix de l'hébergeur

L'utilisation de PHP réduit énormément le choix des hébergeur. Au début nous utilisions Free.fr. Mais, nous avons été déçus par la lenteur de la connexion WWW et FTP. De plus, Free, n'accepte que PHP3 alors, que la plupart des exemples de codes se trouvent en PHP4. La grosse de différence entre PHP3 et PHP4 est que l'extension des fichiers doit être .php3 dans le premier cas et .php dans le second. Il faut donc à chaque fois modifier les extensions de tous les fichiers ainsi que toutes les références à ces fichiers. Nous avons donc essayé multimania. Multimania offre une gestion PHP4 mais le débit n'est pas beaucoup plus important que free et nous eu la surprise de voir une publicité sur notre site. De plus, au mois de Mai, nous avons eu la mauvaise surprise de voir le compte YooGoo supprimé de Multimania! Nous avons su plus tard qu'un script de suppression de comptes s'était emballé et avait supprimé beaucoup de comptes. Ne pouvant nous passé du site pendant une semaine, nous avons déménagé sur Free.fr. Nous avions aussi pensé à utiliser une de nos propre machine comme hebergeur. Mais la faible bande passante nous en a disuadué. Néamoins, nous utilisons notre machine comme serveur de secours (ce qui a été utile plusieurs fois dans l'année)

Une machine

Le nom de domaine YooGoo.com

Lors de la création du projet nous avions décidé de réalisé un projet aux apparences sérieuses et professionnel. C'est dans cet esprit que nous avons monté un serveur Unix réalisant la plupart des services indispensables à une petite entreprise.

Nous avons commencé par acheter YooGoo.com chez Gandi (www.gandi.net) pour 99fr. Gandi propose l'enregistrement du nom de domaine ainsi qu'un DNS (Domain Name Server). Malheureusement une telle configuration ne nous satisfaisait pas car le nom de notre site web restait yoogoo.multimania.com et non www.yoogoo.com. De plus il nous était impossible de profiter pleinement du domaine yoogoo.com. Nous avons alors décidé de monter notre propre DNS.
Le DNS qui a été installé est Named dans sa derniere version (v9.2.0a2). Ainsi nous sommes libres d'organisé notre sous domaine comme bon nous semble. L'état actuel est :

                            IN NS     Zapserv.yoogoo.com.
                            IN MX  10 mail.yoogoo.com.
        ;
        Zapserv             IN A     195.132.224.69


        www                 IN CNAME Zapserv.yoogoo.com.
        zapata              IN CNAME Zapserv.yoogoo.com.
        danao               IN CNAME danao.no-ip.com.
        hargos              IN CNAME hargos.no-ip.com.
        ftp                 IN CNAME Zapserv.yoogoo.com.
        mail                IN CNAME danao.no-ip.com.

(extrait de /etc/namebd/yoogoo.com fichier de configuration du domaine yoogoo.com)

C'est-à-dire Zapserv (le serveur principal) qui fait office de DNS a comme alias ftp, Zapata et www. Le mail @yoogoo.com est redirigé sur la machine de Franck (danao.no-ip.com) ainsi que mail.yoogoo.com. Ainsi que hargos.yoogoo.com est redirigé vers hargos.no-ip.com.

La machine et ses services

La machine qui nous sert actuellement de serveur est un PC Compaq, Pentium 120Mhz, avec 16MB de Ram, et un disque dur de 800MB, il est relié à Internet par une connexion par câble (Noos) avec un débit de 512k descendant et 128k ascendant. Cette machine est sous NetBSD 1.5 et sert de passerelle et pare-feu au réseau local de Marco (l'administrateur de la machine). Pour cela elle utilise IPNAT, IPFilter et l'IP forwarding (aussi appelé IP Masquerading). (REM : Cette machine est un vrai serveur, elle est allumée 24h/24, 7j/7).

Comme on peut le constater il existe un serveur de mail pour les adresses du type login@yoogoo.com , celui-ci est sur la machine de Franck. Mais il faut l'avoué cette fonction nous sert plus pour le prestige (et le coté professionnel) que pour autre chose car nous n'utilisons pas ces adresses (du moins pour l'instant). Néanmoins il existe déjà une interface graphique pour enregistrer un compte accessible au mail.yoogoo.com :81.

Il existe aussi un ftp privée sur la machine Zapserv qui nous sert pour échanger les fichiers, pour mettre les dernières versions de notre travail, et pour rendre les rapports. On essaye de le maintenir le plus a jour possible, ainsi chaque membre du groupe peut accéder a l'ensemble du travail quand il le désir. C'est un élément important car pendant les vacances il nous est parfois difficile de communiquer (et les gros fichier par mail ne sont pas acceptés, merci EPITA). Pour ce qui est de la technique du ftpd, il est lancé a partir de inetd, ainsi il n'est lancé que lorsqu'il y a des connections. De plus il est chrooted afin de protéger l'accès au fichier système.

Par ailleurs, il est aussi possible d'accéder au serveur par ssh si on a besoin de faire des opérations un peut plus complexes. Cela peut permettre aussi aux membres du groupe qui n'ont pas d'Unix chez eux d'en avoir un où ils peuvent s'amuser (sans risquer de close compte). La machine n'ayant pas d'écran, le ssh est aussi le seul moyen de l'administrer (très pratique quand on est en vacances aussi).

Enfin vient le serveur web. Comme on peut le voir à partir de la table du DNS, www.yoogoo.com est redirigé sur Zapserv. Le serveur web est apache, recompilé avec le support du php4. Puisque notre site web se base sur PHPNuke qui utilise mySQL, un serveur mySQL a été recompilé et installé pour permettre un bon fonctionnement du site web. De plus pour une administration plus aisée de mySQL, PhpMyAdmin a été installé. Un alias pour www.yooogoo.com/phpmyadmin/ a été crée, et l'accès du répertoire est protégé grâce à un mot de passe, qui fonctionne par autorisation au près de apache grâce a un .htaccess. Le serveur Unix fait donc tourner Apache/php4/mySQL/PhpMyAdmin.

De la documentation

Nosu sommes efforcé de beaucoup documenter YooGoo. Nous avons donc rédiger une SDK. Pour cela, nous avons utilisé Microsoft HTML Help WorkShop. HHW permet de produire un fichier d'aide .chm a partir de fichiers html. La SDK de YooGoo possède plus d'une centaine de pages et plus de 300 liens. Nous espérons qu'elle donnera une longue vie a YooGoo.

 
 

 
 
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