original in es Angel Lopez
es to en Javier Palacios
en to fr Iznogood
Angel termine ses études d'ingénieur système. Il donne des cours sur Solaris et l'administration réseau chez Sun Microsystems. Il a publié le livre Internet protocols. Design and implementation on Unix systems chez l'éditeur Ra-Ma. Ses centres d'intérêt sont le réseau, la sécurité, la programmation système ou réseau sous Unix, et très récemment, du noyau linux, réduisant ainsi son temps de sommeil ;)
Cet article se veut une introduction aux technologies multicast pour les réseaux TCP/IP. Il traite des concepts théoriques de la communication multicast et détaille l'API Linux que nous utilisons pour programmer les applications multicast. Les fonctions du noyau qui implémentent cette technologie sont aussi présentées pour compléter la vue d'ensemble du support multicast sous Linux. L'article se termine par un exemple de programmation de socket en C, dans lequel la création d'une application multicast est illustrée.
Lorsque vous tentez de contacter un hôte (interface) dans un réseau, vous pouvez utiliser trois types d'adresses :
Les adresses multicast sont utiles lorsque le destinataire n'est pas qu'un hôte et que nous ne voulons pas produire un broadcast réseau. Ce scénario est typique dans les situations qui nécessitent l'envoi d'informations multimédia (audio ou vidéo temps réel par exemple) vers certains hôtes. En terme de bande passante, l'envoi en unicast vers tous les clients devant recevoir l'émission multimédia n'est pas ce qu'il y a de mieux. Le broadcast offre une meilleure solution, surtout si tous les clients sont localisés hors du sous-réseau qui est à l'origine de l'émission.
Comme le sait sûrement le lecteur, l'espace des adresses IP est distribué en trois groupes de classes d'adresses. Les classes d'adresses A, B et C. Il en existe une quatrième (D) réservée pour les adresses multicast. Les adresses IPv$ entre 224.0.0.0 et 239.255.255.255 appartiennent à cette classe.
Les 4 bits les plus significatifs de l'adresse IP autorisent des valeurs
comprises entre 224 et 239. Les autres 28 bits, moins significatifs, sont
réservés à l'identificateur du groupe multicast, comme montré dans la figure ci-dessous :
Au niveau du réseau, les adresses multicast IPv4 doivent être "mappées" sur des adresses physiques du type de réseau sur lequel nous travaillons. Si nous travaillons avec les adresses unicast du réseau, nous devrions obtenir les adresses physiques associées en utilisant le protocole ARP. Dans le cas des adresses multicast, ARP ne peut pas être utilisé et les adresses physiques doivent être obtenues d'une autre manière. Il existe quelques documents RFC expliquant la manière d'effectuer ce "mappage" :
Il existe quelques adresses multicast IPv4 particulières :
Le tableau ci-dessous montre le champ complet des adresses multicast avec les noms usuels pour chaque plage d'adresse et leur TTL associé. Avec le multicast IPv4, le TTL a une double signification. Comme le sait probablement le lecteur, il contrôle la durée de vie d'un datagramme dans le réseau pour éviter toute boucle infinie dans le cas de tables de routage mal configurées. En travaillant avec le multicast, la valeur TTL définit aussi le champ d'activité du datagramme, par exemple, jusqu'où circulera-t-il au sein du réseau. Ceci permet une définition de champ d'activité basée sur la catégorie du datagramme.
Champ d'activité (scope) | TTL | Plage d'adresse | Description |
Noeud | 0 | Le datagramme est limité à l'hôte local. Il n'atteindra aucune autre interface du réseau. | |
Lien | 1 | 224.0.0.0 - 224.0.0.255 | Le datagramme sera limité au sous-réseau de l'hôte émetteur et n'ira pas au-delà des routeurs. |
Département | < 32 | 239.255.0.0 - 239.255.255.255 | Limité à un département de l'organisation. |
Organisation | < 64 | 239.192.0.0 - 239.195.255.255 | Limité à une organisation spécifique. |
Global | < 255 | 224.0.1.0 - 238.255.255.255 | Pas de restriction, application globale. |
Dans un LAN, l'interface réseau d'un hôte enverra aux couches supérieures tous
les paquets qui ont l'hôte comme destination. Ces paquets seront ceux qui auront
pour adresse de destination les adresses de l'interface physique ou une adresse
broadcast.
Si l'hôte fait partie d'un groupe multicast, l'interface réseau reconnaîtra
aussi les paquets destinés à ce groupe : tous ceux avec une adresse de
destination correspondant au groupe multicast dont l'hôte est membre.
C'est pourquoi, si une interface d'hôte possède l'adresse physique 80:C0:F6:A0:4A:B1 et fait partie du groupe multicast 224.0.1.10, les paquets qui seront reconnus comme appartenant à l'hôte seront ceux ayant l'une des adresses suivantes :
Les routeurs envoient aussi des messages IGMP au groupe 224.0.0.1 demandant à chaque hôte des informations sur les groupes auxquels il adhère. Après avoir reçu ce message, un hôte définit un compteur sur une valeur aléatoire et répondra lorsqu'il sera arrivé à zéro. Ceci permet d'éviter que tous les hôtes répondant en même temps, provoquent une surcharge réseau. Lorsque l'hôte répond, il envoie le message à l'adresse multicast du groupe et tous les autres hôtes membres de ce groupe peuvent voir la réponse sans avoir à répondre eux-mêmes, puisqu'un hôte membre est suffisant au routeur du sous-réseau pour gérer les messages multicast de ce groupe.
Si tous les hôtes inscrits à un groupe se sont retirés, aucun ne répondra et le routeur décidera qu'aucun hôte n'est intéressé par ce groupe et cessera le routage des messages correspondants pour ce sous réseau. Une autre option existe dans IGMPv2 : l'hôte informe de son retrait en envoyant un message à l'adresse 224.0.0.2.
Avec des expériences antérieures de programmation de socket, le lecteur ne trouvera que cinq nouvelles opérations de socket pour gérer les options multicast. Les fonctions setsockopt() et getsockopt() seront utilisées pour établir ou lire les valeurs de ces cinq options. Le tableau ci-dessous montre les options disponibles pour multicast avec les types de données gérés et une brève description :
Option IPv4 | Type de Donnée | Description |
IP_ADD_MEMBERSHIP | struct ip_mreq | Adhérer au groupe multicast. |
IP_DROP_MEMBERSHIP | struct ip_mreq | Se retirer du groupe multicast. |
IP_MULTICAST_IF | struct ip_mreq | Spécifier une interface pour l'envoi de messages multicast. |
IP_MULTICAST_TTL | u_char | Spécifier un TTL pour l'envoi de messages multicast. |
IP_MULTICAST_LOOP | u_char | Activer ou désactiver le loopback des messages multicast. |
La structure ip_mreq est définie dans le fichier d'en-tête <linux/in.h> comme décrit ci-dessous :
struct ip_mreq { struct in_addr imr_multiaddr; /* IP multicast address of group */ struct in_addr imr_interface; /* local IP address of interface */ };Et les options multicast dans ce fichier sont :
#define IP_MULTICAST_IF 32 #define IP_MULTICAST_TTL 33 #define IP_MULTICAST_LOOP 34 #define IP_ADD_MEMBERSHIP 35 #define IP_DROP_MEMBERSHIP 36
Un processus peut adhérer à un groupe multicast en envoyant cette option via un socket avec la fonction setsockopt(). Le paramètre est une structure ip_mreq. Le premier champ de la structure, imr_multiaddr, contient l'adresse multicast que nous voulons rejoindre. Le second champ, imr_interface, contient l'adresse IPv4 de l'interface que nous allons utiliser.
En utilisant cette option, un processus peut se retirer d'un groupe multicast. Les champs de la structure ip_mreq sont utilisés de la même manière que dans le cas précédent.
Cette option définit l'interface de réseau que le socket va utiliser pour envoyer des messages multicast. L'interface sera obtenue à partir d'ip_mreq comme dans les cas précédents.
Etablit le TTL (Time To Live) pour les datagrammes avec les messages multicast envoyés en utilisant le socket. La valeur par défaut est à 1, signifiant que le datagramme n'ira pas au-delà du sous-réseau local.
Lorsqu'un processus envoie un message pour un groupe multicast, il reçoit les messages si son interface adhère au groupe, de la même manière qu'il sera reçu si ses origines se situent ailleurs sur le réseau. Cette option permet d'activer ou désactiver ce comportement.
Pour tester les idées présentées dans cet article, nous allons prendre un exemple simple de processus qui envoie des messages à un groupe multicast et de processus associés à ce groupe qui reçoivent les messages, en les affichant à l'écran.
Le programme suivant correspond à un serveur envoyant tout ce qui passe par son
entrée standard vers le groupe multicast 224.0.1.1. Comme vous pouvez le voir, il
n'y a pas besoin d'action particulière pour envoyer des informations à un groupe
multicast. Les adresses du groupe de destination sont suffisantes.
Les options de loopback et de TTL peuvent être changées si leur valeur par
défaut n'étaient pas appropriées pour les applications en cours de développement.
L'entrée standard est envoyée au groupe multicast 224.0.1.1
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #define MAXBUF 256 #define PORT 5000 #define GROUP "224.0.1.1" int main(void) { int s; struct sockaddr_in srv; char buf[MAXBUF]; bzero(&srv, sizeof(srv)); srv.sin_family = AF_INET; srv.sin_port = htons(PORT); if (inet_aton(GROUP, &srv.sin_addr) < 0) { perror("inet_aton"); return 1; } if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket"); return 1; } while (fgets(buf, MAXBUF, stdin)) { if (sendto(s, buf, strlen(buf), 0, (struct sockaddr *) \ srv, sizeof(srv)) < 0) { perror("recvfrom"); } else { fprintf(stdout, "Sent to %s: %s\n", GROUP, buf); } } }
Le programme ci-dessous est le coté client qui reçoit les informations envoyées au groupe multicast par le serveur. Les messages reçus sont affichés sur sa sortie standard. La seule particularité de ce code est l'établissement de l'option IP_ADD_MEMBERSHIP. Le reste du programme est le code standard pour un processus destiné à recevoir des messages UDP.
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #define MAXBUF 256 #define PORT 5000 #define GROUP "224.0.1.1" int main(void) { int s, n, r; struct sockaddr_in srv, cli; struct ip_mreq mreq; char buf[MAXBUF]; bzero(&srv, sizeof(srv)); srv.sin_family = AF_INET; srv.sin_port = htons(PORT); if (inet_aton(GROUP, &srv.sin_addr) < 0) { perror("inet_aton"); return 1; } if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { perror("socket"); return 1; } if (bind(s, (struct sockaddr *)&srv, sizeof(srv)) < 0) { perror("bind"); return 1; } if (inet_aton(GROUP, &mreq.imr_multiaddr) < 0) { perror("inet_aton"); return 1; } mreq.imr_interface.s_addr = htonl(INADDR_ANY); if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, \ mreq, sizeof(mreq)) < 0) { perror("setsockopt"); return 1; } n = sizeof(cli); while (1) { if ((r = recvfrom(s, buf, MAXBUF, 0, (struct sockaddr *) \ cli, &n)) < 0) { perror("recvfrom"); } else { buf[r] = 0; fprintf(stdout, "Message from %s: %s\n", \ inet_ntoa(cli.sin_addr), buf); } } }
Comme nous le montrons ci-dessus, lorsqu'un processus souhaite adhérer à un groupe multicast, il utilise la fonction setsockopt() pour établir l'option IP_ADD_MEMBERSHIP au niveau IP. La manière d'utiliser cette fonction peut être trouvée dans /usr/src/linux/net/ipv4/ip_sockglue.c. Le programme executé dans la fonction pour définir cette option ou celle d'IP_DROP_MEMBERSHIP est :
struct ip_mreqn mreq; if (optlen < sizeof(struct ip_mreq)) return -EINVAL; if (optlen >= sizeof(struct ip_mreqn)) { if(copy_from_user(&mreq,optval,sizeof(mreq))) return -EFAULT; } else { memset(&mreq, 0, sizeof(mreq)); if (copy_from_user(&mreq,optval,sizeof(struct ip_mreq))) return -EFAULT; } if (optname == IP_ADD_MEMBERSHIP) return ip_mc_join_group(sk,&mreq); else return ip_mc_leave_group(sk,&mreq);
Les premières lignes de code contrôlent que le paramètre d'entrée, la structure ip_mreq possède la longueur correcte et qu'il est possible de la copier de la partie utilisateur vers la partie noyau. Une fois que nous avons la valeur du paramètre, la fonction ip_mc_join_group() est appelée pour adhérer à un groupe multicast ou ip_mc_leave_group() si nous voulons nous retirer du groupe.
Le programme pour ces fonctions se trouve dans /usr/src/linux/net/ipv4/igmp.c. Pour adhérer à un groupe, le code source est commenté ci-dessous :
int ip_mc_join_group(struct sock *sk , struct ip_mreqn *imr) { int err; u32 addr = imr->imr_multiaddr.s_addr; struct ip_mc_socklist, *iml, *i; struct in_device *in_dev; int count = 0;
Le point de départ consiste à vérifier que les adresses du groupe se situent bien dans la plage réservée aux adresses multicast, à l'aide de la macro MULTICAST. Il suffit de vérifier que l'octet le plus significatif de l'adresse IP soit à 224.
if (!MULTICAST(addr)) return -EINVAL; rtnl_shlock();
Après la vérification, une interface réseau est mise en place pour dialoguer avec le groupe multicast. S'il n'est pas possible d'accéder par index à l'interface, comme ce devrait être le cas avec IPv6, la fonction ip_mc_find_dev() est appelée pour trouver le périphérique associé à une adresse IP spécifique. Nous supposerons pour le reste de cet article que c'est le cas parce que nous travaillons sous IPv4. Si l'adresse était INADDR_ANY, le noyau devrait trouver lui-même l'interface réseau en lisant la table de routage pour choisir la meilleure interface en tenant compte de l'adresse de groupe et de la définition des tables de routage.
if (!imr->imr_ifindex) in_dev = ip_mc_find_dev(imr); else in_dev = inetdev_by_index(imr->imr_ifindex); if (!in_dev) { iml = NULL; err = -ENODEV; goto done; }
Nous réservons alors de la mémoire pour une structure ip_mc_socklist. Chaque adresse de groupe et interface associés au socket est comparée. Si d'autre entrées associées précédemment au socket correspondent, nous quittons la fonction puisqu'une double association à un groupe et une interface n'a pas de sens. Si les adresses de l'interface réseau ne sont pas INADDR_ANY, le compteur correspondant est incrémenté avant que la fonction ne se termine.
iml = (struct ip_mc_socklist *)sock_kmalloc(sk, sizeof(*iml), GFP_KERNEL); err = -EADDRINUSE; for (i=sk->ip_mc_list; i; i=i->next) { if (memcmp(&i->multi, imr, sizeof(*imr)) == 0) { /* New style additions are reference counted */ if (imr->imr_address.s_addr == 0) { i->count++; err = 0; } goto done; } count++; } err = -ENOBUFS; if (iml == NULL || count >= sysctl_igmp_max_memberships) goto done;
Si nous arrivons à ce point, cela signifie qu'un nouveau socket sera lié à un nouveau groupe pour qu'une nouvelle entrée soit créée et liée à la liste de groupes appartenant au socket. La mémoire a été réservée par avance et nous avons seulement besoin de définir les valeurs correctes pour les différents champs des structures impliquées.
memcpy(&iml->multi,imr, sizeof(*imr)); iml->next = sk->ip_mc_list; iml->count = 1; sk->ip_mc_list = iml; ip_mc_inc_group(in_dev,addr); iml = NULL; err = 0; done: rtnl_shunlock(); if (iml) sock_kfree_s(sk, iml, sizeof(*iml)); return err; }
La fonction ip_mc_leave_group() est chargée du retrait d'un groupe multicast et elle est plus simple que les fonctions précédentes. Elle prend les adresses de l'interface et du groupe et les cherche parmi les entrées liées au socket réel. Une fois qu'elles ont été trouvées, le nombre de références est décrémenté puisqu'il y a un processus de moins associé au groupe. Si la nouvelle valeur est zéro, le compteur est effacé.
int ip_mc_leave_group(struct sock *sk, struct ip_mreqn *imr) { struct ip_mc_socklist *iml, **imlp; for (imlp=&sk->ip_mc_list;(iml=*imlp)!=NULL; imlp=&iml->next) { if (iml->multi.imr_multiaddr.s_addr==imr->imr_multiaddr.s_addr && iml->multi.imr_address.s_addr==imr->imr_address.s_addr && (!imr->imr_ifindex || iml->multi.imr_ifindex==imr->imr_ifindex)) { struct in_device *in_dev; if (--iml->count) return 0; *imlp = iml->next; synchronize_bh(); in_dev = inetdev_by_index(iml->multi.imr_ifindex); if (in_dev) ip_mc_dec_group(in_dev, imr->imr_multiaddr.s_addr); sock_kfree_s(sk, iml, sizeof(*iml)); return 0; } } return -EADDRNOTAVAIL; }
Les autres options multicast que nous avons listées ci-dessus sont très simples car elles définissent simplement des valeurs dans les champs de données de la structure interne qui est associée au socket sur lequel nous travaillons. Ces affectations sont effectuées directement par la fonction ip_setsockopt().