SOCKETS - Kasimashi/Systemes-embarques GitHub Wiki

L’interface socket

Il y a quelques notions de base que vous devez connaître. Une connexion TCP/UDP est identifiée par un tuple de cinq valeurs :

{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}

Toute combinaison unique de ces valeurs identifie une connexion. Par conséquent, deux connexions ne peuvent pas avoir les mêmes cinq valeurs, sinon le système ne serait plus en mesure de distinguer ces connexions.

Définition

« La notion de socket a été introduite dans les distributions de Berkeley (un fameux système de type UNIX, dont beaucoup de distributions actuelles utilisent des morceaux de code), c’est la raison pour laquelle on parle parfois de sockets BSD (Berkeley Software Distribution). Il s’agit d’un modèle permettant la communication inter processus (IPC- Inter Process Communication) afin de permettre à divers processus de communiquer aussi bien sur une même machine qu’à travers un réseau TCP/IP. »

C'est une interface de programmation qui est très utilisé pour la communication réseau et qui est intégrée dans le noyau.

image

Utilisation

Le développeur utilisera donc concrètement une interface pour programmer une application TCP/IP grâce par exemple :

  • à l’API Socket BSD sous Unix/Linux ou
  • à l’API WinSocket sous Microsoft ©Windows

⚠️ Pour accéder aux documentations associés cela se fera donc par la commande : man 7 socket (Chapitre 7 socket) Pour Microsoft ©Windows, on pourra utiliser le service en ligne MSDN :

Modèle

Rappel : une socket est un point de communication par lequel un processus peut émettre et recevoir des données.

image

📝 Ce point de communication devra être relié à une adresse IP et un numéro de port dans le cas des protocoles Internet. Une socket est communément représentée comme un point d’entrée initial au niveau TRANSPORT modèle à couches DoD dans la pile de protocole.

Exemple de processus TCP et UDP

Couche transport

Rappel : la couche Transport est responsable du transport des messages complets de bout en bout (soit de processus à processus) au travers du réseau.

En programmation, si on utilise comme point d’entrée initial le niveau TRANSPORT, il faudra alors choisir un des deux protocoles de cette couche :

  • TCP(Transmission Control Protocol) est un protocole de transport fiable, en mode connecté (RFC 793).
  • UDP(User Datagram Protocol) est un protocole souvent décrit comme étant non-fiable, en mode non-connecté (RFC 768), mais plus rapide que TCP.

Numéro de ports

Rappel : un numéro de port sert à identifier un processus (l’application) en cours de communication par l’intermédiaire de son protocole de couche application (associé au service utilisé, exemple : 80 pour HTTP).

📝 Pour chaque port, un numéro lui est attribué (codé sur 16 bits), ce qui implique qu'il existe un maximum de 65 536 ports (216) par machine et par protocoles TCP et UDP.

L’attribution des ports est faite par le système d’exploitation, sur demande d’une application. Ici, il faut distinguer les deux situations suivantes :

  • cas d’un processus client : le numéro de port utilisé par le client sera envoyé au processus serveur. Dans ce cas, le processus client peut demander à ce que le système d’exploitation lui attribue n’importe quel port, à condition qu’il ne soit pas déjà attribué.
  • cas d’un processus serveur : le numéro de port utilisé par le serveur doit être connu du processus client. Dans ce cas, le processus serveur doit demander un numéro de port précis au système d’exploitation qui vérifiera seulement si ce numéro n’est pas déjà attribué.

📝 Une liste des ports dits réservés est disponible dans le fichier /etc/services sous Unix/Linux.

Caractéristiques des sockets

Rappel : les sockets compatibles BSD représentent une interface uniforme entre le processus utilisateur (user) et les piles de protocoles réseau dans le noyau (kernel) de l’OS.

Pour dialoguer, chaque processus devra préalablement créer une socket de communication en indiquant :

  • le domaine de communication : ceci sélectionne la famille de protocole à employer. Il faut savoir que chaque famille possède son adressage. Par exemple pour les protocoles Internet IPv4, on utilisera le domaine PF_INET ou AF_INET et AF_INET6 pour le protocole IPv6.

  • le type de socket à utiliser pour le dialogue. Pour PF_INET, on aura le choix entre : SOCK_STREAM (qui correspond à un mode connecté donc TCP par défaut), SOCK_DGRAM (qui correspond à un mode non connecté donc UDP) ou SOCK_RAW (qui permet un accès direct aux protocoles de la couche Réseau comme IP, ICMP, ...).

  • le protocole à utiliser sur la socket. Le numéro de protocole dépend du domaine de communication et du type de la socket. Normalement, il n’y a qu’un seul protocole par type de socket pour une famille donnée (SOCK_STREAM − →TCP et SOCK_DGRAM −→ UDP). Néanmoins, rien ne s’oppose à ce que plusieurs protocoles existent, auquel cas il est nécessaire de le spécifier (c’est la cas pour SOCK_RAW où il faudra préciser le protocole à utiliser).

⚠️ Une socket appartient à une famille. Il existe plusieurs types de sockets. Chaque famille possède son adressage.

Modèle Client-Serveur

En mode connecté (TCP)

image

En mode non connecté (UDP)

image

Les fonctions des sockets en détail

L'API socket()

Analogie : Décrocher le combiné du téléphone.

La création d'un socket se fait grâce à la fonction socket() :

