Aller au menu - Aller au contenu

Icône La selection de sockets

Avatar
Mise à jour : 05/05/2009
Difficulté : Intermédiaire Intermédiaire
3 369 visites depuis 7 jours, dont 262 sur ce chapitre classé 48/786
Bien que les threads sont beaucoup utilisés dans le domaine du réseau, on utilise aussi un autre moyen pour manipuler plusieurs sockets : la sélection de sockets.
La sélection de socket est un principe un peu plus simple à comprendre que l'utilisation de threads. Mais, ne vous faites pas d'illusions car dans de nombreux cas vous aurez à utiliser les threads en plus de la sélection de sockets. Dans cette partie, mon but sera de vous expliquer quels sont les avantages et inconvénients de ces deux méthodes ;) .
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire

Le fonctionnement

Le fonctionnement global



La sélection de sockets s'inscrit dans un fonctionnement évènementiel, c'est à dire que tout se fait dans un seul thread et dans un seul et même processus. Elle présente une alternative puissante à l'utilisation des threads. Si vous avez lu le tutoriel de m@teo21 sur la SDL ou si vous connaissez, par exemple, l'API Windows, ce principe vous est déjà un peu familier :) .

Avant, avec l'utilisation des threads sans la sélection de sockets, nous avions un schéma similaire à celui-ci :

Image utilisateur

Et maintenant en utilisant la sélection de socket, nous avons ce schéma :

Image utilisateur
Notez que les schémas ci-dessus sont des grafcets :
  • Chaque rectangle désigne donc une action (étape) repérée par un nombre unique.
  • Chaque barre entre les actions désigne la condition pour que l'action suivante se réalise.
  • Les étapes se déroulent dans l'ordre (l'étape 3 se déroule après l'étape 2 et ainsi de suite).
  • On commence toujours par l'étape initiale (celle qui porte le numéro d'étape 0 et qui est encadré dans deux rectangles).

Dans le cas des threads, on crée une socket serveur, on liste les ports, puis pour chaque clients qui se connecte on crée un thread qui lui est approprié dans lequel la transmission entre le client et le serveur se déroulera.

Dans le cas de la sélection de sockets, on crée une socket serveur, on liste les ports, puis on initialise les descripteurs. Ensuite, on sélectionne la ou les socket(s) voulue(s) et pour chaque socket sélectionnée, on regarde dans quel état elle se trouve (y a t-il des données à lire ? à écrire ? etc.). Le tout ce fait dans un seul thread et dans un seul processus.
Notez que la sélection de sockets est bloquante pendant un temps que vous spécifiez ou non, c'est à dire que tant que l'état des descripteurs ne change pas ou tant que le temps donné n'est pas dépassé, la sélection reste bloquante. Si vous ne spécifiez pas de temps alors seul un changement d'état des descripteurs débloquera la sélection.

Qu'est ce qu'un descripteur de socket ?

Un descripteur de socket est tout simplement une variable (un entier) qui nous servira à manipuler la socket. L'état de cet entier peut nous permettre de connaître si des données ont été reçues ou envoyées sur la socket. Vous ne le savez peut être pas jusque là mais le type de variable SOCKET est lui même un type de descripteur de socket. Le type SOCKET n'est donc qu'un entier (int), néanmoins on préfère utiliser le type SOCKET pour mieux comprendre les choses et respecter les normes.

Qu'est ce qu'un ensemble ?

Un ensemble est un type de variable permettant de connaître l'état du descripteur de socket. Il en existe trois :
  • L'ensemble de lecture readfds, il permet de savoir si le client a envoyé des données sur la socket sélectionnée. Un appel à recv ne sera donc pas bloquant
  • L'ensemble de écriture writefds, il permet de savoir si le client a reçu les données sur la socket sélectionnée. Un appel à send ne sera donc pas bloquant
  • L'ensemble d'exception exceptfds, il permet de gérer les exceptions mais nous ne nous en servirons pas dans ce chapitre.

Ce qu'il faut donc retenir



Vous pouvez choisir si la sélection de sockets sera bloquante ou non quand tel ou tel événement se produit en fonction des descripteurs que vous lui transmettez.
Prenons le cas ou vous spécifiez la sélection d'une socket client avec un descripteur en lecture seulement (on cherche à savoir si l'on peut lire des données sur la socket, si c'est le cas cela signifie que l'on a reçu des données sur cette socket ;) ) et un temps limite de 50 ms : La sélection de la socket cliente est bloquante tant qu'elle ne reçoit pas de données jusqu'à ce que 50 ms se soit écoulé, après, la sélection rend la main (elle ne devient plus bloquante). La valeur qu'elle retourne spécifie l'évènement qui a mis fin au blocage (ici, le temps ou des données reçues peuvent mettre fin au blocage).

Un peu de pratique

Je m'en serais douté, vous avez surement eu du mal à comprendre ce qui a été spécifié ci-dessus et je vous comprends car ce n'est pas très simple :-° .
J'espère donc que la partie pratique vous sera plus parlante :) .


