![]() |
||
![]() |
||
![]() |
|
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-bloquantNou 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.
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 serveurLors 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.
ConnexionLors 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 :
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 parseurUn 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éconnexionLa 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.
ThreadUn 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 MonothreadC'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 MultithreadC'est un programme utilisant plusieurs threads au sein d'un même processus et qui partage un même espace mémoire.
MT-SafePour 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 :
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.
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 :
C'est après avoir effectué ces décompositions qu'on peut savoir quelle partie du projet aura besoin de threads.
Création et gestion des threadsPour 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.
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 :
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.
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 :
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 :
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 :
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 :
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 :
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 :
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 :
Il existe 4 valeurs de retour :
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
IntroductionLa 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 UserChaque 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.
ProfilIl 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 destructeurPour pouvoir créer et détruire proprement un canal il faut adapter le constructeur et le destructeur à nos besoins.
Le profile d'un CanalPour 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
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
AVL
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.
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.
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 :
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.
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 niveauxIl y a différent niveau de log choisit par l'utilisateur. C'est une combinaison de flags. Les différents flags sont :
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 '
;\\
; 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 GraphiqueStructure du clientIntroduction
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.
ParseursIls 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é :
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 !
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 "
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 :
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 clientAu 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 canalL'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 personneIl 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 personneRepré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 canalUn 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 canalAfin 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 ongletUn 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 textesLe 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 messageLorsque 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 ongletUn 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 ...
Pour envoyer un message au serveurUne 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 getmsgLa 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 clientUne 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'installationMais YooGoo c'est aussi
Un site web
But du Site webLe 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
ex: echo
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
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 :
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ébergeurL'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.
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 documentationNosu 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.
| ![]() |
|
|