int socket(famille,type,protocole)

  • famille représente la famille de protocole utilisé (AF_INET pour TCP/IP utilisant une adresse Internet sur 4 octets : l'adresse IP ainsi qu'un numéro de port afin de pouvoir avoir plusieurs sockets sur une même machine, AF_UNIX pour les communications UNIX en local sur une même machine)
  • type indique le type de service (orienté connexion ou non). Dans le cas d'un service orienté connexion (c'est généralement le cas), l'argument type doit prendre la valeur SOCK_STREAM (communication par flot de données). Dans le cas contraire (protocole UDP) le paramètre type doit alors valoir SOCK_DGRAM (utilisation de datagrammes, blocs de données)
  • protocole permet de spécifier un protocole permettant de fournir le service désiré. Dans le cas de la suite TCP/IP il n'est pas utile, on le mettra ainsi toujours à 0 La fonction socket() renvoie un entier qui correspond à un descripteur du socket nouvellement créé et qui sera passé en paramètre aux fonctions suivantes. En cas d'erreur, la fonction socket() retourne -1. Voici un exemple d'utilisation de la fonction socket() :

descripteur = socket(AF_INET,SOCK_STREAM,0);

L'API bind()

Analogie : ne fonctionne pas vraiment avec l'analogie téléphonique, mais cela est utilisé pour indiquer à l'API quel numéro de port votre socket utilisera sur la machine locale

Après création du socket, il s'agit de le lier à un point de communication défini par une adresse et un port, c'est le rôle de la fonction bind() :

bind(int descripteur,sockaddr localaddr,int addrlen)

  • descripteur représente le descripteur du socket nouvellement créé
  • localaddr est une structure qui spécifie l'adresse locale à travers laquelle le programme doit communiquer Le format de l'adresse est fortement dépendant du protocole utilisé :
    • l'interface socket définit une structure standard (sockaddr définie dans <sys/socket.h>) permettant de représenter une adresse :
struct sockaddr {
/* longueur effective de l'adresse */
u_char sa_len;

/* famille de protocole (généralement AF_INET) */
u_char sa_family;

/* l'adresse complète */
char sa_data[14];

}
  • sa_len est un octet (u_char) permettant de définir la longueur utile de l'adresse (la partie réellement utilisée de sa_data)
  • sa_family représente la famille de protocole (AF_INET pour TCP/IP)
  • sa_data est une chaîne de 14 caractères (au maximum) contenant l'adresse
  • La structure utilisée avec TCP/IP est une adresse AF_INET (Généralement les structures d'adresses sont redéfinies pour chaque famille d'adresse). Les adresses AF_INET utilisent une structure sockaddr_in définie dans <netinet/in.h> :
struct sockaddr_in {
/* famille de protocole (AF_INET) */
short sin_family;

/* numéro de port */
u_short sin_port;

/* adresse internet */
struct in_addr sin_addr;

char sin_zero[8];	/* initialise à zéro */
}
  - sin_family représente le type de famille
  - sin_port représente le port à contacter
  - sin_addr représente l'adresse de l'hôte
  - sin_zero[8] contient uniquement des zéros (étant donné que l'adresse IP et le port occupent 6 octets, les 8 octets restants doivent être à zéro)
- addrlen indique la taille du champ localaddr. On utilise généralement sizeof(localaddr).
Voici un exemple d'utilisation de la fonction bind() :
sockaddr_in localaddr ;
localaddr.sin_family = AF_INET; /* Protocole internet */
/* Toutes les adresses IP de la station */
localaddr.sin_addr.s_addr = htonl(INADDR_ANY);

/* port d'écoute par défaut au-dessus des ports réservés */
localaddr.sin_port = htons(port);
if (bind(		  listen_socket,
				   (struct sockaddr*)&localaddr,
				   sizeof(localaddr) )  == SOCKET_ERROR) {
		  // Traitement de l'erreur;

}

Le numéro fictif INADDR_ANY signifie que le socket peut-être associé à n'importe quelle adresse IP de la machine locale (s'il en existe plusieurs). Pour spécifier une adresse IP spécifique à utiliser, il est possible d'utiliser la fonction inet_addr() :

inet_addr("127.0.0.1");

/* utilisation de l'adresse de boucle locale */ Le socket peut être relié à un port libre quelconque en utilisant le numéro 0.

L'API listen()

Analogie : Activer la "sonnerie" de votre téléphone La fonction listen() permet de mettre un socket en attente de connexion.

⚠️ La fonction listen() ne s'utilise qu'en mode connecté (donc avec le protocole TCP)

int listen(int socket,int backlog)

  • socket représente le socket précédemment ouvert
  • backlog représente le nombre maximal de connexions pouvant être mises en attente

La fonction listen() retourne la valeur SOCKET_ERROR en cas de problème, sinon elle retourne 0. Voici un exemple d'utilisation de la fonction listen() :

if (listen(socket,10) == SOCKET_ERROR) {
// traitement de l'erreur
}

L'API accept()

Analogie : Répondre au téléphone

L'API connect()

Analogie : Composer le numéro de téléphone

La fonction connect() permet d'établir une connexion avec un serveur :

int connect(int socket,struct sockaddr * addr,int * addrlen)

  • socket représente le socket précédemment ouvert (le socket à utiliser)
  • addr représente l'adresse de l'hôte à contacter. Pour établir une connexion, le client ne nécessite pas de faire un bind()
  • addrlen représente la taille de l'adresse de l'hôte à contacter La fonction connect() retourne 0 si la connexion s'est bien déroulée, sinon -1. Voici un exemple d'utilisation de la fonction connect(), qui connecte le socket "s" du client sur le port port de l'hôte portant le nom serveur :
