Login

Mode datagramme

Nous allons tout d'abord voir, dans ce document, comment mettre en œuvre un dialogue entre un client et un serveur en mode datagramme. Le schéma ci-dessous illustre les différentes étapes que doivent accomplir un serveur (à droite) et un client (à gauche) pour pouvoir échanger des données en utilisant le protocole UDP. 

Étape 0 : bibliothèques à inclure

Pour utiliser les fonctions de la bibliothèque socket, il est nécessaire d'importer une ou plusieurs bibliothèques de fonctions qui contiennent les primitives que nous utiliserons ci-dessous. 

En Langage C

#include <sys/socket.h>
#include <netdb.h> 
#include <unistd.h>

En Langage Java

import java.net.*;
import java.io.*;

En Langage Python

import socket

 Vous pourrez avoir besoin d'inclure d'autres bibliothèques, notamment stdio.h et string.h en langage C, pour afficher et manipuler les résultats.

étape 1 : création des descripteurs de la communication

La première étape à réaliser, aussi bien par le serveur que par le client, est de créer une structure ou un objet qui va identifier la communication pour y faire référence par la suite. Un programme peut ouvrir de multiples connexions simultanément et il est, par conséquent, nécessaire d'être capable de les distinguer. 

En langage C et en Python, nous devrons faire appel à la fonction socket qui renvoie un nombre entier qui servira d'identifiant de la communication. Cette fonction prend plusieurs arguments : le premier est le type de socket (PF_INET signifie que l'on souhaite créer un canal de communication TCP/IP et SOCK_DGRAM que l'on souhaite une communication en mode datagramme). En Java, on crée un objet appartenant à la classe DatagramSocket qui est directement configuré de la bonne manière.  

En Langage C

int s = socket (PF_INET, SOCK_DGRAM, 0);

En Langage Java

DatagramSocket snew DatagramSocket();

En Langage Python

s = socket.socket (socket.AF_INET, socket.SOCK_DGRAM)

Une fois cette étape réalisée, la variable s nous permettra de faire référence au canal de communication que l'on aura créé. C'est cette variable s que l'on va passer aux différentes fonctions pour mettre en œuvre les étapes de notre communication.

ÉTAPE 2 : réservation du port (coté serveur seul)

Le serveur, avant de pouvoir accepter des communications, devra demander au système d'exploitation de lui transmettre tous les datagrammes qui lui parviendront sur le port de communication choisi. Pour cela, il faudra réserver ce port auprès du système. Il est aussi possible de limiter l'attente à une adresse IP lorsque l'on est sur une machine en possédant plusieurs. Cette étape, nécessaire, passe par une fonction généralement appelée bind et spécifique au serveur. Les paramètres de cette fonction, selon les langages, peuvent lui être passés directement (Python) ou par l'intermédiaire d'une structure (C) ou d'un objet (Java).  

En Langage C

// Structure contenant l'adresse et le port sur lesquels écouter
//     Type d'adresse ; AF_INET = IPv4
//     Adresse du récepteur - INADDR_ANY = n'importe quelle interface 
//     Port sur lequel écouter
struct sockaddr_in myAddress;
myAddress.sin_family      = AF_INET;
myAddress.sin_addr.s_addr = htonl(INADDR_ANY);
myAddress.sin_port        = htons(12345);

// Enregistrement au niveau de l'OS
//     Paramètre 1 : descripteur de connexion
//     Paramètre 2 & 3 : adresse et taille de l'adresse
bind(s, (struct sockaddr *)&myAddress, sizeof(myAddress));

En Langage Java

// Objet représentant l'adresse et le numéro de port sur lesquels écouter
//     Paramètre 1 : Adresse IP sur laquelle écouter (null pour toutes)
//     Paramètre 2 : port sur lequel écouter
InetSocketAddress myAddress = new InetSocketAddress((InetAddress)null, 12345);

// Enregistrement au niveau de l'OS
s.bind(myAddress);

En Langage Python

myAddress = ''         # Écouter sur toutes les interfaces réseau
myPort    = 12345         # Port sur lequel écouter
s.bind((myAddressmyPort))

Si l'appel à bind réussit, le serveur est maintenant prêt à recevoir les datagrammes entrants.

Étape 3 : Le client envoie un datagramme au serveur

Dès que le serveur est prêt à recevoir les datagrammes, le client peut lui envoyer un message en appelant directement la fonction d'envoi : send ou sendto selon les langages. L'appel à cette fonction et notamment ses paramètres est assez différent selon les langages. Cependant, elle a besoin au minimum qu'on lui spécifie le message à envoyer (une chaîne de caractères contenue dans la variable message dans nos exemples) et l'identité de l'application réceptrice. Cette identité, comme nous l'avons vu dans les vidéos, est composée de l'adresse de la machine hébergeant l'application serveur, ainsi que du port qui a été réservé par cette application.

