Afin de ne pas rester bloqué dans une fonction d'écriture sur un socket (fonction write()) ou dans une fonction de lecture sur un socket (fonction read() ou fonction accept()) tandis qu'une autre opération d'entrée/sortie pourrait se faire, une fonction d'attente multiple a donc été intégrée à la "socket library".
Cette fonction prend 5 paramètres :
La fonction modifie les 3 ensembles reçus et elle retourne un entier qui indique le nombre de sockets prêtes s'il est positif ou nulle et un code d'erreur quand il est négatif.
Remarque : Sous Linux, la structure timeout est modifiée, en sortie de la fonction, afin d'indiquer le temps restant, mais il est conseillé, par soucis de compatibilité avec d'autres systèmes d'exploitation, de considérer que le contenu de la structure est alors indéterminé et doit donc être réinitialisé si on souhaite la réutiliser.
Les constantes correspondant aux erreurs sont :
Pour pouvoir utiliser cette fonction ainsi que les macros associées, il faut inclure sys/select.h.
Afin de manipuler les ensembles de numéros de descripteurs de fichiers, quelques macros ont été définies dans la "socket libary" permettant d'initialiser, de possitionner, et de tester chaque descripteur d'un ensemble.
Cette macro est une procédure qui met à zero un ensemble de numéros de descripteurs de fichiers. Elle prend un paramètre :
Voici un exemple de code C, où on déclare un ensemble de numéros de descripteurs de fichiers et on l'initialise à 0. Ainsi les drapeaux de chaque numéro de descripteur de fichiers sont tous désactivés.
#include <sys/select.h> fd_set fds; FD_ZERO(&fds);
Cette macro est une procédure prenant deux paramètres :
Voici un exemple de code C crée et initialise un ensemble de numéro de descripteurs de fichiers pour lequel seuls les drapeaux correspondant aux numéros des socket1 et socket2 sont actifs.
#include <sys/select.h> int socket1,socket2; fd_set fds; FD_ZERO(&fds); FD_SET(socket1, &fds); FD_SET(socket2, &fds);
Cette macro est une fonction prenant deux paramètres :
La fonction retourne Faux (0) si le descripteur n'appartient pas à l'ensemble et Vrai (1) sinon.
#include <sys/select.h> int n, socket1,socket2; fd_set fds_read; ... FD_ZERO(&fds_read); FD_SET(socket1, &fds_read); FD_SET(socket2, &fds_read); n=select(FD_SETSIZE,&fds_read,NULL,NULL,NULL); if (n>=0) { if (FD_ISSET(socket1, &fds_read)) { read(socket1,&buf,sizeof(buf)); ... } if (FD_ISSET(socket2, &fds_read)) { read(socket2,&buf,sizeof(buf)); ... } }
Cette macro est une constante du système donnant le nombre maximum de descripteur de fichier que le système est capable de gérer. C'est donc également la taille maximale d'un ensemble de numéros de descripteurs de fichiers.
Cette macro qui sert à retirer un descripteur de fichiers de l'ensemble concerné, est une procédure prenant deux paramètres :
Ce premier exemple de programme ne gère que les lectures via la fonction select, partant de l'hypothèse (qui peut être fausse) que les écritures ne seront pas bloquantes car les messages sont courts et peu nombreux et pourront donc tenir dans les buffers de sortie de la pile réseau.
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <netdb.h> #include <sys/socket.h> #include <sys/select.h> #include <string.h> // pour la fonction bzero #include <arpa/inet.h> // pour la fonction inet_addr int state[FD_SETSIZE]; void suite(int fd, char *buf) { switch(state[fd]) { case 0: write(fd,"Hello\r\n",7); state[fd]=1; break; case 1: strtok(buf,"\r\n"); if (strcmp(buf,"Bye")==0) state[fd]=0; else write(fd,"I do not undestand\r\n",20); break; } } void srv(int fd_srv) { fd_set fdrs; FD_ZERO(&fdrs); FD_SET(fd_srv,&fdrs); while(1) { fd_set fds_read; int n,i,fd_con; struct sockaddr_in adr_cli; socklen_t adr_cli_len; char buf[1024]; memcpy(&fds_read,&fdrs,sizeof(fd_set)); n=select(FD_SETSIZE,&fds_read,NULL,NULL,NULL); if (n > 0) for (i = 0 ; i < FD_SETSIZE ; i++ ) if (FD_ISSET(i,&fds_read)) if (i==fd_srv) { adr_cli_len = sizeof(adr_cli); fd_con = accept(fd_srv, (struct sockaddr *) &adr_cli, &adr_cli_len); if (fd_con >= 0) { FD_SET(fd_con,&fdrs); printf("Connexion acceptée !\n"); state[fd_con]=0; suite(fd_con,""); } } else { n=read(i,&buf,sizeof(buf)-1); buf[n]=0x00; suite(i,buf); if (state[i]==0) { FD_CLR(i,&fdrs); close(i); printf("Connexion fermée !\n"); } } } } int main() { int fd_srv; struct sockaddr_in adr_srv; int err; fd_srv = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd_srv < 0) { printf("Erreur de création de la socket !\n"); exit(1); } bzero(&adr_srv, sizeof(adr_srv)); adr_srv.sin_family = AF_INET; err = inet_aton("0.0.0.0", &adr_srv.sin_addr); if (err == 0) { printf("Adresse IPv4 invalide !\n"); exit(1); } adr_srv.sin_port = htons(8080); err = bind(fd_srv, (struct sockaddr *) &adr_srv, sizeof(adr_srv)); if (err != 0) { printf("Erreur d'accès au port serveur !\n"); exit(1); } err = listen(fd_srv, 5); if (err != 0) { printf("Erreur de création de la file d'attente !\n"); exit(1); } printf("Serveur lancé !\n"); srv(fd_srv); return 0; }
Pour réaliser cet exemple, un automate a été programmé via la procédure "suite()" qui est appelée après chaque lecture. La procédure opére, en fonction de l'état courant mémorisé via le tableau "state" pour chaque connexion établie, puis elle détermine l'état suivant.
Dans cet automate, l'état numéro 0 correspond à l'état initial et le retour à l'état numéro 0 mettra fin à la connexion. L'état 1 correspond à la lecture et au traitement qui en découle.
Dès que l'on doit construire un programme plus complexe utilisant la fonction "select()" pour dispatcher les tâches, il convient de construire un diagramme d'états dans lequel est associé, à chaque changement d'état, l'attente d'une lecture disponible ou d'une écriture possible.
Pour gérer les attentes des lectures et des écritures, on va utiliser deux fd_set : fdrs et fdws déclarés comme variables globales et on va gérer les différents états de l'automate sous la forme de procédures indépendantes. On fera alors la mémorisation de l'état de chaque connexion via un pointeur vers la procédure correspondante.
fd_set fdrs,fdws; void (*state[FD_SETSIZE])(int);
Dans chaque procédure gérant un état, on se propose de faire appel aux procédures suivantes que l'on définit ainsi :
void will_read(int fd, void (*st)(int)) { FD_SET(fd,&fdrs); FD_CLR(fd,&fdws); state[fd]=st; }
void will_write(int fd, void (*st)(int)) { FD_SET(fd,&fdws); FD_CLR(fd,&fdrs); state[fd]=st; }
void will_close(int fd, void (*st)(int)) { FD_CLR(fd,&fdrs); FD_CLR(fd,&fdws); state[fd]=NULL; st(fd); close(fd); }
Chacune de ces procédures est appelée avec pour paramètres :
On modifie donc le contenu de la procédure srv() afin qu'elle surveille à la fois la disponibilité d'une lecture et la disponibilité d'une écriture en fonction de ce qui est demandé par chaque connexion et qu'elle appelle alors la procédure qui a été mémorisée dans le tableau des pointeurs de fonction. Le code de la procédure laisse à chaque procédure, gérant un état de l'automate, le soin de commencer par faire le read ou le write attendu. L'état inital de l'automate est géré par la procédure nommée state0.
void srv(int fd_srv) { FD_ZERO(&fdrs); FD_SET(fd_srv,&fdrs); FD_ZERO(&fdws); while(1) { fd_set fds_read,fds_write; int n,i,fd_con; struct sockaddr_in adr_cli; socklen_t adr_cli_len; memcpy(&fds_read,&fdrs,sizeof(fd_set)); memcpy(&fds_write,&fdws,sizeof(fd_set)); n=select(FD_SETSIZE,&fds_read,&fds_write,NULL,NULL); if (n > 0) for (i = 0 ; i < FD_SETSIZE ; i++ ) { if (FD_ISSET(i,&fds_read)) if (i==fd_srv) { adr_cli_len = sizeof(adr_cli); fd_con = accept(fd_srv, (struct sockaddr *) &adr_cli, &adr_cli_len); if (fd_con >= 0) state0(fd_con); } else state[i](i); if (FD_ISSET(i,&fds_write)) state[i](i); } } }
L'automate à états qui va gèrer à la fois les différentes écritures et les lectures correspondants à l'exemple est le suivant :
A la place de la procédure nommée suite() on trouve les procédures de chaque état de l'automate nommées respectivement state0(), state1(), state2() et state3() :
void state0(int); void state1(int); void state2(int); void state3(int); void state0(int fd) { printf("Connexion acceptée !\n"); will_write(fd,&state1); } void state1(int fd) { write(fd,"Hello !\r\n",9); will_read(fd,&state2); } void state2(int fd) { char buf[1024]; int n; n=read(fd,&buf,sizeof(buf)-1); buf[n]=0x00; strtok(buf,"\r\n"); if (strcmp(buf,"Bye")==0) will_close(fd); else will_write(fd,&state3); } void state3(int fd) { write(fd,"I do not undestand\r\n",20); will_read(fd,&state2); }