Information

Author(s) Olivier Tilmans
Deadline No deadline
Submission limit 12 submissions
every 1 hour(s)

Sign in

Envoyer et recevoir des données

Le but de cette tâche est de vous familiariser avec les différents appels systèmes nécessaires pour pouvoir envoyer et recevoir des données entre deux ordinateurs distants.

Pour ce faire, vous allez réaliser un système de chat, utilisant des paquets UDP.

Contrairement au protocole à réaliser dans le cadre du projet, ce système de chat ne donnera aucune garantie concernant la bonne réception des messages envoyés.

Voici les différents fichiers utilisés pour la tâche:

https://inginious.info.ucl.ac.be/course/LINGI1341/envoyer-et-recevoir-des-donnees/chat.c

https://inginious.info.ucl.ac.be/course/LINGI1341/envoyer-et-recevoir-des-donnees/Makefile

https://inginious.info.ucl.ac.be/course/LINGI1341/envoyer-et-recevoir-des-donnees/real_address.h

https://inginious.info.ucl.ac.be/course/LINGI1341/envoyer-et-recevoir-des-donnees/read_write_loop.h

https://inginious.info.ucl.ac.be/course/LINGI1341/envoyer-et-recevoir-des-donnees/create_socket.h

https://inginious.info.ucl.ac.be/course/LINGI1341/envoyer-et-recevoir-des-donnees/wait_for_client.h

Pour chaque question, votre réponse sera composée de la fonction à implémenter, ainsi que des éventuelles sous-fonctions et directives #include nécessaire pour leur bon fonctionnement.

Comme pour la tâche précédente, n'affichez rien sur la sortie standard (stdout) qui ne soit explicitement demandé, utilisez la sortie d'erreur standard à la place pour des messages de debug.


Question 1: Résoudre un nom de ressource

Pour pouvoir envoyer des données d'un ordinateur à l'autre, il est nécessaire de connaître l'adresse du destinataire. Dans le cadre de ce projet, nous utiliserons des adresses IPv6. Bien que ces adresses permettent d'identifier explicitement un hôte, elles sont difficile à retenir, il est en effet plus facile de retenir monsuperchat.com que 2001:6a8:308f:100::f, ou encore localhost pour ::1

Pour supporter cette fonctionnalité, nous vous demandons d'écrire une fonction permettant de convertir une chaîne de caractères représentant soit un nom de dommaine soit une adresse IPv6, en une structure utilisable par l'OS, càd une

struct sockaddr_in6

Il vous est fortement conseillé de lire les manpages suivantes:

getaddrinfo, netinet_in.h

Cette fonction à été déclarée dans un header comme suit:

/* Resolve the resource name to an usable IPv6 address
 * @address: The name to resolve
 * @rval: Where the resulting IPv6 address descriptor should be stored
 * @return: NULL if it succeeded, or a pointer towards
 *          a string describing the error if any.
 *          (const char* means the caller cannot modify or free the return value,
 *           so do not use malloc!)
 */
const char * real_address(const char *address, struct sockaddr_in6 *rval);

Réflexion: Cette fonction ne renvoie qu'une seule adresse, alors qu'un nom de domaine peut désigner plusieurs IPv6. Nous n'avons pas de garantie que le serveur correspondant à l'adresse choisie aura suffisament de ressources disponibles pour nous répondre. Pourquoi est-ce acceptable dans notre cas ? Cette raison serait-elle toujours valable si l'on utilisait TCP à la place d'UDP ?

Question 2: Création d'un socket

Un socket est un file descriptor spécialisé permettant de lire des données et d'en écrire sur un adaptateur réseau.

Lors de la création d'un socket, il faut spécifier le protocole utilisé (UDP dans ce cas ci). Ensuite, il est possible de lui spécifier le port sur lequel il écoutera et recevra des messages entrants ainsi que l'adresse source qu'il utilisera pour envoyer des données; et/ou l'adresse de destination et le port sur lequel il enverra les données.

Nous vous demandons d'écrire une fonction permettant de créer un socket, ainsi que de le lier à certains ports et adresses.

Il vous est fortement conseillé de lire les manpages suivantes:

socket, bind, connect

Cette fonction à été déclarée dans un header comme suit:

/* Creates a socket and initialize it
 * @source_addr: if !NULL, the source address that should be bound to this socket
 * @src_port: if >0, the port on which the socket is listening
 * @dest_addr: if !NULL, the destination address to which the socket should send data
 * @dst_port: if >0, the destination port to which the socket should be connected
 * @return: a file descriptor number representing the socket,
 *         or -1 in case of error (explanation will be printed on stderr)
 */
int create_socket(struct sockaddr_in6 *source_addr,
                 int src_port,
                 struct sockaddr_in6 *dest_addr,
                 int dst_port);
Question 3: Lire et écrire des données

Le coeur de l'application de chat est le transfert de ce que tape un utilisateur vers l'écran de l'autre utilisateur.

En d'autres termes, pour des utilisateurs A et B, on observe:

A[ fread(stdin) -> write(socket) ]  --  B[ read(socket) -> fwrite(stdout) ]
B[ fread(stdin) -> write(socket) ]  --  A[ read(socket) -> fwrite(stdout) ]

Si A et B tapent en même temps, on observe donc que les lectures et écritures sont multiplexées sur le même file descriptor (le socket). Afin de ne pas bloquer le processus indéfiniment (par ex. en attendant que A finisse de taper avant d'afficher le texte de B), il est donc nécessaire de traiter la lecture et l'écriture simultanément. Cela peut se faire de façon équivalente au moyen des appels systèmes suivants:

select, poll

Dans cette question, nous vous demandons d'implémenter une fonction qui permet de lire le contenu de l'entrée standard et de l'envoyer sur un socket, tout en permettant d'afficher sur la sortie standard ce qui est lu sur ce même socket. Il ne vous est pas permis d'utiliser des threads.

N'oubliez pas que fread() et fwrite() font des lectures/écritures via un buffer c-à-d non immédiates! De même, forcer l'écriture de toute les données avec fflush() peut être nécessaire.

Vous pouvez considérer que la taille maximale des segments envoyés ou reçus sera de 1024 bytes.

Cette fonction à été déclarée dans un header comme suit:

/* Loop reading a socket and printing to stdout,
 * while reading stdin and writing to the socket
 * @sfd: The socket file descriptor. It is both bound and connected.
 * @return: as soon as stdin signals EOF
 */
void read_write_loop(int sfd);
Question 4: Détecter la connection d'un client

Les fonctions précédentes permettent d'écrire le code principal du programme de chat comme visible dans le fichier chat.c

Cependant, celui-ci n'est pas encore fonctionnel. En effet, lorsque un client veut parler à un serveur, il spécifie l'adresse et port du serveur, et choisit un port aléatoire pour lui. Le serveur par contre ne connait pas à priori l'adresse du client qui se connectera.

Pour question, nous vous demandons d'écrire une fonction qui interceptera le premier message reçu par le serveur, afin de connaître l'adresse du client de pouvoir connecter le socket du serveur au client, pour pouvoir après utiliser la fonction read_write_loop de façon transparente pour le client et le serveur.

Nous vous conseillons de lire les manpages suivantes:

recvfrom, connect

Cette fonction à été déclarée dans un header comme suit:

/* Block the caller until a message is received on sfd,
 * and connect the socket to the source addresse of the received message
 * @sfd: a file descriptor to a bound socket but not yet connected
 * @return: 0 in case of success, -1 otherwise
 * @POST: This call is idempotent, it does not 'consume' the data of the message,
 * and could be repeated several times blocking only at the first call.
 */
int wait_for_client(int sfd);