Communication réseau par les sockets
Principes des sockets
1. Rôle des sockets
Les sockets (de l’anglais socket : prise, connecteur) sont une méthode de communication entre processus, en local ou à travers un réseau.
Les documentations en français emploient généralement le terme socket, sans le traduire. On trouve le plus souvent le mot au féminin, mais il est parfois au masculin. Nous suivons la traduction française du manuel Linux, une socket.
D’origine Unix BSD, les sockets sont devenues un standard de fait dans le monde Unix, avant d’être normalisées POSIX, puis complétées et intégrées au standard SUS. Elles sont implémentées sur de nombreux systèmes d’exploitation, y compris Windows (winsocks).
La version Linux des sockets est gérée par un seul appel système, socketcall(). Cependant, pour des raisons de facilité et de portabilité, il est fortement conseillé d’utiliser les fonctions de la bibliothèque sockets, intégrée dans la bibliothèque standard de gcc. Dans la suite du chapitre, nous considérerons ces fonctions comme des appels système unitaires.
2. Types de sockets
Les sockets peuvent gérer deux types d’échange de données, le mode stream (flot) et le mode datagramme.
a. Stream
Le mode stream (SOCK_STREAM) permet à deux processus d’établir...
Gestion des adresses, numéros de port, noms d’hôtes et de services
Une socket réseau est identifiée par deux valeurs : une adresse réseau, associée à la machine (host) où elle se trouve, et un numéro de port, qui la distingue des autres sockets de la machine. Ces deux valeurs forment l’adresse de la socket, et permettent d’identifier l’émetteur et le destinataire d’un message. Le format de l’adresse réseau diffère selon le domaine de communication, les numéros de ports ont le même format (un entier).
Pour faciliter la gestion des ressources réseau, il est fréquent d’associer des noms d’hôtes (hostname) aux adresses et des noms de services aux numéros de port. Il faut alors mettre en place des méthodes de résolution de nom, permettant de faire la relation entre les adresses ou numéros de port et les noms.
1. Représentation des adresses et des données
Les identifiants réseau, adresse et numéro de port, sont des nombres entiers, échangés entre machines à travers un réseau. Leur représentation interne peut être différente, en particulier en fonction de la famille de processeurs utilisée.
L’ordre des octets représentant un nombre entier dépend du processeur. On distingue les représentations big endian, où les octets de poids fort sont en premier, et little endian, où les octets de poids faible sont en premier.
Cette terminologie vient d’une analogie avec la consommation des œufs durs, selon que l’on entame la coquille par le gros bout (big endian) ou le petit (little endian), tirée d’un texte satirique de Jonathan Swift.
Les processeurs Intel de la famille x86 utilisent la représentation little endian. Cependant, les protocoles réseau ont normalisé une représentation des identifiants numériques réseau big endian.
La représentation interne des nombres par un système est appelée host byte order, la représentation réseau est appelée network byte order. Il existe différentes fonctions permettant de convertir un nombre selon ces différentes représentations.
Syntaxe
#include <arpa/inet.h>
uint16_t htons(uint16_t...
Sockets en mode stream
Les sockets réseau en mode stream s’appuient sur le protocole de transport TCP. Elles permettent à deux applications de communiquer à travers un réseau TCP/IP, en établissant une connexion bidirectionnelle identifiée par deux paires de valeurs Adresse IP/Numéro de port.
Les communications de ce type s’appuient en général sur un schéma client-serveur. Le serveur est en mode passif, il crée une socket et attend une demande de connexion envoyée vers son numéro de port, via une des adresses de la machine où il s’exécute. Le client est en mode actif, il envoie une demande de connexion vers une adresse et un numéro de port du serveur. Si le serveur accepte la demande, et que le client acquitte l’acceptation, la connexion est alors établie, via la création d’une nouvelle socket côté serveur. Cette socket est identifiée de façon unique par l’adresse IP et le numéro de port du serveur, l’adresse IP et le numéro de port du client. Cette connexion permet un échange de données bidirectionnel, en mode fiable.
Quand un des deux processus ne souhaite plus communiquer, il ferme sa socket, générant un signal à destination de l’autre processus qui peut fermer à son tour sa socket, terminant ainsi la connexion.
Côté serveur, on peut avoir un seul processus gérant la connexion avec un seul client à la fois (modèle itératif), ou avoir un processus serveur par client (modèle concurrent). Dans ce dernier cas, une fois que le serveur principal a établi une connexion sur une socket avec un client, il crée un processus enfant qui hérite de cette socket de connexion et la gère. Le serveur principal se remet alors immédiatement à l’écoute sur sa socket initiale et peut traiter la demande de connexion suivante.
1. Créer une socket : socket()
La fonction socket() permet de créer une socket, en spécifiant son domaine de communication et son type. Elle retourne un descripteur de fichier associé à la socket créée.
Syntaxe
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Arguments
domain |
Domaine... |
Sockets en mode datagramme
Les sockets réseau en mode datagramme s’appuient sur le protocole de transport UDP. Elles permettent à des applications de communiquer à travers un réseau TCP/IP, en s’échangeant des messages, l’émetteur et le destinataire étant identifiés par deux paires de valeurs Adresse IP/Numéro de port.
En général, les échanges de ce type s’appuient sur un schéma client-serveur. Le serveur est le plus souvent en mode passif, il crée une socket et attend un message envoyé vers son numéro de port, via une des adresses de la machine où il s’exécute. Le client est en mode actif, il envoie un message vers une adresse et un numéro de port. Comme le message contient l’adresse IP et le numéro de port de l’émetteur, le serveur peut répondre en lui envoyant un message.
Ce type d’échanges n’utilise pas de connexion. De plus, le protocole de transport UDP ne garantit pas la fiabilité de l’échange : les messages peuvent être perdus, dupliqués ou livrés dans le désordre.
Les principaux avantages des sockets en mode datagramme sont les suivants :
-
Rapidité, utilisation optimale de la bande passante : comme ils ne nécessitent pas de connexion, l’échange de données est immédiat et chaque message contient des données.
-
Multidestinataire : comme il n’y a pas de connexion, un même message peut concerner plusieurs destinataires (broadcast ou multicast).
Nous allons maintenant présenter les principaux appels système à utiliser pour une communication en mode datagramme. Certains sont les mêmes que ceux des sockets en mode stream, mais avec des paramètres différents. Nous présentons donc ici uniquement les spécificités du mode datagramme.
1. Créer une socket datagramme : socket()
Syntaxe
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Arguments
domain |
Domaine de communication : AF_INET, AF_INET6 |
type |
Type de socket : SOCK_DGRAM en mode datagramme |
protocol |
0 |
Valeur retournée
-1 |
Erreur, code erreur positionné dans la variable errno |
!= -1 |
Descripteur associé à la socket... |