L'initialisation des descripteurs



Pour initialiser les descripteurs, nous allons utiliser des fonctions. Ces fonctions nous permettront de lier une ou plusieurs sockets à des ensembles. Par exemple, si nous voulons que la sélection d'une socket cliente soit bloquante jusqu'à se qu'elle reçoive des données en lecture, alors nous allons initialiser un ensemble de lecture et nous allons lui ajouter cette socket. Si l'ensemble en lecture est vide cela voudra dire qu'il n'y a rien à lire sur la socket. A l'inverse, si l'ensemble n'est pas vide cela signifie que la socket a reçu des données et que nous pouvons les lire.
De même, si nous voulons par exemple que deux sockets clientes bloquent la sélection jusqu'à ce qu'elles reçoivent des données en lecture, il suffira d'ajouter ces deux sockets à un même ensemble de lecture ;) .
Pour faire cela nous somme face à quatre fonctions présenté ci-dessous.

FD_SET


Code : C
1
FD_SET(int fd, fd_set* set);

Cette fonction ajoute le descripteur fd à l'ensemble set.
Le descripteur fd n'est rien d'autre qu'une socket mais comme dit plus haut, une socket est avant tout un type int.

FD_ISSET


Code : C
1
FD_ISSET(int fd, fd_set* set);

Cette fonction vérifie si le descripteur fd est contenu dans l'ensemble set après l'appel à select.
Par exemple, si l'ensemble set est un ensemble de lecture la fonction servira à savoir si la socket fd a reçu des données.

FD_CLR


Code : C
1
FD_CLR(int fd, fd_set *set);

Cette fonction supprime le descripteur fd de l'ensemble set.
Cette fonction est beaucoup moins utilisé que les trois autre mais n'en n'est pas pour au temps inutile.

FD_ZERO


Code : C
1
FD_ZERO(fd_set *set);

Cette fonction vide l'ensemble set. Cela revient à supprimer tout les descripteurs ajouté précédemment à l'ensemble.


La sélection de la socket



La sélection de sockets se fait via la fonction select qui détient le prototype suivant :

