6. Serveur ou listener socket unique

Comme dans le cas du programme client, l'utilisation des programmes présentés dans cette section est définie dans le Tableau 2, « Protocole de couche réseau utilisé suivant les conditions de tests ». On propose un programme pour chaque protocole de la couche transport qui utilise une prise réseau (socket) unique indépendante du protocole de couche réseau utilisé.

Dans ce but, on reprend à nouveau la boucle de parcours des enregistrements de type addrinfo à la suite de l'appel à la fonction getaddrinfo(). La boucle de parcours s'interrompt dès qu'une prise réseau (socket) est ouverte avec succès. Ce principe est donc identique à celui adopté pour le programme client ou talker (voir Section 5, « Client ou talker »). Le code présenté ci-dessous se distingue du précédent par le nombre d'étapes à franchir avec succès pour confirmer l'ouverture de la prise réseau (socket).

  1. Appel à la fonction socket().

  2. Appel à la fonction setsockopt() si la famille de socket utilisée correspond au protocole IPv6. C'est grâce à cet appel que l'on applique l'option bindv6only=0 qui assure la prise en charge transparente des deux protocoles de couche réseau.

  3. Appel à la fonction bind().

Exemple d'exécution des programmes udp-listener et udp-talker

  • Copie d'écran côté serveur ou listener.

    $ ./udp-listener.o 
    Entrez le numéro de port utilisé en écoute (entre 1500 et 65000) : 
    5000
     IPv6: ::
    Attente de requête sur le port 5000
    >>  Depuis [2001:db8:feb2:10::12]:54272
    >>  Message reçu : Message émis depuis vm2.fake.domain
    >>  Depuis [::ffff:192.0.2.13]:51254
    >>  Message reçu : message émis depuis vm3.fake.domain

    Comme indiqué dans le Tableau 2, « Protocole de couche réseau utilisé suivant les conditions de tests », le client vm2.fake.domain apparaît avec son adresse IPv6 tandis que le client vm4.fake.domain apparaît avec une adresse IPv6 établie par correspondance avec son adresse IPv4 (IPv4-mapped IPv6 address).

    Les informations relatives à l'ouverture de la prise réseau (socket) au niveau système peuvent être obtenues au moins de trois façon différentes à l'aide des commandes netstat, lsof et ss.

    $ netstat -aup
    (Tous les processus ne peuvent être identifiés, les infos sur les processus
    non possédés ne seront pas affichées, vous devez être root pour les voir toutes.)
    Connexions Internet actives (serveurs et établies)
    Proto Recv-Q Send-Q Adresse locale    Adresse distante   Etat   PID/Program name
    udp6       0      0 [::]:5000         [::]:*                    10839/udp-listener.
    $ lsof -i
    COMMAND     PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    udp-liste 10839  etu    3u  IPv6  62111      0t0  UDP *:5000
    $ ss -lup
    State      Recv-Q Send-Q    Local Address:Port    Peer Address:Port   
    UNCONN     0      0         :::5000               :::*        users:(("udp-listener.o",10839,3))
  • Copie d'écran côté client ou talker dual stack ; hôte vm2.fake.domain.

    $ ./udp-talker.o 
    Entrez le nom du serveur ou son adresse IP : 
    vm1.fake.domain
    Entrez le numéro de port du serveur : 
    5000
    Open IPv6 socket
    
    Entrez quelques caractères au clavier.
    Le serveur les modifiera et les renverra.
    Pour sortir, entrez une ligne avec le caractère '.' uniquement.
    Si une ligne dépasse 80 caractères,
    seuls les 80 premiers caractères seront utilisés.
    
    Saisie du message : 
    Message émis depuis vm2.fake.domain
    Message traité : MESSAGE éMIS DEPUIS VM2.FAKE.DOMAIN
    Saisie du message : 
    .
  • Copie d'écran côté client ou talker single stack IPv4 ; hôte vm3.fake.domain.

    $ ./udp-talker.o 
    Entrez le nom du serveur ou son adresse IP : 
    vm1.fake.domain
    Entrez le numéro de port du serveur : 
    5000
    Open IPv4 socket
    
    Entrez quelques caractères au clavier.
    Le serveur les modifiera et les renverra.
    Pour sortir, entrez une ligne avec le caractère '.' uniquement.
    Si une ligne dépasse 80 caractères,
    seuls les 80 premiers caractères seront utilisés.
    
    Saisie du message : 
    message émis depuis vm3.fake.domain
    Message traité : MESSAGE éMIS DEPUIS VM3.FAKE.DOMAIN
    Saisie du message : 
    .

