7. Serveur ou listener socket double

Comme dans les deux sections précédentes, 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 deux prises réseau (socket) ; une par protocole de couche réseau.

La boucle de parcours des enregistrements de type addrinfo est modifiée de façon à ce qu'une prise réseau soit ouverte par famille de protocole de couche réseau. Il est donc nécessaire de franchir les étapes suivantes avec succès pour les deux protocoles IPv4 et IPv6.

  • Pour le protocole IPv4

    1. Appel à la fonction socket().

    2. Appel à la fonction bind().

  • Pour le protocole IPv6

    1. Appel à la fonction socket().

    2. Appel à la fonction setsockopt() pour appliquer l'option bindv6only=1 qui rend la prise réseau (socket) dédiée au protocole de couche réseau IPv6.

    3. Appel à la fonction bind().

Par souci de cohérence avec le code du programme précédent, on utilise le même indicateur booléen sockSuccess comme condition de sortie de la boucle de parcours des enregistrements addrinfo.

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
     IPv4: 0.0.0.0
     IPv6: ::
    Attente de requête sur le port 5000
    >>  Depuis [2001:db8:feb2:10::12]:51708
    >>  Message reçu : Message UDP émis depuis le système vm2.fake.domain
    >>  Depuis [192.0.2.13]:55801
    >>  Message reçu : Message UDP émis depuis le système vm3.fake.domain

    Les deux clients apparaissent avec leurs adresses IP respectives. Le système vm2.fake.domain a utilisé la prise réseau dédiée au protocole IPv6 tandis que le système vm3.fake.domain a utilisé l'autre prise réseau dédiée au protocole IPv4.

    Comme dans les exemples précédents, les informations relatives à l'ouverture des deux prises réseau (sockets) sont obtenues à 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
    udp        0      0 *:5000                  *:*                       10953/udp-listener.
    udp6       0      0 [::]:5000               [::]:*                    10953/udp-listener.
    $ lsof -i
    COMMAND     PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    udp-liste 10953  etu    3u  IPv4  63543      0t0  UDP *:5000 
    udp-liste 10953  etu    4u  IPv6  63544      0t0  UDP *:5000
    $ ss -lup
    State      Recv-Q Send-Q    Local Address:Port    Peer Address:Port   
    UNCONN     0      0          *:5000                *:*        users:(("udp-listener.o",10953,3))
    UNCONN     0      0         :::5000               :::*        users:(("udp-listener.o",10953,4))
  • 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 UDP émis depuis le système vm2.fake.domain
    Message traité : MESSAGE UDP éMIS DEPUIS LE SYSTèME 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 UDP émis depuis le système vm3.fake.domain
    Message traité : MESSAGE UDP éMIS DEPUIS LE SYSTèME 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)

enum IPVERSION {
 v4, v6
};