toinfo = gethostbyname(serveur);
toaddr = (u_long *)toinfo.h_addr_list[0];

/* Protocole internet */
to.sin_family = AF_INET;

/* Toutes les adresses IP de la station */
to.sin_addr.s_addr = toaddr;

/* port d'écoute par défaut au-dessus des ports réservés */
to.sin_port = htonl(port);

if (connect(socket,(struct sockaddr*)to,sizeof(to)) == -1) {
// Traitement de l'erreur;
}

L'API select()

Cette API est une fonction avancée pour les sockets et n'est pas obligatoire. Elle permet de monitorer l'activité du réseau/processus sur plusieurs descripteurs de fichiers.

Analogie : Le maître de classe d'école qui garde un oeil sur chacun de ses élèves en même temps.

L’appel-système select(), comme son homologue poll(), sert à attendre passivement qu’une condition soit réalisée sur un ou plusieurs descripteurs de fichier que l’on peut surveiller simultanément : La grande force de select() est que l’attente est passive : tant qu’aucune condition n’est réalisée, la tâche appelante est endormie. Elle peut néanmoins indiquer un délai maximal d’attente, un timeout, au-delà de laquelle elle sera automatiquement réveillée. Les conditions pour réveiller l'API sont les suivantes :

  • Une nouvelle connexion est établis avec un nouveau client
  • Des données d'un client déjà connecté sont reçus

⚠️ IMPORTANT : La fonction select est une fonction bloquante mais rend la main à la fin de son timeout.

image

Quand la fonction select se débloque, le serveur a besoin de vérifier s'il s'agit une nouvelle connexion, ou de données entrantes : Dans le dernier cas le serveur a besoin de trouver quel client a envoyé la donnée.

Pour cela on utilise la structure fd_set qui est un tableau de filedescriptor (fd). Ainsi que les macros ci dessous pour manipuler la structure.

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

Le premier paramètre n correspond à la valeur du plus grand descripteur de fichiers de vos ensembles + 1.

⚠️ L'appel système select a une limitation selon laquelle il ne peut surveiller qu'un nombre de descripteurs de fichiers inférieur à FD_SETSIZE.

Les ensembles :

  • readfds : les descripteurs seront surveillés pour voir si des données en lecture sont disponibles, un appel à recv par exemple, ne sera donc pas bloquant.
  • writefds : les descripteurs seront surveillés en écriture pour voir s'il y a de l'espace afin d'écrire les données, un appel à write ne sera donc pas bloquant.
  • exceptfds

Lorsque l'état de l'un des descripteurs change select retourne une valeur > 0, et les ensembles sont modifiés. Il faut par la suite vérifier l'état de chacun des descripteurs des ensembles via des macros présentées plus bas.

Afin de manipuler les ensembles des descripteurs, plusieurs macros sont à notre disposition.

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
  • FD_CLR supprime le descripteur fd de l'ensemble set.
  • FD_ISSET vérifie si le descripteur fd est contenu dans l'ensemble set après l'appel à select.
  • FD_SET ajoute le descripteur fd à l'ensemble set.
  • FD_ZERO vide l'ensemble set.

Example :

SOCKET sock;
[...];
fd_set readfs;

while(1)
{
   int ret = 0;
   FD_ZERO(&readfs);
   FD_SET(sock, &readfs);
   
   if((ret = select(sock + 1, &readfs, NULL, NULL, NULL)) < 0)
   {
      perror("select()");
      exit(errno);
   }

   /* 
   if(ret == 0)
   {
      ici le code si la temporisation (dernier argument) est écoulée (il faut bien évidemment avoir mis quelque chose en dernier argument).
   }    
   */
   
   if(FD_ISSET(sock, readfs))
   {
      /* des données sont disponibles sur le socket */
      /* traitement des données */
   }
}

L'API recv()

Analogie : écouter votre combiné téléphonique

La fonction recv() permet de lire dans un socket en mode connecté (TCP) :

int recv(int socket,char * buffer,int len,int flags)

  • socket représente le socket précédemment ouvert
  • buffer représente un tampon qui recevra les octets en provenance du client
  • len indique le nombre d'octets à lire
  • flags correspond au type de lecture à adopter :
    • le flag MSG_PEEK indiquera que les données lues ne sont pas retirées de la queue de réception
    • le flag MSG_OOB indiquera que les données urgentes (Out Of Band) doivent être lues
    • le flag 0 indique une lecture normale
    • La fonction recv() renvoie le nombre d'octets lus. De plus cette fonction bloque le processus jusqu'à ce qu'elle reçoive des données. Voici un exemple d'utilisation de la fonction recv() :
retour = recv(socket,Buffer,sizeof(Buffer),0 );
if (retour == SOCKET_ERROR) {
// traitement de l'erreur
}

⚠️La fonction recv est une fonction bloquante : votre application sera donc bloquée tant qu'il n'y a rien à lire sur le socket.

L'API send()

Analogie : Parler dans votre combiné téléphonique

La fonction send() permet d'écrire dans un socket (envoyer des données) en mode connecté (TCP) :

int send(int socket,char * buffer,int len,int flags)

  • socket représente le socket précédemment ouvert
  • buffer représente un tampon contenant les octets à envoyer au client
  • len indique le nombre d'octets à envoyer
  • flags correspond au type d'envoi à adopter :
    • le flag MSG_DONTROUTE indiquera que les données ne routeront pas
    • le flag MSG_OOB indiquera que les données urgentes (Out Of Band) doivent être envoyées
    • le flag 0 indique un envoi normal

