Une socket est une interface de communication introduite par les systèmes Unix pour la communication réseau. Il s’agit d’un point d’accès aux services de la couche transport, c’est-à-dire TCP ou UDP. La communication par sockets sur un réseau adopte généralement un modèle client-serveur ; en d’autres termes pour communiquer il faut créer un serveur prêt à recevoir les requêtes d’un client.
Dans tous les cas, avant d’utiliser une socket il faut la créer, c’est-à-dire créer un descripteur associé à l’ensemble des informations constituant la socket (buffers, adresse, port, état, etc.). Ensuite il est éventuellement possible de l’attacher à une adresse représentant la provenance des messages envoyés.
Côté serveur, la création de la socket est suivie d’une mise en attente de message dans le cas d’une communication UDP, ou de mise en attente de connexion dans le cas d’une communication TCP. Dans le cas d’une communication TCP, il est généralement profitable de permettre au serveur de gérer plusieurs connexions simultanées ; dans ce cas un nouveau processus sera créé pour chaque connexion ou bien les sockets seront enregistrées pour être utilisées dans un appel système select(2).
Côté client, la communication se fait tout d’abord en renseignant l’adresse du serveur à contacter. Ensuite peut avoir lieu l’envoi proprement dit de données ou la demande de connexion (selon le cas).
Ce document constituant seulement une introduction à la programmation par sockets, il est volontairement simplificateur sur la plupart des concepts et présente seulement les fonctions les plus importantes.
Création de socket (socket())
Attribution de l’adresse locale (bind())
Envoie d’un paquet (write() ou sendto())
attente de la réponse (read() ou recvfrom())
|
Création de socket (socket())
Attribution de l’adresse locale (bind())
Boucle (souvent infinie)
Lecture d’un datagramme (read() ou recvfrom())
[Traitement]
Envoie de la réponse (write() ou sendto())
|
La création d’une socket se fait par l’appel système socket(2) dont la
déclaration se trouve dans <sys/socket.h>
. Cet appel permet de créer
une structure en mémoire contenant tous les renseignements associés à
la socket (buffers, adresse, etc.) ; il renvoie un descripteur de
fichier permettant d’identifier la socket créée (-1 en cas d’erreur).
int socket ( int domain, /* AF_INET pour l'internet */ int type, /* SOCK_DGRAM pour une communication UDP, SOCK_STREAM pour une communication TCP */ int protocole /* 0 pour le protocole par defaut du type */ ); |
Une fois la socket créée, il est possible de lui attacher une adresse qui sera généralement l’adresse locale ; sans adresse une socket ne pourra pas être contactée (il s’agit simplement d’une structure qui ne peut pas être vue de l’extérieur). L’attachement permet de préciser l’adresse ainsi que le port de la socket. On attache une adresse à une socket à l’aide de la fonction bind(2) qui renvoie 0 en cas de succès et -1 sinon.
int bind ( int descr, /* descripteur de la socket */ struct sockaddr *addr, /* adresse a attacher */ int addr_size /* taille de l'adresse */ ); |
Cet exemple définit une fonction permettant de créer une socket et de l’attacher sur le port spécifié de l’hôte local.
/* ***************************************************************** * type : type de la socket a creer = SOCK_DGRAM ou SOCK_STREAM * port : numéro de port désiré pour l'attachement en local *******************************************************************/ int creer_socket (int type, int port) { int desc; int longueur=sizeof(struct sockaddr_in); struct sockaddr_in adresse; /* Creation de la socket */ if ((desc=socket(AF_INET,type,0)) == -1) { perror("Creation de socket impossible"); return -1; } /* Preparation de l'adresse d'attachement = adresse IP Internet */ adresse.sin_family=AF_INET; /* Indication de l'adresse IP locale de la socket */ /* Conversion (representation interne) -> (reseau) avec htonl et htons */ adresse.sin_addr.s_addr=htonl(INADDR_ANY); /* toutes les interfaces présentes */ /* Indication du port local de la socket */ /* si port = 0, l'adresse sera choisie au hasard par le système au dessus de 1024 */ adresse.sin_port=htons(port); /* Demande d'attachement de la socket */ if (bind(desc,(struct sockaddr*)&adresse,longueur) == -1) { perror("Attachement de la socket impossible"); close(desc); return -1; } /* Pour récupérer les informations d'attachement local on * peut utiliser la fonction getsockname(2) * * struct sockaddr_in adresse; * getsockname(desc,(struct sockaddr*)&adresse,&longueur); */ return desc; } |
Afin d’établir une communication UDP entre deux machines, il faut d’une part créer un serveur sur la machine réceptrice et d’autre part créer un client sur la machine émettrice. Ensuite la communication peut se faire à l’aide des fonctions sendto(2) et recvfrom(2). Chaque utilisation de sendto génère un paquet UDP qui doit être lu en une seule fois par la fonction recvfrom.
Il s’agit ici de créer la socket qui recevra le message. Ensuite on attend le message à l’aide de la fonction recvfrom.
int recvfrom ( int desc, /* descripteur de la socket */ void *message, /* adresse de reception */ int longueur, /* taille de la zone reservee */ int option, /* 0 pour une lecture simple */ struct sockaddr *ptr_adresse, /* adresse emetteur */ int *long_adresse /* taille de la zone adresse */ ); |
On illustre ici le côté serveur par la création d’un processus permettant la réception d’un unique message sur le port passé en argument.
[...] int main (int argc, char *argv[]) { struct sockaddr_in adresse; int port,desc_socket,lg=sizeof(adresse); char message[4096]; if (argc < 2) { fprintf(stderr,"udp_serveur num_socket\n"); return 1; } /* creation et attachement de la socket */ port=atoi(argv[1]); if ((desc_socket=creer_socket(SOCK_DGRAM, port)) == -1) { fprintf(stderr,"Creation de socket impossible\n"); exit(2); } /* attente du message */ recvfrom(desc_socket,message,4096,0,(struct sockaddr*)&adresse,&lg); /* &adresse contient les informations sur l'émetteur */ printf("Message : %s", message); close(desc_socket); return 0; } |
Il s’agit ici d’énvoyer un message sur une machine distante. Pour cela on commence par créer la socket émettrice, puis on prépare l’adresse de destination et on envoie le message à l’aide de la fonction sendto.
int sendto ( int desc, /* descripteur de la socket */ void *message, /* message a envoyer */ int longueur, /* longueur du message */ int option, /* 0 pour un envoi simple */ struct sockaddr *ptr_adresse, /* adresse destinataire */ int *long_adresse /* taille de la zone adresse */ ); |
On illustre ici le côté client par la creation d’un processus permettant l’énvoi du message "Salut" sur la machine et le port specifié en argument.
[...] int main (int argc, char *argv[]) { struct sockaddr_in adresse; int port,desc_socket,lg=sizeof(adresse); struct hostent *hp; char message[]="Salut\n"; if (argc < 3) { fprintf(stderr,"udp_client machine port_distant\n"); exit(1); } /* creation et attachement de la socket sur un port quelconque */ port=0; if ((desc_socket=creer_socket(SOCK_DGRAM, &port)) == -1) { fprintf(stderr,"Creation de socket impossible\n"); exit(2); } /* recherche de l'adresse internet du serveur */ if ((hp=gethostbyname(argv[1])) == NULL) { fprintf(stderr,"Machine %s inconnue\n",argv[1]); exit(3); } /* preparation de l'adresse destinatrice */ adresse.sin_family=AF_INET; adresse.sin_port=htons(atoi(argv[2])); memcpy(&adresse.sin_addr.s_addr,hp->h_addr,hp->h_length); /* envoi du message */ sendto(desc_socket,message,strlen(message)+1,0,(struct sockaddr*)&adresse,lg); close(desc_socket); return 0; } |
Afin d’établir une communication TCP entre deux machines, il faut d’une part créer un serveur sur la machine réceptrice, d’autre part créer un client sur la machine émettrice. Il faut ensuite réaliser une connexion entre les deux machines, qui sera gérée côté serveur par les fonctions listen(2) et accept(2), et côté client par la fonction connect(2). La communication peut alors se faire à l’aide des fonctions write(2) et read(2).
Il s’agit ici de créer la socket qui acceptera la connexion. On déclare alors la socket comme acceptant les connexions à l’aide de la fonction listen (retourne 0 en cas de succès).
int listen ( int desc, /* descripteur de la socket */ int nb_pendantes /* nombre maximal de connexions en attente */ ); |
Afin d’attendre une nouvelle demande de connexion, on utilise la fonction accept. A l’arrivée d’une nouvelle demande de connexion, cette fonction retourne un descripteur correspondant à une nouvelle socket créée pour l’occasion (ou -1 si une erreur s’est produite). La communication est alors possible par l’intermédiaire de cette dernière socket, qui s’utilise comme un fichier de caractères (ou un tube).
int accept ( int desc, /* descripteur de la socket */ struct sockaddr *ptr_adresse, /* adresse de lémeteur */ int *long_adresse /* taille de la zone adresse */ ); |
Si l’on souhaite alléger la charge du serveur, il est également possible de créer un nouveau processus gérant cette communication afin de permettre au serveur de retourner immédiatement à un état d’attente de demande de connexion.
On illustre ici le côté serveur par la création d’un processus permettant la réception de connexions sur le port passé en argument, et l’écho de tous les caractères envoyés lors d’une connexion.
[...] int main (int argc, char *argv[]) { struct sockaddr_in adresse; int port,socket_ecoute,socket_service,lg=sizeof(adresse); char car; if (argc < 2) { fprintf(stderr,"udp_serveur num_socket\n"); return 1; } /* creation et attachement de la socket */ port=atoi(argv[1]); if ((socket_ecoute=creer_socket(SOCK_STREAM, port)) == -1) { fprintf(stderr,"Creation de socket impossible\n"); exit(2); } /* declaration de l'ouverture du service */ if (listen(socket_ecoute, 10) == -1) { perror("Listen"); exit(1); } /* boucle de prise en charge des connexions */ /* on accepte ici qu'une seule connexion à la fois */ while (1) { socket_service=accept(socket_ecoute,(struct sockaddr*) &adresse, &lg); if (socket_service == -1) { perror("Accept"); exit(3); } /* la connexion est acceptee, on lit tout */ while (read(socket_service, &car, sizeof(car))) { write(socket_service, &car, sizeof(car)); if (car == 'x') break; } /* femeture de la socket créée par le accept(), la socket * socket_ecoute reste active */ close(socket_service); } } |
Il s’agit ici d’établir une connexion avec une machine distante afin de pouvoir communiquer par l’envoi de flux de caratères. Pour cela on commence par créer la socket émettrice, puis on prépare l’adresse de destination avant de faire la demande de connexion à l’aide de la fonction connect(2) (qui retourne -1 sur erreur).
int connect ( int desc, /* descripteur de la socket */ struct sockaddr *ptr_adresse, /* adresse du destinataire */ int *long_adresse /* taille de la zone adresse */ ); |
On illustre ici le côté client par la création d’un processus permettant une connexion puis l’envoi du message "salut" sur la machine et le port specifiés en argument. Ce client lit également les messages retournés par le serveur.
[...] int main (int argc, char *argv[]) { struct sockaddr_in adresse; int port,desc_socket,lg=sizeof(adresse); struct hostent *hp; char message[]="Salut\n",car; /* controle du nombre de parametres */ if (argc < 3) { fprintf(stderr,"tcp_client machine port\n"); exit(1); } /* creation et attachement de la socket sur un port quelconque */ port=0; if ((desc_socket=creer_socket(SOCK_STREAM, port)) == -1) { fprintf(stderr,"Creation de socket impossible\n"); exit(2); } /* recherche de l'adresse internet du serveur */ if ((hp = gethostbyname(argv[1])) == NULL) { fprintf(stderr,"Machine %s inconnue\n",argv[1]); exit(3); } /* preparation de l'adresse destinatrice */ adresse.sin_family=AF_INET; adresse.sin_port=htons(atoi(argv[2])); memcpy(&(adresse.sin_addr.s_addr), hp->h_addr, hp->h_length); /* demande de connexion au serveur */ if (connect(desc_socket,(struct sockaddr*) &adresse, lg) == -1) { perror ("Connect"); exit(4); } write(desc_socket, message, strlen(message)); car='x'; write(desc_socket, &car, sizeof(char)); while (read(desc_socket, &car, sizeof(char))) { printf("%c",car); } printf("\n"); close(desc_socket); return 0; } |
Il existe de nombreux autres cas d’utilisation des sockets ainsi qu’une multitude de façons d’utiliser et de combiner les fonctions d’accès au réseau. Les pages du manuel Unix (man) vous seront d’une aide précieuse pour aller au delà de ces simples exemples. Un des livres de référence pour la programmation réseau sous Unix est celui de Richard Stevens [].