Code : C
1
int select(int fdmax, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

  • En cas de réussite la fonction retourne le nombre de descripteurs dans les ensembles. Si la fonction rend la main à l'application car le timeout a expiré alors elle retourne 0, sinon en cas d'erreur la fonction retourne -1.
  • Le paramètre fdmax correspond au descripteur de socket le plus grand auquel on ajoute un.
    Une fois que vous avez ajouté des descripteurs de sockets au ensembles vous allez chercher le descripteur le plus grand (vous savez maintenant que les descripteurs de sockets sont de simples entiers avant tout ^^ ) et le passer en paramètre à la fonction select tout en lui ajoutant un.
  • Le paramètre readfds correspond à l'ensemble de lecture. Si on ne veut pas recevoir des données sur aucune des sockets sélectionnées, on peut mettre ce paramètre à NULL.
  • Le paramètre writefds correspond à l'ensemble d'écriture. Si on ne veut pas envoyer des données sur aucune des sockets sélectionnées, on peut mettre ce paramètre à NULL.
  • Le paramètre exceptfds correspond à l'ensemble d'exception. Nous le mettrons à NULL car en général, nous ne l'utiliserons pas.
  • Le paramètre timeout est une structure qui contient le temps limite d'attente de blocage de la fonction. En général, nous le mettrons à NULL ce paramètre pour que la fonction reste bloquante tant qu'elle ne reçoit pas de changements d'états des descripteurs.

Notez que la recherche du descripteur de socket le plus grand n'est pas toujours très rapide sur des serveurs qui peuvent avoir des centaines ou même milliers de clients. On préférera alors faire la recherche du plus grand descripteur seulement quand un client quitte le serveur ou qu'un autre se connecte au lieu de le calculer à chaque fois avant l'utilisation de la fonction select.

Notez aussi que la fonction select peut modifier les ensembles qui lui sont passés en paramètres. Nous redéfinirons alors à chaque fois les descripteurs associés au ensembles avant l'utilisation de la fonction select


Un exemple



Voici un exemple de serveur multi-clients utilisant la sélection de sockets.
Le client se connecte au serveur puis est immédiatement déconnecté de celui-ci :

Code : C
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
#if defined (WIN32)
    #include <winsock2.h>
    typedef int socklen_t;
#elif defined (linux)
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #define INVALID_SOCKET -1
    #define SOCKET_ERROR -1
    #define closesocket(s) close (s)
    typedef int SOCKET;
    typedef struct sockaddr_in SOCKADDR_IN;
    typedef struct sockaddr SOCKADDR;
#endif

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define PORT 23



int main(void)
{
    #if defined (WIN32)
        WSADATA WSAData;
        int erreur = WSAStartup(MAKEWORD(2,2), &WSAData);
    #else
        int erreur = 0;
    #endif

    SOCKADDR_IN sin;
    SOCKET sock;
    int recsize = sizeof sin;

    int sock_err;

    if(!erreur)
    {
        sock = socket(AF_INET, SOCK_STREAM, 0);

        if(sock != INVALID_SOCKET)
        {
            printf("La socket %d est maintenant ouverte en mode TCP/IP\n", sock);

            sin.sin_addr.s_addr = htonl(INADDR_ANY);
            sin.sin_family = AF_INET;
            sin.sin_port = htons(PORT);
            sock_err = bind(sock, (SOCKADDR*) &sin, recsize);

            if(sock_err != SOCKET_ERROR)
            {
                sock_err = listen(sock, 5);
                printf("Listage du port %d...\n", PORT);

                if(sock_err != SOCKET_ERROR)
                {
                    /* Création de l'ensemble de lecture */
                    fd_set readfs;

                    while(1)
                    {
                        /* On vide l'ensemble de lecture et on lui ajoute 
                        la socket serveur */
                        FD_ZERO(&readfs);
                        FD_SET(sock, &readfs);

                        /* Si une erreur est survenue au niveau du select */
                        if(select(sock + 1, &readfs, NULL, NULL, NULL) < 0)
                        {
                            perror("select()");
                            exit(errno);
                        }

                        /* On regarde si la socket serveur contient des 
                        informations à lire */
                        if(FD_ISSET(sock, &readfs))
                        {
                            /* Ici comme c'est la socket du serveur cela signifie 
                            forcement qu'un client veut se connecter au serveur. 
                            Dans le cas d'une socket cliente c'est juste des 
                            données qui serons reçues ici*/

                            SOCKADDR_IN csin;
                            int crecsize = sizeof csin;

                            /* Juste pour l'exemple nous acceptons le client puis 
                            nous refermons immédiatement après la connexion */
                            SOCKET csock = accept(sock, (SOCKADDR *) &csin, &crecsize);
                            closesocket(csock);

                            printf("Un client s'est connecte\n");
                        }
                    }
                }
            }
        }
    }

    #if defined (WIN32)
        WSACleanup();
    #endif

    return EXIT_SUCCESS;
}


Notez qu'une socket serveur reçoit des données en lecture que quand un client se connecte à celui-ci. Bien que, les fonctions recv et accept soit bloquantes en temps normale, ici, elles ne le sont plus car on les appellent lorsqu'il le faut (par exemple, on sait que la fonction recv ne sera pas bloquante si des données viennent d'être reçues).

La sélection de sockets est présentée, ici, pour une application serveur mais sachez que le principe fonctionne aussi avec les applications clientes.
Chapitre précédent Sommaire

Partager

6 commentaires pour "La selection de sockets"
Note moyenne : 3.54 / 4 (79 votes)
Pseudo Commentaire
Hors ligne Pilou492 # Posté le 05/05/2009 à 23:40:39
Avatar

Ville : Talence
Pays : France métropolitaine
Études : Epitech Bordeaux

Merci pour la suite, en attente du reste avec impatience !

~ Pilou ~
{AER} - {EPITECH.} Bordeaux - EPITECH_2013
 
Hors ligne kanalkyte # Posté le 09/05/2010 à 16:40:11
plop
Avatar

Avis : Très bon

Études : Télécom Bretagne

Salut,
dans ton example, tu ne le fais qu'avec un seul socket, mais si on veut du multi client, on peut crée 6 sockets par exemple, les ajouter aux descripteurs et après utiliser le select ?
genre on fait un tableau de socket : SOCKET sock[6];
Après on fais les test pour les 6 sockets, et une fois que c'est fait, on ajoute aux descripteur et on gère avec le select ?
Peut-tu donner un exemple avec reception de données depuis le client ?
Merci d'avance.

Tuto Eyetoy sur PC
Image utilisateur
0 invitation Soptify restante.
 
Hors ligne Genroa # Posté le 18/06/2010 à 11:25:12

Pas mal, mais expliquer le fonctionnement d'un programme multi-clients et multi-serveur, avec un regroupement global à la fin, urait été le bienvenu.

ex: comment faire un tchat.

Ca aurait fait un peu de pratique. :p :p :p
Hors ligne NLS le pingouin # Posté le 17/08/2011 à 23:38:26

Tu devrais utiliser plus souvent exit() pour éviter toutes ces accolades imbriquées. Ton code serait plus digeste.
Attention, FD_ZERO, FD_SET, FD_CLR et FD_ISSET ne sont pas des fonctions, mais des macros.
Hors ligne Twilink # Posté le 29/01/2012 à 13:55:30
Avatar

Avis : Mitigé

il aurais été vraiment bon de donner des exemple multi client.

Venez visiter le Projet Lyoko Online. si le projet vous interesse,n'hesitez pas nous recherchons des modeliseurs 3D sur blender et des Codeurs C++.
 

Voir tous les commentaires