La fonction send() renvoie le nombre d'octets effectivement envoyés. Voici un exemple d'utilisation de la fonction send() :

retour = send(socket,Buffer,sizeof(Buffer),0 );
if (retour == SOCKET_ERROR) {
// traitement de l'erreur
}

L'API close() et shutdown()

Analogie : Raccrocher le téléphone

La fonction close() permet la fermeture d'un socket en permettant au système d'envoyer les données restantes (pour TCP) :

int close(int socket) La fonction shutdown() permet la fermeture d'un socket dans un des deux sens (pour une connexion full-duplex) : int shutdown(int socket,int how)

  • Si how est égal à 0, le socket est fermé en réception
  • Si how est égal à 1, le socket est fermé en émission
  • Si how est égal à 2, le socket est fermé dans les deux sens

close() comme shutdown() retournent -1 en cas d'erreur, 0 si la fermeture se déroule bien.

Modifier les options de ses sockets

On peux modifier le comportement des trames du réseau avec les API getsockopt et setsockopt, les API fcntl ou encore ioctl

L'API getsockopt() et setsockopt()

Intéressons nous d'abord aux API : getsockopt et setsockopt :

#include  <sys/socket.h>

int  getsockopt ( int  sockfd ,  int  level ,  int  optname ,  void  * optval ,  socklen_t  * optlen ); 
int  setsockopt ( int  sockfd ,  int  level ,  int  optname ,  const  void  * optval  socklen_t  optlen );

/* Les deux renvoient : 0 si OK,–1 en cas d'erreur */

Arguments:

  • sockfd doit faire référence à un descripteur de socket ouvert.
  • level spécifie le code du système qui interprète l'option : le code socket général ou un code spécifique au protocole (par exemple, IPv4, IPv6, TCP ou SCTP).
  • optval est un pointeur vers une variable à partir de laquelle la nouvelle valeur de l'option est récupérée par setsockopt, ou dans laquelle la valeur actuelle de l'option est stockée par getsockopt. La taille de cette variable est spécifiée par l'argument final optlen , comme valeur pour setsockoptet comme valeur-résultat pour getsockopt.

Le tableau suivant répertorie les options de socket et de socket de couche IP pour getsockoptet setsockopt.

image

Les options de sockets

level optname get set Description Flag Datatype
SOL_SOCKET SO_BROADCAST x x Permit sending of broadcast datagrams x int
  SO_DEBUG x x Enable debug tracing x int
  SO_DONTROUTE x x Bypass routing table lookup x int
  SO_ERROR x   Get pending error and clear   int
  SO_KEEPALIVE x x Periodically test if connection still alive x int
  SO_LINGER x x Linger on close if data to send   linger{}
  SO_OOBINLINE x x Leave received out-of-band data inline x int
  SO_RCVBUF x x Receive buffer size   int
  SO_SNDBUF x x Send buffer size   int
  SO_RCVLOWAT x x Receive buffer low-water mark   int
  SO_SNDLOWAT x x Send buffer low-water mark   int
  SO_RCVTIMEO x x Receive timeout   timeval{}
  SO_SNDTIMEO x x Send timeout   timeval{}
  SO_REUSEADDR x x Allow local address reuse x int
  SO_REUSEPORT x x Allow local port reuse x int
  SO_TYPE x   Get socket type   int
  SO_USELOOPBACK x x Routing socket gets copy of what it sends x int
IPPROTO_IP IP_HDRINCL x x IP header included with data x int
  IP_OPTIONS x x IP header options   (see text)
  IP_RECVDSTADDR x x Return destination IP address x int
  IP_RECVIF x x Return destination IP address x int
  IP_TOS x x Type-of-service and precedence   int
  IP_TTL x x TTL   int
  IP_MULTICAST_IF x x Specify outgoing interface   in_addr{}
  IP_MULTICAST_TTL x x Specify outgoing TTL   u_char
  IP_MULTICAST_LOOP x x Specify loopback   u_char
  IP_{ADD,DROP}_MEMBERSHIP   x Join or leave multicast group   ip_mreq{}
  IP_{BLOCK,UNBLOCK}_SOURCE   x Block or unblock multicast source   ip_mreq_source{}
  IP_{ADD,DROP}_SOURCE_MEMBERSHIP   x Join or leave source-specific multicast   ip_mreq_source{}
IPPROTO_ICMPV6 ICMP6_FILTER x x Specify ICMPv6 message types to pass   icmp6_filter{}
IPPROTO_IPV6 IPV6_CHECKSUM x x Offset of checksum field for raw sockets   int
  IPV6_DONTFRAG x x Drop instead of fragment large packets x int
  IPV6_NEXTHOP x x Specify next-hop address   sockaddr_in6{}
  IPV6_PATHMTU x   Retrieve current path MTU   ip6_mtuinfo{}
  IPV6_RECVDSTOPTS x   Receive destination options x int
  IPV6_RECVHOPLIMIT x x Receive unicast hop limit x int
  IPV6_RECVHOPOPTS x x Receive hop-by-hop options x int
  IPV6_RECVPATHMTU x x Receive path MTU x int
  IPV6_RECVPKTINFO x x Receive packet information x int
  IPV6_RECVRTHDR x x Receive source route x int
  IPV6_RECVTCLASS x x Receive traffic class x int
  IPV6_UNICAT_HOPS x x Default unicast hop limit   int
  IPV6_USE_MIN_MTU x x Use minimum MTU x int
  IPV6_V6ONLY x x Disable v4 compatibility x int
  IPV6_XXX x x Sticky ancillary data   (see text)
  IPV6_MULTICAST_IF x x Specify outgoing interface   u_int
  IPV6_MULTICAST_HOPS x x Specify outgoing hop limit   int
  IPV6_MULTICAST_LOOP x x Specify loopback x u_int
  IPV6_JOIN_GROUP   x Join multicast group   ipv6_mreq{}
  IPV6_LEAVE_GROUP   x Leave multicast group   ipv6_mreq{}
