5. Client ou talker

Relativement aux conditions énoncées dans la section précédente (Tableau 2, « Protocole de couche réseau utilisé suivant les conditions de tests »), l'objectif ici est de présenter un programme pour chaque protocole de la couche transport qui fonctionne indifféremment dans les deux modes : dual stack avec IPv4 + IPv6 et single stack avec IPv4 uniquement.

Dans ce but, on reprend le code donné dans la Section 3, « Utilisation de getaddrinfo() ». Suivant le contenu des enregistrements de type addrinfo renseignés par l'appel à getaddrinfo(), le choix du protocole de couche réseau se fait automatiquement à partir de la configuration système. Sur un système dual stack l'enregistrement IPv6 est en première position alors que sur un système single stack, seul l'enregistrement IPv4 est disponible. Par conséquent, la boucle de scrutation des enregistrements s'arrête à l'ouverture de la première prise réseau (socket) valide.

Sur un système GNU/Linux, on consulte l'indicateur d'état disable_ipv6 à l'aide de l'instruction suivante.

$ cat /proc/sys/net/ipv6/conf/all/disable_ipv6
0

Dans l'infrastructure de test utilisée pour ce document, on a créé le fichier /etc/sysctl.d/disableipv6.conf sur le système vm3.fake.domain pour désactiver l'utilisation du protocole IPv6. Le contenu de ce fichier est le suivant.

$ cat /etc/sysctl.d/disableipv6.conf 
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1

Boucle de parcours des enregistrements addrinfo