Code source complet du programme udp-listener.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <stdbool.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define MAX_PORT 5
#define PORT_ARRAY_SIZE (MAX_PORT+1)
#define MAX_MSG 80
#define MSG_ARRAY_SIZE (MAX_MSG+1)
// Utilisation d'une constante x dans la définition
// du format de saisie
#define str(x) # x
#define xstr(x) str(x)

// extraction adresse IPv4 ou IPv6:
void *get_in_addr(struct sockaddr *sa) {
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

// extraction numéro de port
unsigned short int get_in_port(struct sockaddr *sa) {
    if (sa->sa_family == AF_INET) {
        return ((struct sockaddr_in*)sa)->sin_port;
    }

    return ((struct sockaddr_in6*)sa)->sin6_port;
}

int main() {

  int listenSocket, status, recv, i;
  unsigned short int msgLength;
  struct addrinfo hints, *servinfo, *p;
  struct sockaddr_storage clientAddress;
  socklen_t clientAddressLength = sizeof clientAddress;
  void *addr;
  char msg[MSG_ARRAY_SIZE], listenPort[PORT_ARRAY_SIZE], ipstr[INET6_ADDRSTRLEN], ipver;
  int optval = 0; // socket unique et IPV6_V6ONLY à 0
  bool sockSuccess = false;

  memset(listenPort, 0, sizeof listenPort);  // Mise à zéro du tampon
  puts("Entrez le numéro de port utilisé en écoute (entre 1500 et 65000) : ");
  scanf("%"xstr(MAX_PORT)"s", listenPort);

  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_INET6;      // IPv6 et IPv4 mappées
  hints.ai_socktype = SOCK_DGRAM;  // UDP
  hints.ai_flags = AI_PASSIVE;     // Toutes les adresses disponibles

  if ((status = getaddrinfo(NULL, listenPort, &hints, &servinfo)) != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
    return 1;
  }

  // Scrutation des résultats et création de socket
  // Sortie après création de la première «prise»
  p = servinfo;
  while((p != NULL) && !sockSuccess) {

    // Identification de l'adresse courante
    if (p->ai_family == AF_INET) { // IPv4
      struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
      addr = &(ipv4->sin_addr);
      ipver = '4';
    }
    else { // IPv6
      struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
      addr = &(ipv6->sin6_addr);
      ipver = '6';
    }

    // Conversion de l'adresse IP en une chaîne de caractères
    inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
    printf(" IPv%c: %s\n", ipver, ipstr);

    if ((listenSocket = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
      perror("socket:");
      sockSuccess = false; // Echec ouverture socket
      p = p->ai_next;      // Enregistrement d'adresse suivant
    }

    else if ((p->ai_family == AF_INET6) && // IPv6 uniquement
             (setsockopt(listenSocket, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof optval) == -1)) {
      close(listenSocket);
      perror("setsockopt:");
      sockSuccess = false; // Echec option bindv6only=0
      p = p->ai_next;      // Enregistrement d'adresse suivant
    }

    else if (bind(listenSocket, p->ai_addr, p->ai_addrlen) == -1) {
      close(listenSocket);
      perror("bind:");
      sockSuccess = false; // Echec socket en écoute
      p = p->ai_next;      // Enregistrement d'adresse suivant
    }

    else // La prise est bien ouverte
      sockSuccess = true;
  }

  if (p == NULL) {
    fputs("Création de socket impossible\n", stderr);
    exit(EXIT_FAILURE);
  }

  // Libération de la mémoire occupée par les enregistrements
  freeaddrinfo(servinfo);

  printf("Attente de requête sur le port %s\n", listenPort);

  while (1) {

    // Mise à zéro du tampon de façon à connaître le délimiteur
    // de fin de chaîne.
    memset(msg, 0, sizeof msg);
    if ((recv = recvfrom(listenSocket, msg, sizeof msg, 0,
                         (struct sockaddr *) &clientAddress,
                         &clientAddressLength)) == -1) {
      perror("recvfrom:");
      exit(EXIT_FAILURE);
    }

    if ((msgLength = strlen(msg)) > 0) {
      // Affichage de l'adresse IP du client.
      inet_ntop(clientAddress.ss_family, get_in_addr((struct sockaddr *)&clientAddress),
                ipstr, sizeof ipstr);
      printf(">>  Depuis [%s]:", ipstr);

      // Affichage du numéro de port du client.
      printf("%hu\n", ntohs(get_in_port((struct sockaddr *)&clientAddress)));

      // Affichage de la ligne reçue
      printf(">>  Message reçu : %s\n", msg);

      // Conversion de cette ligne en majuscules.
      for (i = 0; i < msgLength; i++)
        msg[i] = toupper(msg[i]);

      // Renvoi de la ligne convertie au client.
      if (sendto(listenSocket, msg, msgLength, 0, (struct sockaddr *) &clientAddress,
                 clientAddressLength) == -1) {
        perror("sendto:");
        exit(EXIT_FAILURE);
      }
    }
  }

  // Jamais atteint
  return 0;
}

Code source du correctif tcp-listener.c.patch

Toujours comme dans le cas du programme client ou talker, le passage du protocole de couche transport UDP au protocole TCP ne présente aucune modification de fond dans la gestion de prise réseau (socket). En revanche, le protocole TCP est orienté connexion et l'échange d'information est toujours précédé de l'établissement de la connexion. Côté client, on fait appel à la fonction connect() et côté serveur on fait appel à la fonction accept().

--- udp-listener.c    2016-10-25 16:22:31.000000000 +0200
+++ tcp-listener.c      2016-10-25 16:22:31.000000000 +0200
@@ -12,6 +12,7 @@
 #define PORT_ARRAY_SIZE (MAX_PORT+1)
 #define MAX_MSG 80
 #define MSG_ARRAY_SIZE (MAX_MSG+1)
+#define BACKLOG 5
 // Utilisation d'une constante x dans la définition
 // du format de saisie
 #define str(x) # x
@@ -37,7 +38,7 @@
 
 int main() {
 
-  int listenSocket, status, recv, i;
+  int listenSocket, connectSocket, status, i;
   unsigned short int msgLength;
   struct addrinfo hints, *servinfo, *p;
   struct sockaddr_storage clientAddress;
@@ -53,7 +54,7 @@
 
   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_INET6;      // IPv6 et IPv4 mappées
-  hints.ai_socktype = SOCK_DGRAM;  // UDP
+  hints.ai_socktype = SOCK_STREAM; // TCP
   hints.ai_flags = AI_PASSIVE;     // Toutes les adresses disponibles
 
   if ((status = getaddrinfo(NULL, listenPort, &hints, &servinfo)) != 0) {
@@ -115,42 +116,54 @@
   // Libération de la mémoire occupée par les enregistrements
   freeaddrinfo(servinfo);
 
-  printf("Attente de requête sur le port %s\n", listenPort);
+  // Attente des requêtes des clients.
+  // Appel non bloquant et passage en mode passif
+  // Demandes d'ouverture de connexion traitées par accept
+  if (listen(listenSocket, BACKLOG) == -1) {
+    perror("listen");
+    exit(EXIT_FAILURE);
+  }
 
   while (1) {
+    printf("Attente de connexion sur le port %s\n", listenPort);
 
-    // Mise à zéro du tampon de façon à connaître le délimiteur
-    // de fin de chaîne.
-    memset(msg, 0, sizeof msg);
-    if ((recv = recvfrom(listenSocket, msg, sizeof msg, 0,
+    // Appel bloquant en attente d'une nouvelle connexion
+    // connectSocket est la nouvelle prise utilisée pour la connexion active
+    if ((connectSocket = accept(listenSocket, 
                          (struct sockaddr *) &clientAddress,
                          &clientAddressLength)) == -1) {
-      perror("recvfrom:");
+      perror("accept:");
+      close(listenSocket);
       exit(EXIT_FAILURE);
     }
 
-    if ((msgLength = strlen(msg)) > 0) {
       // Affichage de l'adresse IP du client.
       inet_ntop(clientAddress.ss_family, get_in_addr((struct sockaddr *)&clientAddress),
                 ipstr, sizeof ipstr);
-      printf(">>  Depuis [%s]:", ipstr);
+    printf(">>  connecté à [%s]:", ipstr);
 
       // Affichage du numéro de port du client.
       printf("%hu\n", ntohs(get_in_port((struct sockaddr *)&clientAddress)));
 
-      // Affichage de la ligne reçue
-      printf(">>  Message reçu : %s\n", msg);
+    // Mise à zéro du tampon de façon à connaître le délimiteur
+    // de fin de chaîne.
+    memset(msg, 0, sizeof msg);
+    while (recv(connectSocket, msg, sizeof msg, 0) > 0) 
+      if ((msgLength = strlen(msg)) > 0) {
+        printf("  --  %s\n", msg);
 
       // Conversion de cette ligne en majuscules.
       for (i = 0; i < msgLength; i++)
         msg[i] = toupper(msg[i]);
 
       // Renvoi de la ligne convertie au client.
-      if (sendto(listenSocket, msg, msgLength, 0, (struct sockaddr *) &clientAddress,
-                 clientAddressLength) == -1) {
-        perror("sendto:");
+        if (send(connectSocket, msg, msgLength, 0) == -1) {
+          perror("send:");
+          close(listenSocket);
         exit(EXIT_FAILURE);
       }
+
+        memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
     }
   }
 

Exemple d'exécution des programmes tcp-listener et tcp-talker

En faisant abstraction des modes de fonctionnement des deux protocoles de couche transport, on constate que le comportement des programmes est identique.

  • Copie d'écran côté serveur ou listener.

    $ ./tcp-listener.o 
    Entrez le numéro de port utilisé en écoute (entre 1500 et 65000) : 
    5000
     IPv6: ::
    Attente de connexion sur le port 5000
    >>  connecté à [2001:db8:feb2:10::12]:32892
      --  Message émis depuis vm2.fake.domain
    Attente de connexion sur le port 5000
    >>  connecté à [::ffff:192.0.2.13]:40622
      --  message émis depuis vm3.fake.domain
    Attente de connexion sur le port 5000

    Tout comme avec le protocole UDP, les informations relatives à l'ouverture de la prise réseau (socket) au niveau système peuvent être obtenues au moins de trois façon différentes à l'aide des commandes netstat, lsof et ss. À la différence du cas précédent, les résultats font apparaître la connexion active depuis le poste client ou talker.

    $ netstat -atp |grep 5000
    (Tous les processus ne peuvent être identifiés, les infos sur les processus
    non possédés ne seront pas affichées, vous devez être root pour les voir toutes.)
    tcp6       0      0 [::]:5000               [::]:*                  LISTEN      10897/tcp-listener.
    tcp6       0      0 vm1.fake.domain:5000    vm2.fake.domain:32912   ESTABLISHED 10897/tcp-listener.
    $ lsof -i
    COMMAND     PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    tcp-liste 10897  etu    3u  IPv6  62332      0t0  TCP *:5000 (LISTEN)
    tcp-liste 10897  etu    4u  IPv6  62333      0t0  TCP vm1.fake.domain:5000->vm2.fake.domain:32912 (ESTABLISHED)
    $ ss -tre
    State      Recv-Q Send-Q    Local Address:Port        Peer Address:Port   
    ESTAB      0      0         vm1.fake.domain:5000      vm2.fake.domain:32912    uid:1000 ino:62333 sk:ffff880003e977c0
    ESTAB      0      0         vm1.fake.domain:ssh       cooper.fake.domain:60445   timer:(keepalive,84min,0) ino:60796 sk:ffff88001f042080
  • Copie d'écran côté client ou talker dual stack ; hôte vm2.fake.domain.

    $ ./tcp-talker.o 
    Entrez le nom du serveur ou son adresse IP : 
    vm1.fake.domain
    Entrez le numéro de port du serveur : 
    5000
    Open IPv6 socket
    
    Entrez quelques caractères au clavier.
    Le serveur les modifiera et les renverra.
    Pour sortir, entrez une ligne avec le caractère '.' uniquement.
    Si une ligne dépasse 80 caractères,
    seuls les 80 premiers caractères seront utilisés.
    
    Saisie du message : 
    Message émis depuis vm2.fake.domain
    Message traité : MESSAGE éMIS DEPUIS VM2.FAKE.DOMAIN
    Saisie du message : 
    .
  • Copie d'écran côté client ou talker single stack IPv4 ; hôte vm3.fake.domain.

    $ ./tcp-talker.o 
    Entrez le nom du serveur ou son adresse IP : 
    vm1.fake.domain
    Entrez le numéro de port du serveur : 
    5000
    Open IPv4 socket
    
    Entrez quelques caractères au clavier.
    Le serveur les modifiera et les renverra.
    Pour sortir, entrez une ligne avec le caractère '.' uniquement.
    Si une ligne dépasse 80 caractères,
    seuls les 80 premiers caractères seront utilisés.
    
    Saisie du message : 
    message émis depuis vm3.fake.domain
    Message traité : MESSAGE éMIS DEPUIS VM3.FAKE.DOMAIN
    Saisie du message : 
    .