IPPROTO_IP or IPPROTO_IPV6 MCAST_JOIN_GROUP   x Join multicast group   group_req{}
  MCAST_LEAVE_GROUP   x Leave multicast group   group_source_req{}
  MCAST_BLOCK_SOURCE   x Block multicast source   group_source_req{}
  MCAST_UNBLOCK_SOURCE   x Unblock multicast source   group_source_req{}
  MCAST_JOIN_SOURCE_GROUP   x Join source-specific multicast   group_source_req{}
  MCAST_LEAVE_SOURCE_GROUP   x Leave source-specific multicast   group_source_req{}
SO_REUSEADDR

Si SO_REUSEADDR est activé sur un socket avant de le lier, le socket peut être lié avec succès à moins qu'il n'y ait un conflit avec un autre socket lié exactement à la même combinaison d'adresse source et de port. Maintenant, vous vous demandez peut-être en quoi est-ce différent d’avant ? Le mot-clé est « exactement ». SO_REUSEADDR modifie principalement la façon dont les adresses génériques (« n'importe quelle adresse IP ») sont traitées lors de la recherche de conflits.

Sans SO_REUSEADDR, la liaison socketA puis 0.0.0.0:21la liaison socketB échoueront 192.168.0.1:21(avec l'erreur EADDRINUSE), puisque 0.0.0.0 signifie "n'importe quelle adresse IP locale", donc toutes les adresses IP locales sont considérées comme utilisées par ce socket et cela inclut 192.168.0.1également. Cela SO_REUSEADDR réussira, car 0.0.0.0et ne 192.168.0.1sont pas exactement la même adresse, l'un est un caractère générique pour toutes les adresses locales et l'autre est une adresse locale très spécifique. Notez que l'affirmation ci-dessus est vraie quel que soit l'ordre dans lequel socketA elles socketB sont liées ; sans SO_REUSEADDR elle, elle échouera toujours, avec SO_REUSEADDR elle, elle réussira toujours.

Pour vous donner un meilleur aperçu, faisons ici un tableau et listons toutes les combinaisons possibles :

SO_REUSEADDR socketA socketB Résultat
-------------------------------------------------- -------------------
  ON/OFF 192.168.0.1:21 192.168.0.1:21 Erreur (EADDRINUSE)
  MARCHE/ARRÊT 192.168.0.1:21 10.0.0.1:21 OK
  MARCHE/ARRÊT 10.0.0.1:21 192.168.0.1:21 OK
   DÉSACTIVÉ 0.0.0.0:21 192.168.1.0:21 Erreur (EADDRINUSE)
   DÉSACTIVÉ 192.168.1.0:21 0.0.0.0:21 Erreur (EADDRINUSE)
   ACTIVÉ 0.0.0.0:21 192.168.1.0:21 OK
   ACTIVÉ 192.168.1.0:21 0.0.0.0:21 OK
  ON/OFF 0.0.0.0:21 0.0.0.0:21 Erreur (EADDRINUSE)

Le tableau ci-dessus suppose qu'il socketA a déjà été lié avec succès à l'adresse donnée pour socketA, puis socketB qu'il est créé, qu'il est SO_REUSEADDR défini ou non, et enfin qu'il est lié à l'adresse donnée pour socketB. Result est le résultat de l'opération de liaison pour socketB. Si la première colonne indique ON/OFF, la valeur de SO_REUSEADDR n'a aucun rapport avec le résultat.

D'accord, SO_REUSEADDR cela a un effet sur les adresses génériques, bon à savoir. Mais ce n’est pas le seul effet qu’elle produit. Il existe un autre effet bien connu qui est également la raison pour laquelle la plupart des gens utilisent SO_REUSEADDR en premier lieu des programmes serveur. Pour l’autre utilisation importante de cette option, nous devons examiner de plus près le fonctionnement du protocole TCP.

Si un socket TCP est fermé, une négociation à trois est normalement effectuée ; la séquence s'appelle FIN-ACK. Le problème ici est que le dernier ACK de cette séquence peut être arrivé de l'autre côté ou ne pas être arrivé et seulement si c'est le cas, l'autre côté considère également le socket comme étant complètement fermé. Pour empêcher la réutilisation d'une combinaison adresse + port, qui peut encore être considérée comme ouverte par un homologue distant, le système ne considérera pas immédiatement un socket comme mort après l'envoi du dernier, mais placera plutôt le ACK socket dans un état communément appelé TIME_WAIT. Il peut rester dans cet état pendant quelques minutes (paramètre dépendant du système). Sur la plupart des systèmes, vous pouvez contourner cet état en activant la attente et en définissant un temps d'attente de zéro1, mais il n'y a aucune garantie que cela soit toujours possible, que le système honorera toujours cette demande, et même si le système l'honore, cela provoque le socket à fermer par une réinitialisation ( RST), ce qui n'est pas toujours une bonne idée. Pour en savoir plus sur le temps d'attente, jetez un œil à ma réponse sur ce sujet .

La question est, comment le système traite-t-il une socket dans l'état TIME_WAIT? Si SO_REUSEADDR n'est pas défini, une socket dans l'état TIME_WAIT est considérée comme toujours liée à l'adresse et au port source et toute tentative de liaison d'une nouvelle socket à la même adresse et au même port échouera jusqu'à ce que la socket soit réellement fermée. Ne vous attendez donc pas à pouvoir relier l’adresse source d’un socket immédiatement après sa fermeture. Dans la plupart des cas, cela échouera. Cependant, si SO_REUSEADDR est défini pour le socket que vous essayez de lier, un autre socket lié à la même adresse et au même port dans l'état TIME_WAIT est simplement ignoré, après tout, il est déjà "à moitié mort", et votre socket peut se lier exactement à la même adresse sans aucun problème. Dans ce cas, le fait que l’autre socket ait exactement la même adresse et le même port ne joue aucun rôle. Notez que lier un socket exactement à la même adresse et au même port qu'un socket en train de mourir TIME_WAIT peut avoir des effets secondaires inattendus et généralement indésirables dans le cas où l'autre socket est toujours "au travail", mais cela dépasse le cadre de cette réponse et heureusement, ces effets secondaires sont plutôt rares en pratique.

Il y a une dernière chose que vous devriez savoir SO_REUSEADDR. Tout ce qui est écrit ci-dessus fonctionnera tant que la socket à laquelle vous souhaitez vous lier a la réutilisation des adresses activée. Il n'est pas nécessaire que l'autre socket, celle qui est déjà liée ou qui est dans un TIME_WAIT état, ait également cet indicateur défini lorsqu'elle a été liée. Le code qui décide si la liaison réussira ou échouera inspecte uniquement l' SO_REUSEADDR indicateur du socket entré dans l' bind()appel, pour toutes les autres sockets inspectées, cet indicateur n'est même pas examiné.

SO_REUSEPORT

SO_REUSEPORT c'est ce à quoi la plupart des gens s'attendraient SO_REUSEADDR. Fondamentalement, SO_REUSEPORT cela vous permet de lier un nombre arbitraire de sockets exactement à la même adresse source et au même port, à condition que toutes les sockets liées précédemment aient également été SO_REUSEPORT définies avant d'être liées. Si la première socket liée à une adresse et à un port n'est pas SO_REUSEPORT définie, aucune autre socket ne peut être liée exactement à la même adresse et au même port, que cette autre socket soit définie SO_REUSEPORT ou non, jusqu'à ce que la première socket libère à nouveau sa liaison. Contrairement au cas où SO_REUSEADDR la gestion du code SO_REUSEPORT vérifiera non seulement que le socket actuellement lié est SO_REUSEPORT défini, mais également que le socket avec une adresse et un port en conflit était SO_REUSEPORT défini lorsqu'il a été lié.

SO_REUSEPORT n'implique pas SO_REUSEADDR. Cela signifie que si un socket n'était pas SO_REUSEPORT défini lorsqu'il a été lié et qu'un autre socket a été SO_REUSEPORT défini lorsqu'il est lié exactement à la même adresse et au même port, la liaison échoue, ce qui est attendu, mais elle échoue également si l'autre socket est déjà en train de mourir et est en TIME_WAIT état. Pour pouvoir lier un socket aux mêmes adresses et au même port qu'un autre socket dans TIME_WAIT l'état, il faut soit SO_REUSEADDR qu'il soit défini sur ce socket, soit SO_REUSEPORT qu'il doit avoir été défini sur les deux sockets avant de les lier. Bien sûr, il est permis de définir les deux, SO_REUSEPORT et SO_REUSEADDR, sur un socket.

Il n'y a pas grand chose à dire à SO_REUSEPORT ce sujet, à part qu'il a été ajouté plus tard que SO_REUSEADDR, c'est pourquoi vous ne le trouverez pas dans de nombreuses implémentations de sockets d'autres systèmes, qui ont "forké" le code BSD avant que cette option ne soit ajoutée, et qu'il n'y avait pas moyen de lier deux sockets exactement à la même adresse de socket dans BSD avant cette option.

SO_LINGER

Une autre option de socket couramment appliquée est l’option SO_LINGER. Cette option diffère de l'option SO_REUSEADDR dans la mesure où la structure de données utilisée n'est pas un simple type de données int. Le but de l'option SO_LINGER est de contrôler la façon dont le socket est arrêté lorsque la fonction close(2) est appelée. Cette option s'applique uniquement aux protocoles orientés connexion tels que TCP.

Le comportement par défaut du noyau est de permettre à la fonction close(2) de revenir immédiatement à l'appelant. Toutes les données TCP/IP non envoyées seront transmises et livrées si possible, mais aucune garantie n'est donnée. Étant donné que l'appel close(2) renvoie immédiatement le contrôle à l'appelant, l'application n'a aucun moyen de savoir si le dernier bit de données a réellement été transmis.

L'option SO_LINGER peut être activée sur le socket, pour que l'application se bloque lors de l'appel close(2) jusqu'à ce que toutes les données finales soient transmises à l'extrémité distante. De plus, cela garantit le l'appelant que les deux extrémités ont reconnu un arrêt normal du socket. A défaut, le délai d'attente de l'option indiqué se produit et une erreur est renvoyée à l'application appelante.

Les modes de fonctionnement de SO_LINGER sont contrôlés par la structure linger :

struct  linger
{
    int l_onoff;        /* option on/off */
    int l_linger;       /* linger time */
};

Le membre l_onoff agit comme une valeur booléenne, où une valeur différente de zéro indique VRAI et zéro indique FAUX. Les trois variantes de cette option sont précisées comme suit :

  • La définition de l_onoff sur FALSE entraîne l'ignorance du membre l_linger et le comportement de fermeture par défaut (2) implicite. Autrement dit, l'appel close(2) reviendra immédiatement à l'appelant et toutes les données en attente seront transmises si possible.
  • La définition de l_onoff sur TRUE rend la valeur du membre l_linger significative. Lorsque l_linger est différent de zéro, cela représente le temps en secondes pendant lequel le délai d'attente doit être appliqué au moment de close(2) (l'appel close(2) "s'attardera"). Si les données en attente et la fermeture réussie se produisent avant l'expiration du délai, un retour réussi a lieu. Sinon, un retour d'erreur se produit et errno est défini sur la valeur de EWOULDBLOCK.
  • Définir l_onoff sur TRUE et définir l_linger sur zéro entraîne l'abandon de la connexion et toutes les données en attente sont immédiatement supprimées à la fermeture (2).