<snipped/>

  p = servinfo;
  while((p != NULL)1 && !sockSuccess)2 {

    // Identification de la famille d'adresse
    if (p->ai_family == AF_INET)
      puts("Open IPv4 socket");
    else
      puts("Open IPv6 socket");

    if ((socketDescriptor = 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 // La prise réseau est valide
      sockSuccess = true;
  }

  if (p == NULL) {3
    fputs("Création de socket impossible", stderr);
    return 2;
  }

1

Dès que l'adresse du pointeur p vaut NULL, il n'y a plus d'enregistrement à examiner.

2

La variable booléenne sockSuccess est initialisée à la valeur false. Dès qu'une prise réseau a été ouverte avec succès, on arrête la scrutation des enregistrements.

3

Si, en sortie de la boucle de scrutation des enregistrements de type addrinfo l'adresse du pointeur p vaut NULL, aucune prise réseau n'a été ouverte. L'exécution du programme s'arrête là.

Les autres particularités du programme client UDP ou udp-talker sont décrites dans le support Initiation au développement C sur les sockets. Il faut simplement noter que la fonction select() est utilisée ici pour gérer une temporisation d'attente de la réponse du serveur. Le protocole de couche transport UDP n'est pas orienté connexion et n'offre aucun service de fiabilisation. Il incombe donc à la couche application d'assurer une forme de détection d'erreur. Dans notre cas, le client attend la réponse du traitement effectué par le serveur pendant une seconde. Si aucune réponse n'a été reçue dans le temps imparti, il faut considérer que le message émis a été perdu.

Code source complet du programme udp-talker.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.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)

int main()
{
  int socketDescriptor, status;
  unsigned int msgLength;
  struct addrinfo hints, *servinfo, *p;
  struct timeval timeVal;
  fd_set readSet;
  char msg[MSG_ARRAY_SIZE], serverPort[PORT_ARRAY_SIZE];
  bool sockSuccess = false;

  puts("Entrez le nom du serveur ou son adresse IP : ");
  memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
  scanf("%"xstr(MAX_MSG)"s", msg);

  puts("Entrez le numéro de port du serveur : ");
  memset(serverPort, 0, sizeof serverPort);  // Mise à zéro du tampon
  scanf("%"xstr(MAX_PORT)"s", serverPort);

  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_DGRAM;

  if ((status = getaddrinfo(msg, serverPort, &hints, &servinfo)) != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
    exit(EXIT_FAILURE);
  }

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

    // Identification de la famille d'adresse
    if (p->ai_family == AF_INET)
      puts("Open IPv4 socket");
    else
      puts("Open IPv6 socket");

    if ((socketDescriptor = 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 // La prise réseau est valide
      sockSuccess = true;
  }

  if (p == NULL) {
    fputs("Création de socket impossible", stderr);
    return 2;
  }

  puts("\nEntrez quelques caractères au clavier.");
  puts("Le serveur les modifiera et les renverra.");
  puts("Pour sortir, entrez une ligne avec le caractère '.' uniquement.");
  puts("Si une ligne dépasse "xstr(MAX_MSG)" caractères,");
  puts("seuls les "xstr(MAX_MSG)" premiers caractères seront utilisés.\n");

  // Invite de commande pour l'utilisateur et lecture des caractères jusqu'à la
  // limite MAX_MSG. Puis suppression du saut de ligne en mémoire tampon.
  puts("Saisie du message : ");
  memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
  scanf(" %"xstr(MAX_MSG)"[^\n]%*c", msg);

  // Arrêt lorsque l'utilisateur saisit une ligne ne contenant qu'un point
  while (strcmp(msg, ".")) {
    if ((msgLength = strlen(msg)) > 0) {
      // Envoi de la ligne au serveur
      if (sendto(socketDescriptor, msg, msgLength, 0,
                 p->ai_addr, p->ai_addrlen) == -1) {
        perror("sendto");
        close(socketDescriptor);
        exit(EXIT_FAILURE);
      }

      // Attente de la réponse pendant une seconde.
      FD_ZERO(&readSet);
      FD_SET(socketDescriptor, &readSet);
      timeVal.tv_sec = 1;
      timeVal.tv_usec = 0;

      if (select(socketDescriptor+1, &readSet, NULL, NULL, &timeVal)) {
        // Lecture de la ligne modifiée par le serveur.
        memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
        if (recv(socketDescriptor, msg, sizeof msg, 0) == -1) {
          perror("recv:");
          close(socketDescriptor);
          exit(EXIT_FAILURE);
        }

        printf("Message traité : %s\n", msg);
      }
      else {
        puts("Pas de réponse dans la seconde.");
      }
    }
    // Invite de commande pour l'utilisateur et lecture des caractères jusqu'à la
    // limite MAX_MSG. Puis suppression du saut de ligne en mémoire tampon.
    // Comme ci-dessus.
    puts("Saisie du message : ");
    memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
    scanf(" %"xstr(MAX_MSG)"[^\n]%*c", msg);
  }

  close(socketDescriptor);

  freeaddrinfo(servinfo);

  return 0;
}

Code source du correctif tcp-talker.c.patch

Le passage du protocole de couche transport UDP au protocole TCP ne présente aucune singularité relativement au document initial : Initiation au développement C sur les sockets. Le protocole TCP est orienté connexion et assure la fiabilisation des échanges de bout en bout. Le correctif suivant fait apparaître l'appel à la fonction connect() qui correspond à la phase d'établissement de la connexion. De plus, le recours à la temporisation d'attente de réponse devient inutile. Les variables de gestion du temps et l'appel à select() est donc supprimé.

--- udp-talker.c      2017-10-01 19:38:37.000000000 +0200
+++ tcp-talker.c        2016-10-25 16:22:31.000000000 +0200
@@ -22,8 +22,6 @@
   int socketDescriptor, status;
   unsigned int msgLength;
   struct addrinfo hints, *servinfo, *p;
-  struct timeval timeVal;
-  fd_set readSet;
   char msg[MSG_ARRAY_SIZE], serverPort[PORT_ARRAY_SIZE];
   bool sockSuccess = false;
 
@@ -37,7 +35,7 @@
 
   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_UNSPEC;
-  hints.ai_socktype = SOCK_DGRAM;
+  hints.ai_socktype = SOCK_STREAM;
 
   if ((status = getaddrinfo(msg, serverPort, &hints, &servinfo)) != 0) {
     fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
@@ -69,6 +67,12 @@
     return 2;
   }
 
+  if (connect(socketDescriptor, p->ai_addr, p->ai_addrlen) == -1) {
+    perror("connect");
+    close(socketDescriptor);
+    exit(EXIT_FAILURE);
+  }
+
   puts("\nEntrez quelques caractères au clavier.");
   puts("Le serveur les modifiera et les renverra.");
   puts("Pour sortir, entrez une ligne avec le caractère '.' uniquement.");
@@ -92,13 +96,6 @@
         exit(EXIT_FAILURE);
       }
 
-      // Attente de la réponse pendant une seconde.
-      FD_ZERO(&readSet);
-      FD_SET(socketDescriptor, &readSet);
-      timeVal.tv_sec = 1;
-      timeVal.tv_usec = 0;
-
-      if (select(socketDescriptor+1, &readSet, NULL, NULL, &timeVal)) {
         // Lecture de la ligne modifiée par le serveur.
         memset(msg, 0, sizeof msg);  // Mise à zéro du tampon
         if (recv(socketDescriptor, msg, sizeof msg, 0) == -1) {
@@ -106,13 +103,10 @@
           close(socketDescriptor);
           exit(EXIT_FAILURE);
         }
+    }
 
         printf("Message traité : %s\n", msg);
-      }
-      else {
-        puts("Pas de réponse dans la seconde.");
-      }
-    }
+
     // Invite de commande pour l'utilisateur et lecture des caractères jusqu'à la
     // limite MAX_MSG. Puis suppression du saut de ligne en mémoire tampon.
     // Comme ci-dessus.