// 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[2], 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];
  int optval = 1; // socket double et IPV6_V6ONLY à 1
  bool sockSuccess = false;
  struct timeval timeVal;
  fd_set readSet[2];

  listenSocket[v4] = listenSocket[v6] = -1;

  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_UNSPEC; // IPv6 et IPv4
  hints.ai_socktype = SOCK_DGRAM;
  hints.ai_flags = AI_PASSIVE; // use my IP

  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 d'une «prise» IPv4 et d'une «prise» IPv6
  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);
      // conversion de l'adresse IP en une chaîne de caractères
      inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
      printf(" IPv4: %s\n", ipstr);

      if ((listenSocket[v4] = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
        perror("socket:"); // Echec ouverture socket
      }

      else if (bind(listenSocket[v4], p->ai_addr, p->ai_addrlen) == -1) {
        close(listenSocket[v4]);
        perror("bind:");
        listenSocket[v4] = -1; // Echec socket en écoute
      }
    }
    else { // IPv6
      struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
      addr = &(ipv6->sin6_addr);
      // conversion de l'adresse IP en une chaîne de caractères
      inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
      printf(" IPv6: %s\n", ipstr);

      if ((listenSocket[v6] = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {
        perror("socket:"); // Echec ouverture socket
      }

      else if (setsockopt(listenSocket[v6], IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof optval) == -1) {
        perror("setsockopt:");
        listenSocket[v6] = -1; // Echec option bindv6only=1
      }

      else if (bind(listenSocket[v6], p->ai_addr, p->ai_addrlen) == -1) {
        close(listenSocket[v6]);
        perror("bind:");
        listenSocket[v6] = -1; // Echec socket en écoute
      }
    }

    if ((listenSocket[v4] != -1) && (listenSocket[v6] != -1)) // deux prises ouvertes
      sockSuccess = true;
    else
      p = p->ai_next; // Enregistrement d'adresse suivant
  }

  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);

  // Utilisation de select en mode scrutation
  timeVal.tv_sec = 0;
  timeVal.tv_usec = 0;

  while (1) {

    FD_ZERO(&readSet[v4]);
    FD_SET(listenSocket[v4], &readSet[v4]);
    FD_ZERO(&readSet[v6]);
    FD_SET(listenSocket[v6], &readSet[v6]);

    if (select(listenSocket[v4]+1, &readSet[v4], NULL, NULL, &timeVal) == -1) { // IPv4
      perror("select:");
      exit(EXIT_FAILURE);
    }

    if (select(listenSocket[v6]+1, &readSet[v6], NULL, NULL, &timeVal) == -1) { // IPv6
      perror("select:");
      exit(EXIT_FAILURE);
    }

    // Mise à zéro du tampon de façon à connaître le délimiteur
    // de fin de chaîne.
    memset(msg, 0, sizeof msg);

    if (FD_ISSET(listenSocket[v4], &readSet[v4])) // IPv4
      if ((recv = recvfrom(listenSocket[v4], msg, sizeof msg, 0, (struct sockaddr *) &clientAddress,
                           &clientAddressLength)) == -1) {
        perror("recvfrom:");
        exit(EXIT_FAILURE);
      }

    if (FD_ISSET(listenSocket[v6], &readSet[v6])) // IPv6
      if ((recv = recvfrom(listenSocket[v6], 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 (clientAddress.ss_family == AF_INET) { // IPv4
        if (sendto(listenSocket[v4], msg, msgLength, 0, (struct sockaddr *) &clientAddress,
                   clientAddressLength) == -1) {
          perror("sendto:");
          exit(EXIT_FAILURE);
        }
      }
      else { // IPv6
        if (sendto(listenSocket[v6], 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

Relativement aux correctifs des sections précédentes, celui-ci est plus important. En effet, si le principe de détection d'évènement sur les deux prises réseau (sockets) est identique au programme UDP ci-dessus, l'appel à la fonction accept() est bloquant. Le code de traitement de réception et d'émission de chaîne de caractères a été dupliqué pour chaque prise réseau.

--- udp-listener.c    2016-10-25 16:22:31.469862376 +0200
+++ tcp-listener.c      2016-10-25 16:22:31.469862376 +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
@@ -41,7 +42,7 @@
 
 int main() {
 
-  int listenSocket[2], status, recv, i;
+  int listenSocket[2], connectSocket, status, i;
   unsigned short int msgLength;
   struct addrinfo hints, *servinfo, *p;
   struct sockaddr_storage clientAddress;
@@ -61,7 +62,7 @@
 
   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_UNSPEC; // IPv6 et IPv4
-  hints.ai_socktype = SOCK_DGRAM;
+  hints.ai_socktype = SOCK_STREAM;
   hints.ai_flags = AI_PASSIVE; // use my IP
 
   if ((status = getaddrinfo(NULL, listenPort, &hints, &servinfo)) != 0) {
@@ -135,6 +136,19 @@
   timeVal.tv_sec = 0;
   timeVal.tv_usec = 0;
 
+  // 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[v4], BACKLOG) == -1) {
+    perror("listen");
+    exit(EXIT_FAILURE);
+  }
+
+  if (listen(listenSocket[v6], BACKLOG) == -1) {
+    perror("listen:");
+    exit(EXIT_FAILURE);
+  }
+
   while (1) {
 
     FD_ZERO(&readSet[v4]);
@@ -152,56 +166,84 @@
       exit(EXIT_FAILURE);
     }
 
+    if (FD_ISSET(listenSocket[v4], &readSet[v4])) { // IPv4
+      // Appel bloquant en attente d'une nouvelle connexion
+      // connectSocket est la nouvelle prise utilisée pour la connexion active
+      if ((connectSocket = accept(listenSocket[v4], (struct sockaddr *) &clientAddress,
+                                 &clientAddressLength)) == -1) {
+        perror("accept:");
+        close(listenSocket[v4]);
+        exit(EXIT_FAILURE);
+      }
+
+      // Affichage de l'adresse IP du client.
+      inet_ntop(clientAddress.ss_family, get_in_addr((struct sockaddr *)&clientAddress),
+                ipstr, sizeof ipstr);
+      printf(">>  connecté à [%s]:", ipstr);
+
+      // Affichage du numéro de port du client.
+      printf("%hu\n", ntohs(get_in_port((struct sockaddr *)&clientAddress)));
+
     // 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]);
 
-    if (FD_ISSET(listenSocket[v4], &readSet[v4])) // IPv4
-      if ((recv = recvfrom(listenSocket[v4], msg, sizeof msg, 0, (struct sockaddr *) &clientAddress,
-                           &clientAddressLength)) == -1) {
-        perror("recvfrom:");
+          // Renvoi de la ligne convertie au client.
+          if (send(connectSocket, msg, msgLength, 0) == -1) {
+            perror("send:");
+            close(listenSocket[v4]);
         exit(EXIT_FAILURE);
       }
 
-    if (FD_ISSET(listenSocket[v6], &readSet[v6])) // IPv6
-      if ((recv = recvfrom(listenSocket[v6], msg, sizeof msg, 0, (struct sockaddr *) &clientAddress,
+          memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
+        }
+    }
+
+    if (FD_ISSET(listenSocket[v6], &readSet[v6])) { // IPv6
+      // Appel bloquant en attente d'une nouvelle connexion
+      // connectSocket est la nouvelle prise utilisée pour la connexion active
+      if ((connectSocket = accept(listenSocket[v6], (struct sockaddr *) &clientAddress,
                            &clientAddressLength)) == -1) {
-        perror("recvfrom:");
+        perror("accept:");
+        close(listenSocket[v6]);
         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 (clientAddress.ss_family == AF_INET) { // IPv4
-        if (sendto(listenSocket[v4], msg, msgLength, 0, (struct sockaddr *) &clientAddress,
-                   clientAddressLength) == -1) {
-          perror("sendto:");
-          exit(EXIT_FAILURE);
-        }
-      }
-      else { // IPv6
-        if (sendto(listenSocket[v6], msg, msgLength, 0, (struct sockaddr *) &clientAddress,
-                   clientAddressLength) == -1) {
-          perror("sendto:");
+          if (send(connectSocket, msg, msgLength, 0) == -1) {
+            perror("send:");
+            close(listenSocket[v6]);
           exit(EXIT_FAILURE);
         }
+
+          memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
       }
     }
+
   }
 
   // jamais atteint

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

Excepté le mode connecté, le comportement des programmes est à nouveau 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
     IPv4: 0.0.0.0
     IPv6: ::
    Attente de requête sur le port 5000
    >>  connecté à [2001:db8:feb2:10::12]:32906
      --  Message TCP émis depuis le système vm2.fake.domain
    >>  connecté à i[192.0.2.13]:40625
      --  Message TCP émis depuis le système vm3.fake.domain

    Les informations relatives à l'ouverture des deux prises réseau (sockets) et aux connexions sont obtenues à l'aide des commandes netstat, lsof et ss.

    $ $ 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.)
    tcp        0      0 *:5000                  *:*                     LISTEN      10964/tcp-listener.
    tcp6       0      0 [::]:5000               [::]:*                  LISTEN      10964/tcp-listener.
    tcp6       0      0 vm1.fake.domain:5000    vm2.fake.domain:32913   ESTABLISHED 10964/tcp-listener.
    $ lsof -i
    lsof -i
    COMMAND     PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    tcp-liste 10964  etu    3u  IPv4  63617      0t0  TCP *:5000 (LISTEN)
    tcp-liste 10964  etu    4u  IPv6  63618      0t0  TCP *:5000 (LISTEN)
    tcp-liste 10964  etu    5u  IPv6  63619      0t0  TCP vm1.fake.domain:5000->vm2.fake.domain:32913 (ESTABLISHED)
    $ ss -tre
    State      Recv-Q Send-Q     Local Address:Port         Peer Address:Port   
    ESTAB      0      0        vm1.fake.domain:5000      vm2.fake.domain:32913    uid:1000 ino:63619 sk:ffff88001b94c800
    ESTAB      0      0        vm1.fake.domain:ssh    cooper.fake.domain:60445    timer:(keepalive,52min,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 TCP émis depuis le système vm2.fake.domain
    Message traité : MESSAGE TCP éMIS DEPUIS LE SYSTèME 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 TCP émis depuis le système vm3.fake.domain
    Message traité : MESSAGE TCP éMIS DEPUIS LE SYSTèME VM3.FAKE.DOMAIN
    Saisie du message :
    .