Sous-sections
La stratégie d'entrées/sorties est la manière dont les
informations reçus et envoyées sont traitées. Un bon choix est
déterminant car tout le code réseau repose sur celui ci. Pour
rappel les différentes méthodes possibles sont :
- Sockets Bloquants
- : Par défaut, un socket est bloquant, c'est
à dire qu'il ne rend pas la main à l'application tant qu'il n'as
pas finis d'effectuer ces tâches ou qu'il y a eu un problème.
- Sockets purement non-bloquants
- : Un appel avec un socket
non-bloquant rend la main immédiatement si il ne réussit pas
à terminer sa tâche immédiatement. Ceci permet au programme
d'effectuer d'autres tâches pendant que les opérations du réseau
finissent. Par contre il faut continuellement tester le socket
pour savoir si son opération est finie.
- Sockets Asynchrones
- : Ce sont des sockets non-bloquants qui
renvoient automatiquement un message quand il se passe quelque
chose d'intéressant sur le socket.
- Select()
- : La fonction select() permet à un thread de
s'occuper d'un groupe de sockets intéressants, c'est à dire qui
reçoivent ou émettent des données. Elle est généralement utilisée
pour éviter d'effectuer des test continus sur les sockets
non-bloquants.
- Objets événement
- : utilisés avec WSAEventSelect(), ce mécanisme
rassemble à la fonction select(), mais un peu plus efficace. Par
contre elle ne fonctionne que sous Windows, tandis que
select() fonctionne aussi avec les sockets BSD.
- Overlapped I/O
- : une des nouvelles fonctions de Winsock 2.
C'est la méthode la plus efficace car elle se base directement
sur la méthode de Windows pour gérer les entrées/sorties.
C'est ce que nous utilisons dabs le serveur YooGoo.
Si on dresse un tableau récapitulatif des différentes
méthodes utilisables pour chaque système d'exploitation on obtient
:
|
Win9x |
WinCE |
Win NT4+ |
Win NT 3.x |
Win16 |
Unix |
| Sockets Bloquants |
oui |
oui |
oui |
oui |
oui |
oui |
| Sockets non-bloquants |
oui |
oui |
oui |
oui |
oui |
oui |
| Sockets asynchrones |
oui |
non |
oui |
oui |
oui |
non |
| Objects événement |
oui |
non |
oui |
non |
non |
non |
| Overlapped I/O |
oui |
non |
oui |
non |
non |
non |
| Threads |
oui |
oui |
oui |
oui |
non |
oui |
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.
Nou utilisons dans YooGoo des sockets non-bloquants,
couplés avec la fonction select() qui permet de
savoir s'il y a de l'activité sur un des sockets
surveillés. Cette méthode malgré qu 'elle soit plus lente
nous permet de ne jamais être bloqué lors d'une réception
ou d'un envoie de message. Lorsqu'on envoie (ou reçoit),
winsock va faire un appel à la fonction send() (ou
recv()) et va envoyer (ou recevoir) le plus de
données en un coup, et même si tout le message n'est pas
envoyé, il va continuer l'exécution du programme. C'est au
programmeur de gérer l'envoie de la fin du message qui n'a
pas été envoyé.
Il est important de noter que toutes ces
fonctions ont une syntaxe très proche de la version sous
Unix des socket, ainsi que la création de thread qui est à
peu de choses près pareil. De plus les fonctions utilisant
Winsock ont toutes été regroupées dans un même fichier afin
de faciliter le portage du code sous une
autre plateforme.
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.
En mode non-bloquant, le programme lance la procédure de
lecture ou d'écriture et renvoie tout de suite la main au
programme. Il faut donc faire attention car toutes les
données n'ont peut-être pas été envoyés, et il faut
reprendre plus tard au bon endroit. Pour cela nous avons
dans la classe user un " buffer " d'envoie, un " buffer "
de réception, le nombre d'octets dans chacun de ces
buffers, et la taille du message en
cours de réception.
On a découpé la gestion du transfert des messages en trois
fonctions.
- *
- La fonction NetRecv() reçoit les messages qui
sont sur le socket tant qu'il y a de la place dans le " buffer "
de réception. Il faut à chaque appel de la fonction vérifier à
quel point du message on est. On reçoit d'abord les données. Si
on a une taille de message à recevoir égale à zéro alors on n'a
pas encore commencé à recevoir de messages. Les deux premiers
octets sont donc utilisés pour lire la taille du message qui
arrive. On supprime ensuite ces deux octets du "buffer". Si le
nombre d'octets dans le buffer est supérieur ou égal à la taille
du message qu'on est en train de recevoir, on décrypte le message,
on l'enfile dans la file de message de l'utilisateur et on enfile
l'utilisateur dans la file des utilisateurs qui ont des messages.
C'est le thread qui gère les utilisateurs qui traitera ces diverses
files. A ce moment on boucle, on regarde si on a deux octets pour
avoir la taille du prochain message et on regarde si on a assez
d'octets pour recevoir le prochain messages aussi. La fonction se
termine dès que le nombre d'octets dans le buffer n'est pas
suffisant soit pour lire la taille du prochain message, soit est
inférieur à la taille du message que l'on est en train de recevoir.
Ce qu'il faut bien comprendre c'est qu'à chaque appel de la
fonction on reprend là où l'on s'était arrêté à l'appel précédent,
car la fonction peut se terminer dans n'importe quel état.
- *
- La fonction NetEnqueu() sert à ajouter un message
dans le " buffer " d'envoie de l'utilisateur. La fonction s'occupe
de crypter le message, et ajoute la taille du message dans le buffer,
suivit du message.
- *
- La fonction NetSend() ne fait rien d'autre que
d'envoyer les données qui sont dans le " buffer " d'envoie.
Dans toutes ces fonctions il faut faire attention au
débordement de " buffer ". La fonction renvoie le nombre
d'octets qui sont soit envoyés, soit reçus, soit enfilés.
Si la valeur de retour est zéro c'est qu'il y a eu un
problème de "buffer" et il faudra donc réessayer plus tard
quand il y aura de la place dans le buffer (ou alors
découpé le message en plusieurs morceau si on veut
l'envoyé). Si la valeur de retour est -1 c'est qu'il y a eu
une erreur de socket et dans ce cas l'erreur du socket sera
géré par la boucle principale.