by Angel Lopez Content: |
Abstract:
This article attempts to be an introduction to multicast technologies on TCP/IP networks. It deals with the theoretic concepts of multicast communication, and details the Linux API that we can use for programming multicast applications. The kernel functions implementing this technology are also shown, to complete the global view of the multicast support under Linux. The article finishes with a simple C example on socket programming, where the creation of a multicast application is illustrated.
W IPv4 zakres adresów multicastowych to 224.0.0.0 do 239.255.255.255 (włącznie). Określa się je jako klasę D.
4 górne bity określają klasę D, pozostałę 28 bitów to identyfikator (numer) grupy
Do zamiany normalnych (unicast) adresów IPv4 na adresy fizyczne używa się protokołu ARP. Dla adresów multicastowych trzeba inaczej uzyskać adres fizyczny.
W przypadku Ethernetu górne 24 bajty (3 bajty) adresu fizycznego ustawiamy na 01:00:5E, a kolejny bit na 0. Pozostałe 23 najmniej znaczące bity ustawia się tak, jak w adresie multicastowym IPv4. Na przykład, adresowi multicastowemu IPv4 224.0.0.5 odpowiada adres ethernetowy 01:00:5E:00:00:05.
Pewne adresy mulitcastowe są zastrzeżone i należy unikać ich używania:
Kiedy host dołącza się do grupy multicastowej, wysyła do wszystkich lokalnych ruterów pakiet IGMP (Internet Group Management Protocol) informujący o tym. Rutery powiadomią o tym inne rutery obsługujące o tym multicast.
Rutery też wysyłają pakiety IGMP do grupy 224.0.0.1, ządający od hostów podania grup, do których są zapisani. Host odczekuje chwilę i odpowiada na adres multicastowy grupy, o ile nikt wcześniej nie odpowiedział (ruterowi wystarczy jedna odpowiedź).
Jeśli wszystkie hosty grupy wypisały się z niej, żadnej odpowiedzi nie będzie i ruter przestaje rutować komunikaty tej grupy. W protokole IGMPv2 host może wysłać rezygnację z grupy na adres 224.0.0.2.
With previous experience in sockets programming, the reader will only find five new socket operations to deal with multicast options. Functions setsockopt() and getsockopt() will be used to establish or read the values of these five options. The table below shows the available options for multicast, with their managed data types and a brief description:
| IPv4 Option | Data type | Description |
| IP_ADD_MEMBERSHIP | struct ip_mreq | Join the multicast group. |
| IP_DROP_MEMBERSHIP | struct ip_mreq | Resign from the multicast group. |
| IP_MULTICAST_IF | struct ip_mreq | Specify an interface for submission of multicast messages. |
| IP_MULTICAST_TTL | u_char | Specify a TTL for submission of multicast messages. |
| IP_MULTICAST_LOOP | u_char | Activate or deactivate the multicast messages loopback. |
The ip_mreq struct is defined in the header file <linux/in.h> as described below:
struct ip_mreq {
struct in_addr imr_multiaddr; /* IP multicast address of group */
struct in_addr imr_interface; /* local IP address of interface */
};
And the multicast options in that file are:
#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
A process can join to a multicast group sending this option over a socket with the function setsockopt(). The parameter is a ip_mreq struct. The first structure field, imr_multiaddr, contains the multicast address we want join to. The second field, imr_interface, contains the IPv4 address of the interface we will use.
Using this option a process can resign from a multicast group. The fields of the ip_mreq struct are used in the same manner as in the previous case.
This option allows us to fix the network interface that the socket will use to sent the multicast messages. The interface will be given in the ip_mreq as in the previous cases.
Establish the TTL (Time To Live) for the datagrams with the multicast messages sent using the socket. Default value is 1, meaning that the datagram will not go beyond the local subnet.
When a process sends a message for a multicast group, he will receive the messages if his interface is joined to the group, in the same way that it will be received if its origin is any other place in the network. This option allows to activate or deactivate this behavior.
To test the ideas shown in this article, we will show a simple example, where there is a process that submits messages to a multicast group, and some processes associated to this group are receiving the messages, showing them on the screen.
The next code implements a server sending to the multicast group
224.0.1.1 everything going through his standard input. As can be
seen, there is no need of any special action to sent information to
a multicast group. The destination group addresses are enough.
Loopback and TTL options could be changed, if their default values
were not appropriate for the application under development.
The standard input is sent to multicast group 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 PUERTO 5000
#define GRUPO "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(PUERTO);
if (inet_aton(GRUPO, &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, "Enviado a %s: %s\n", GRUPO, buf);
}
}
}
The code below is the client side, which receives the information submitted to the multicast group by the server. The received messages are shown on standard output. The only peculiarity of this code is the establishment of the IP_ADD_MEMBERSHIP option. The remaining code is the standard one for a process which needs to receive UDP messages.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#define MAXBUF 256
#define PUERTO 5000
#define GRUPO "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(PUERTO);
if (inet_aton(GRUPO, &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(GRUPO, &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, "Mensaje desde %s: %s\n", inet_ntoa(cli.sin_addr), buf);
}
}
}
As we showed above, when a process wants to join a multicast group, it uses the setsockopt() function to establish the option IP_ADD_MEMBERSHIP at the IP level. The actual implementation for this function can be found on /usr/src/linux/net/ipv4/ip_sockglue.c. The code executed within the function to set this option or the IP_DROP_MEMBERSHIP one is:
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);
The very first lines of code check that the input parameter, the ip_mreq struct, has a correct length, and it is possible to copy it from user to kernel areas. Once we get the parameter value, the function ip_mc_join_group() is called to join a multicast group, or ip_mc_leave_group() if we want to resign.
The code for these functions is found at /usr/src/linux/net/ipv4/igmp.c. To join a group, the source code is commented below:
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;
At the very beginning we check, using the MULTICAST macro, that the group address are within the ranges reserved for multicast addresses. It´s enough to check that the most significant byte on the IP address is set to 224.
if (!MULTICAST(addr))
return -EINVAL;
rtnl_shlock();
After the verification, a network interface is set up to deal with the multicast group. If it is not possible the access by index to the interface, as should be under IPv6, the function ip_mc_find_dev() is called to find the device associated to a specified IP address. We will assume for the remaining of the article that this is the case, because we are working under IPv4. If the address were INADDR_ANY, the kernel should find itself the network interface, reading the routing table to choose the better interface taking into account the group address and the definition of the routing tables.
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;
}
Then we reserve memory for a ip_mc_socklist struct, and each group address and interface associated to the socket are compared. If any entry previously associated to the socket matches, we jump out of the function, because it does not make sense to do a double association to a group and interface. If the network interface addresses were not INADDR_ANY, the corresponding counter is incremented before the function ends.
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;
If we arrive at this point, this means that a new socket will be linked to a new group so a new entry must be created and linked to the list of groups belonging to the socket. The memory was reserved in advance, and we only need to set the correct values for the various fields of the involved structures.
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;
}
The function ip_mc_leave_group() is in charge to resign from a multicast group, and is much simpler than the previous function. It takes the interface addresses and the group, and searches them among the entries related to the actual socket. Once they have been found, the number of references is decremented, as there is one less process associated to the group. If the new value is zero, the counter itself is deleted.
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;
}
The other multicast options that we listed above are very simple, because they just set some values in the data fields of the internal structure that is associated to the socket we are working with. These assignments are performed directly by the function ip_setsockopt().
|
© Angel Lopez, FDL LinuxFocus.org |