Dans les exemples ci-dessous, on commence par récupérer l'adresse IP du serveur situé sur la machine locale (localhost), on prépare ensuite (si nécessaire) une structure contenant l'identification du serveur (adresse IP et numéro de port notamment). On prépare le message à envoyer, puis on l'envoie au moyen de la fonction idoine. 

En Langage C

// Interrogation du DNS pour obtenir l'adresse IP de la destination
struct hostent *destination = gethostbyname("localhost");
in_addr_t destIPAddr = *((in_addr_t *)(destination->h_addr));

// structure représentant l'adresse (+ numéro de port) de destination
struct sockaddr_in destAddress;
destAddress.sin_family      = AF_INET;
destAddress.sin_addr.s_addr = destIPAddr;
destAddress.sin_port        = htons(12345);

// Préparation du message à envoyer
char message[] = "Hello World\n";

// Envoi effectif du message
//     Paramètre 1 : identificateur de socket
//     Paramètres 2 & 3 : message & longueur du message
//     Paramètre 4 : drapeaux pour transmissions particulières
//     Paramètres 5 & 6 : adresse destination et taille de la structure
sendto(s, message ,strlen(message), 0, (struct sockaddr *)&destAddress, sizeof(destAddress));

En Langage Java

// Interrogation du DNS pour obtenir l'adresse IP de la destination
InetAddress destination = InetAddress.getByName("localhost");

// Objet représentant l'adresse
//     Paramètre. 1 : adresse IP
//     Paramètre 2 : numéro de port
InetSocketAddress destIPAddr=new InetSocketAddress(destination, 12345);

// Préparation du message à envoyer
String message = "Hello World\n";
byte[] payload = message.getBytes();
DatagramPacket packet = new DatagramPacket(payload, payload.length, destIPAddr);

// Envoi effectif du message
s.send(packet);

En Langage Python

destIPAddr  = "localhost"
destPort    = 12345
message = "Hello, World\n"

s.sendto(message, (destIPAddrdestPort))

Étape 3 bis : Le serveur reçoit le datagramme du client

Du coté du serveur, on se préparera à recevoir des données provenant du client en allouant un peu de mémoire pour le tampon de réception, puis en appelant la fonction recvfrom, qui est bloquante. Cela veut dire que l'exécution du programme serveur s'arrêtera à cette ligne en attendant qu'un message parvienne à notre application. 

Une fois que le serveur aura reçu un datagramme, il pourra utiliser le contenu du message et il disposera aussi de l'adresse de l'émetteur qui sera stockée dans une structure qu'il aura préalablement allouée si nécessaire. Cela permettra alors au serveur d'envoyer une réponse au client de la même manière. 

En Langage C

// Tampon de réception
char message[1024];
int nbCars;

// Descripteur pour stocker l'adresse de l'émetteur
struct sockaddr_in sourceAddr;
socklen_t length = sizeof(sourceAddr);

// Réception effective (bloquante) du message
//     Paramètre 1 : socket
//    Paramètres 2 & 3 : tampon de réception et taille de ce tampon
//    Paramètre 4 : drapeaux (inutilisé ici)


//    Paramètres 5 & 6 : adresse source & taille de l'adresse
nbCars = recvfrom (s, message, 1024, 0, (struct sockaddr *)&sourceAddr, &length);

En Langage Java

//Tampon de réception

byte[] message = new byte[1024]; 

// Réception effective (bloquante) du message
DatagramPacket packet = new DatagramPacket(message, message.length);
s.receive(packet);

// Récupération de l'adresse de l'émetteur
InetSocketAddress sourceAddr =(InetSocketAddress)packet.getSocketAddress();

En Langage Python

# Réception, taille du tampon = 1024 octets
message, sourceAddr = s.recvfrom(1024)

Fin de la communication

UDP fonctionnant en mode datagramme, la communication s'arrête dès que les deux correspondants ne transmettent aucune donnée. Il n'y a donc pas de fonction réelle pour mettre fin à la communication, mais il est toutefois nécessaire d'appeler une fonction pour libérer les ressources (mémoire etc.) qui ont été réservées par le système lorsque l'on n'a plus l'usage du canal de communication. Cette opération est réalisée au moyen d'une fonction nommée close.

En Langage C

close(s);

En Langage Java

s.close();

En Langage Python

s.close()