3. Programme client UDP : talker

3.1. Utilisation des sockets avec le client UDP

Au niveau du client ou talker, l'objectif est d'ouvrir une nouvelle prise réseau ou socket ; ce qui revient à ouvrir un canal de communication réseau avec la fonction socket() après avoir défini les paramètres de l'hôte à contacter avec la fonction getaddrinfo().

Les bibliothèques standard associées au Langage C fournissent à la fois des sous-programmes et des définitions d'enregistrements (ou structures) de données. On rejoint ici le mode opératoire des langages orientés objet dont les bibliothèques fournissent des classes comprenant respectivement les définitions des méthodes et les attributs des données à manipuler.

Ainsi, la fonction getaddrinfo() manipule des enregistrements de type addrinfo. Pour utiliser cette fonction, on commence par orienter le choix du type de prise réseau à ouvrir avant de l'appeler en affectant certains champs de l'enregistrement hints puis on exploite les résultats contenus dans l'enregistrement servinfo après l'avoir appelée.

hints.ai_family = AF_INET1;
hints.ai_socktype = SOCK_DGRAM2;

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

1

AF_INET désigne la famille de protocole de couche réseau IPv4.

2

SOCK_DGRAM désigne un service de transmission de datagrammes non orienté connexion. Autrement dit, le protocole de couche transport UDP.

3

La chaîne de caractères msg contient le nom de l'hôte à contacter. Cet hôte peut être désigné directement par une adresse IP ou par son nom. Dans ce dernier cas, le resolver du service de noms de domaine (DNS) est sollicité automatiquement.

L'appel à la fonction socket() se fait sur la base des champs renseignés par la fonction getaddrinfo(). Le premier enregistrement de la chaîne pointée par servinfo contient tous les paramètres nécessaires.

if ((socketDescriptor = socket(servinfo->ai_family, servinfo->ai_socktype,
                               servinfo->ai_protocol)) == -1) {
    perror("socket:");
    exit(EXIT_FAILURE);
  }

Une fois le canal de communication réseau correctement ouvert, on peut passer à l'émission des datagrammes avec la fonction sendto.

while (strcmp(msg, ".")) {
  if ((msgLength = strlen(msg)) > 0) {
    // Envoi de la ligne au serveur
    if (sendto(socketDescriptor1, msg, msgLength2, 0,
               servinfo->ai_addr3, servinfo->ai_addrlen) == -1) {
      perror("sendto:");
      close(socketDescriptor);
      exit(EXIT_FAILURE);
    }

1

socketDescriptor désigne la prise réseau ou encore le canal de communication entre le programme de l'espace utilisateur et le sous-système réseau de l'espace noyau.

2

msg et msgLength correspondent au datagramme et à sa longueur. Ici, on émet des chaînes de caractères directement vers le correspondant réseau.

3

On utilise à nouveau les champs de l'enregistrement pointé par servinfo pour désigner l'hôte vers lequel les datagrammes sont émis.

Comme le service d'échange de datagrammes entre deux hôtes, n'est pas orienté connexion, le protocole UDP de la couche transport n'offre aucune garantie sur la délivrance de ces datagrammes. En conséquence, il incombe au programme utilisateur de fournir une forme de contrôle d'erreur. La solution généralement adoptée consiste à utiliser une temporisation d'attente de réponse. Dans notre cas, si aucune réponse du serveur ou listener n'a été reçue au bout d'une seconde, on peut considérer que le datagramme émis a été perdu.

C'est la fonction select() qui permet de superviser des descripteurs de flux tels que les prises réseau (socket).

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

if (select(socketDescriptor+1, &readSet, NULL, NULL, &timeVal)3) {
  // 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);
    }

1

readSet est réinitialisé à chaque itération. C'est cette variable qui sert à associer la prise réseau socketDescriptor à la fonction de supervision select().

2

timeVal est un enregistrement dont le champ tv_sec définit le temps d'attente de la réponse de l'hôte réseau vers lequel un datagramme a été précédemment émis.

3

select() renvoie une valeur supérieure à 0 en cas de succès : un datagramme de réponse est en attente moins d'une seconde après l'émission précédente.

Enfin, il ne reste plus que la réception du ou des datagrammes renvoyés par le serveur à l'aide de la fonction recv().

Pour toute information complémentaire sur les fonctions utilisées, consulter les pages de manuels correspondantes. Pour la fonction socket on peut utiliser man 2 socket ou man 7 socket par exemple.

3.2. Code source complet

Code du programme udp-talker.c :

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.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)

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

  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_INET;
  hints.ai_socktype = SOCK_DGRAM;

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

  if ((socketDescriptor = socket(servinfo->ai_family, servinfo->ai_socktype,
                                 servinfo->ai_protocol)) == -1) {
    perror("socket:");
    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.");
  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,
                 servinfo->ai_addr, servinfo->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);

  return EXIT_SUCCESS;
}