Vous avez probablement intérêt à écrire vos applications de manière à ce que l'option SO_LINGER soit activée et qu'un délai d'attente raisonnable soit fourni. Ensuite, la valeur de retour de close(2) peut être testée pour voir si la connexion a été mutuellement fermée avec succès. Si une erreur est renvoyée, cela indique à votre application qu'il est probable que l'application distante n'a pas pu recevoir toutes les données que vous avez envoyées. Alternativement, cela peut simplement signifier que des problèmes sont survenus lorsque la connexion a été fermée (après que les données ont été reçues avec succès par l'homologue).

Vous devez cependant être conscient que s'attarder sur certaines conceptions de serveurs créera de nouveaux problèmes. Lorsque l'option SO_LINGER est configurée pour s'attarder lors de close(2), cela empêchera les autres clients d'être servis pendant que l'exécution de votre serveur s'attarde dans l'appel de fonction close(2). Ce problème existe si vous servez plusieurs clients au sein d'un même processus (généralement un serveur qui utilise select(2) ou poll(2)). Utiliser le comportement par défaut pourrait être plus approprié car il permettra à close(2) de revenir immédiatement. Toutes les données écrites en attente seront toujours fournies par le noyau, s'il en est capable.

Enfin, l'utilisation du comportement d'abandon (mode numéro 3 répertorié précédemment) est appropriée si l'application ou le serveur sait que la connexion doit être interrompue. Cela peut être appliqué lorsque le serveur a déterminé qu'une personne sans privilège d'accès tente d'accéder. Le client dans cette situation ne mérite aucun soin particulier et donc un minimum de frais généraux est dépensé pour éliminer le coupable.

L'API ioctl

Abréviation de Input Output Controler : elle permet de modifier le comportement de périphériques comme les périphériques liés au socket

#include <sys/ioctl.h>
ioctl( int s, int cmd, int *arg )

En général, ioctl renvoie 0 s'il réussit, ou -1 s'il échoue. Certaines requêtes ioctl utilisent la valeur de retour comme paramètre de sortie, et renvoient une valeur positive si elles réussissent (et -1 pour les erreurs).

Les macros et constantes symboliques décrivant les requêtes ioctl() se trouvent dans le fichier <sys/ioctl.h>.

Programmation socket TCP (Linux)

L’échange entre un client et un serveur TCP peut être schématisé de la manière suivante:

image

Pour illustrer nos propos il faudra donc écrire deux codes : le code serveur et le code client.

Pour les deux codes il faudra : Pour créer une socket on utilisera l'appel système socket(). (Se référencer au man de socket)

Le prototype de la fonction est la suivante int socket(int domain, int type, int protocol);

  • On souhaite créer une socket dans le domaine IPV4 il faudra donc utiliser AF_INET
  • On souhaite créer une socket TCP il faudra donc utiliser une socket du type : SOCK_STREAM
  • On souhaite créer une socket TCP il faudra donc utiliser une socket avec le protocol : IPPROTO_TCP

On écrira alors : socketDescriptor = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

Ensuite les appels connect() côté client et bind()/accept() coté serveur sont basés sur l'utilisation d'une structure d'adresse pour identifier les bouts de communications.

Il faut savoir que l’adressage des processus (local et distant) dépend du domaine de communication (cad la famille de protocole employée). Ici, nous avons choisi ledomaine AF_INET pour les protocoles InternetIPv4. Dans cette famille, un processus sera identifié par:

  • Une adresse IPV4
  • Un numéro de port

L'interface socket propose une structure d'adresse générique :

struct sockaddr
{
  unsigned short int sa_family;
  unsigned char sa_data[14];
}

Le domaine AF_IINET utilise une structure compatible :

/* Structure used to store an IPv4 address */
struct in_addr
{
    u_long s_addr; /* 32bit netid/hostid address in network byte order */
};

struct sockaddr_in
{
    u_char          sin_family; /* AF_INET */
    u_short         sin_port;   /* Numero de port */
    struct  in_addr sin_addr;   /* IPV4 Addr : 32bit netid/hostid in network byte order */
    char            sin_zero[8];/* unused : ajustement pour être compatible avec sockaddr */
};

Il suffit donc d’initialiser une structure sockaddr_in :

  • pour le client : avec les informations distantes du serveur (adresse IPv4 et numéro de port) pour l’appel connect().
  • pour le serveur : avec les informations locales du serveur (adresse IPv4 et numéro de port) pour l’appel bind().

Pour écrire ces informations dans la structure d’adresse, il faudra utiliser :

  • inet_aton() pour convertir une adresse IP depuis la notation IPv4 décimale pointée vers une forme binaire (dans l’ordre d’octet du réseau)
  • htons() pour convertir le numéro de port (sur 16 bits) depuis l’ordre des octets de l’hôte vers celui du réseau

⚠️ L'ordre des octets du réseau est en fait big-endian. Il est donc plus prudent d'appeler des fonctions qui respectent cet ordre pour coder des informations dans les en-têtes des protocoles réseaux.

On rappelle qu’une communication TCP est bidirectionnelle full duplex et orientée flux d’octets. Il faut donc des fonctions pour écrire (envoyer) et lire (recevoir) des octets dans la socket. Les fonctions d’échanges de données sur une socket TCP sont :

  • read() et write() qui permettent la réception et l’envoi d’octets sur un descripteur de socket
  • recv() et send() qui permettent la réception et l’envoi d’octets sur un descripteur de socket avec un paramètre flags

⚠️ Les appels recv() et send() sont spéciques aux sockets en mode connecté. La seule différence avec read() et write() est la présence de flags (cf. man 2 send).

Évidemment, un serveur TCP a lui aussi besoin de créer une socket SOCK_STREAM dans le domaine AF_INET. Mis à part cela, le code source d’un serveur TCP basique est très différent d’un client TCP dans le principe. On rappelle qu’un serveur TCP attend des demandes de connexion en provenance de processus client. Le processus client doit connaître au moment de la connexion le numéro de port d’écoute du serveur. Pour mettre en oeuvre cela, le serveur va utiliser l’appel système bind() qui va lui permettre de lier sa socket d’écoute à une interface et à un numéro de port local à sa machine . Il suffit donc d’initialiser une structure sockaddr_in avec les informations locales du serveur (adresse IPv4 et numéro de port).

⚠️ Normalement il faudrait indiquer l'adresse IPv4 de l'interface locale du serveur qui acceptera les demandes de connexions. Il est ici possible de préciser avec INADDR_ANY que toutes les interfaces locales du serveur accepteront les demandes de connexion des clients.

Maintenant que le serveur a créé et attaché une socket d’écoute, il doit la placer en attente passive, c’est-à-dire capable d’accepter les demandes de connexion des processus clients. Pour cela, on va utiliser l’appel système listen() :

⚠️ Si la file est pleine, le serveur sera dans une situation de DOS (Deny Of Service) car il ne peut plus traiter les nouvelles demandes de connexion.

Cette étape est cruciale pour le serveur. Il lui faut maintenant accepter les demandes de connexion en provenance des processus client. Pour cela, il va utiliser l’appel système accept().

⚠️ Explication : imaginons qu'un client se connecte à notre socket d'écoute. L'appel accept() va retourner une nouvelle socket connectée au client qui servira de socket de dialogue. La socket d'écoute reste inchangée et peut donc servir à accepter des nouvelles connexions.

Le principe est simple mais un problème apparaît pour le serveur : comment dialoguer avec le client connecté et continuer à attendre des nouvelles connexions? Il y a plusieurs solutions à ce problème notamment la programmation multi-tâche car ici le serveur a besoin de paralléliser plusieurs traitements.

Programmation socket UDP (Linux)

Socket UDP

Pour créer une socket on utilisera l'appel système socket(). (Se référencer au man de socket)

Le prototype de la fonction est la suivante int socket(int domain, int type, int protocol);

  • On souhaite créer une socket dans le domaine IPV4 il faudra donc utiliser AF_INET
  • On souhaite créer une socket TCP il faudra donc utiliser une socket du type : SOCK_DGRAM
  • On souhaite créer une socket TCP il faudra donc utiliser une socket avec le protocol : IPPROTO_UDP

Maintenant que nous avons créé une socket UDP, le client pourrait déjà communiquer avec un serveur UDP car nous utilisons un mode non-connecté.

Un échange en UDP

On va tout d’abord attacher cette socket à une interface et à un numéro de port local de sa machine en utilisant utiliser l’appel système bind(). Cela revient à créer un point de rencontre local pour le client. Les fonctions d’échanges de données sur une socket UDP sont recvfrom() et sendto() qui permettent la réception et l’envoi d’octets sur un descripteur de socket en mode non-connecté.

⚠️ Les appels recvfrom() et sendto() sont spécifiques aux sockets en mode non-connecte. ls utiliseront en argument une structure sockaddr_in pour PF_INET.

Le code source d’un serveur UDP basique est très similaire à celui d’un client UDP. Évidemment, un serveur UDP a lui aussi besoin de créer une socket SOCK_DGRAM dans le domaine PF_INET. Puis, il doit utiliser l’appel système bind() pour lier sa socket d’écoute à une interface et à un numéro de port local à sa machine car le processus client doit connaître et fournir au moment de l’échange ces informations. Il suffit donc d’initialiser une structure sockaddr_in avec les informations locales du serveur (adresse IPv4 et numéro de port).

⚠️ Il est ici possible de préciser avec INADDR_ANY que toutes les interfaces locales du serveur accepteront les échanges des clients.

⚠️ **GitHub.com Fallback** ⚠️