初识组播(1)
我们知道在内核中用结构体struct net_device标识一个网络设备接口,该结构体有一个成员指针ip_ptr,它是留给IPv4协议用于填充协议相关的一些数据的。IPv4协议的模块将其指向一个结构体struct in_device,该结构体含有很多协议相关的数据,比如配置在这个网络设备接口上的所有的IPv4的地址,该网络设备接口接受的组播地址等,下面是其完整的定义:
struct in_device
{
struct net_device *dev;
atomic_t refcnt;
int dead;
struct in_ifaddr *ifa_list; //IP地址列表
rwlock_t mc_list_lock;
struct ip_mc_list *mc_list; //IP组播过滤列表。
spinlock_t mc_tomb_lock; //下面都是组播相关数据。
struct ip_mc_list *mc_tomb;
unsigned long mr_v1_seen;
unsigned long mr_v2_seen;
unsigned long mr_maxdelay;
unsigned char mr_qrv;
unsigned char mr_gq_running;
unsigned char mr_ifc_count;
struct timer_list mr_gq_timer; /* general query timer */
struct timer_list mr_ifc_timer; /* interface change timer */
struct neigh_parms *arp_parms;
struct ipv4_devconf cnf;
struct rcu_head rcu_head;
};
我们暂时还无法完全理解这个结构体,目前只需关注的是mc_list成员,这是关于组播的一个最为关键的数据结构。mc_list是一个链表,链表的一个结点代表一个组播地址(也就是一个多播组的组号),代表这个网络设备接口已经加入了这个组播组,需要接收来自这个组的数据报。下面是该节点的结构体定义:
struct ip_mc_list
{
struct in_device *interface;
unsigned long multiaddr;
struct ip_sf_list *sources;
struct ip_sf_list *tomb;
unsigned int sfmode;
unsigned long sfcount[2];
struct ip_mc_list *next;
struct timer_list timer;
int users;
atomic_t refcnt;
spinlock_t lock;
char tm_running;
char reporter;
char unsolicit_count;
char loaded;
unsigned char gsquery;
unsigned char crcount;
};
同样,我们把其中的大部分成员留待以后理解。multiaddr就是组播地址,sources和tomb是关于组播源地址的一个列表,sfmode和sfcount是过滤参数,也就是说,该网络设备接口虽然加入了某个组播组,但对某些主机向该组发的数据报不接收,或者只接收某个主机发向该组的数据报,这就要对组播源进行过滤。
关于D类地址的常识,这里不再介绍,可参考相关书籍。
下面介绍一个特殊的组播地址224.0.0.1,它标识子网中的所有主机,同一个子网内具有组播功能的主机都属于这个组。我们的my_inet模块在初始化时,myinetdev_event函数收到网络设备接口启动(NETDEV_UP)的消息后,调用myip_mc_up启动组播功能。
启动组播功能的第一件事便是把本机加入到这个特殊的组播组IGMP_ALL_HOSTS(即224.0.0.1),调用myip_mc_inc_group函数完成加入动作。因为我们的my_inet模块是作为系统中的第二个IPv4模块,在系统正常运行后被加载的,所以,网络设备接口早已完成了加入该组播组的操作。my_inet模块的加入动作是只是简单地将成员users加1,然后调用myip_mc_add_src函数加入组播源过滤。
IGMP_ALL_HOSTS组的sources为NULL,sfmode为MCAST_EXCLUDE(过滤掉sources中列出的所有源),所以结果是不过滤任何组播源。myip_mc_add_src函数中,将sfcount[MCAST_EXCLUDE]的值加1,表示新增一个过滤机制。
初识组播(2)
上一篇我们讲到,因为my_inet是系统中第二个加载的IPv4模块,所以网络设备接口早已完成了加入IGMP_ALL_HOST组的操作,my_inet只是简单增加引用计数和源过滤计数,下面我们来看看,第一个加载的IPv4模块(即内核原有的TCP/IP协议栈模块)是如何把网络设备接口加入IGMP_ALL_HOST组的。
在myip_mc_inc_group函数中,首先检查in_device->mc_list列表中已加入的组播组,看本接口是否已经加入了IGMP_ALL_HOST组,结果当然是没有。则,首先创建一个新的结构体struct ip_mc_list *im,初始化其成员值,设成员multiaddr为组播地址224.0.0.1,sf_mode为MCAST_EXCLUDE,sfcount[MCAST_EXCLUDE]为1,sources为NULL,表示使用一个源过滤机制,该机制不过滤任何组播源。成员loaded为0,表示该组播组尚未被载入(稍后将看到载入的操作)。初始化完成后,将这个新的组播组加入到mc_list链表的表头。
前面讲到过,mc_tomb也是in_device的一个成员,也表示一个组播组列表,这个列表中的组应该是不活跃的(当前不在使用的,具体留待以后分析),新的组加入到mc_list成功后,还要到这个列表中查找,看是否也存在于这个列表中,如果存在,要删除,因为该组当前是活跃的。
最后,调用myigmp_group_added完成真正的加入组播组的操作,对于IGMP_ALL_HOST这个组来讲,该函数做的事情相对比较少,它检查loaded成员,发现为0,则调用myip_mc_filter_add,加入一个网络设备级的组播地址。也就是说,代表网络设备接口的结构体struct net_device有一个成员mc_list,它是一个链表,每个结点代表一个组播组的mac地址。与in_device的mc_list中的组播IP地址对应。loaded为0时,我们要做的事情就是把IP地址224.0.0.1映射成一个mac地址加到net_device的mc_list链表中去,然后把loaded置1,该成员的结点定义如下:
struct dev_mc_list
{
struct dev_mc_list *next;
__u8 dmi_addr[MAX_ADDR_LEN];
unsigned char dmi_addrlen;
int dmi_users;
int dmi_gusers;
};
dmi_addr是mac地址,dmi_addrlen是地址长度,dmi_users是引用计数。添加完成后,net_device的成员mc_count相应的加1。
下面我们来看看组播IP地址是如何被映射成组播mac地址的。一个mac地址总共有6字节,48位,被分成两段:前3字节和后3字节,前3字节用于标识网卡的制造厂商,其中第40位(第一字节的最低位)用于标识组播,所以在网卡的mac地址中必须置0,后3字节是厂商内部使用的序列号。一个组播IP地址映射成mac地址的规则是:前三字节强制置01:00:5E,后3字节中,第23位置0,0-22位放入IP地址的0-23位。
加入一个组播组
网络中的一台主机如果希望能够接收到来自网络中其它主机发往某一个组播组的数据报,那么这么主机必须先加入该组播组,然后就可以从组地址接收数据包。在广域网中,还涉及到路由器支持组播路由等,但本文希望以一个最为简单的例子解释清楚协议栈关于组播的一个最为简单明了的工作过程,甚至,我们不希望涉及到IGMP包。
我们先从一个组播客户端的应用程序入手来解析组播的工作过程:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "my_inet.h"
#include <arpa/inet.h>
#define MAXBUF 256
#define PUERTO 5000
#define GRUPO "224.0.1.1"
int main(void)
{
int fd, n, r;
struct sockaddr_in srv, cli;
struct ip_mreq mreq;
char buf[MAXBUF];
memset( &srv, 0, sizeof(struct sockaddr_in) );
memset( &cli, 0, sizeof(struct sockaddr_in) );
memset( &mreq, 0, sizeof(struct ip_mreq) );
srv.sin_family = MY_AF_INET;
srv.sin_port = htons(PUERTO);
if( inet_aton(GRUPO, &srv.sin_addr ) < 0 ) {
perror("inet_aton");
return -1;
}
if( (fd = socket( MY_AF_INET, SOCK_DGRAM, MY_IPPROTO_UDP) ) < 0 ){
perror("socket");
return -1;
}
if( bind(fd, (struct sockaddr *)&srv, sizeof(srv)) < 0 ){
perror("bind");
return -1;
}
if (inet_aton(GRUPO, &mreq.imr_multiaddr) < 0)
{
perror("inet_aton");
return -1;
}
inet_aton( "172.16.48.2", &(mreq.imr_interface) );
if( setsockopt(fd, SOL_IP, IP_ADD_MEMBERSHIP, &mreq,sizeof(mreq)) < 0 )
{
perror("setsockopt");
return -1;
}
n = sizeof(cli);
while(1)
{
if( (r = recvfrom(fd, buf, MAXBUF, 0, (struct sockaddr *)&cli, (socklen_t*)&n)) < 0 )
{
perror("recvfrom");
}
else
{
buf[r] = 0;
fprintf(stdout, "Mensaje desde %s: %s", inet_ntoa(cli.sin_addr), buf);
}
}
}
这是一个非常简单的组播客户端,它指定从组播组224.0.1.1的5000端口读数据,并显示在终端上,下面我们通过分析该程序来了解内核的工作过程。
前面我们讲过,bind操作首先检查用户指定的端口是否可用,然后为socket的一些成员设置正确的值,并添加到哈希表myudp_hash中。然后,协议栈每次收到UDP数据,就会检查该数据报的源和目的地址,还有源和目的端口,在myudp_hash中找到匹配的socket,把该数据报放入该socket的接收队列,以备用户读取。在这个程序中,bind操作把socket绑定到地址224.0.0.1:5000上, 该操作产生的直接结果就是,对于socket本身,下列值受影响:
struct inet_sock{
.rcv_saddr = 224.0.0.1;
.saddr = 0.0.0.0;
.sport = 5000;
.daddr = 0.0.0.0;
.dport = 0;
}
这五个数据表示,该套接字在发送数据包时,本地使用端口5000,本地可以使用任意一个网络设备接口,发往的目的地址不指定。在接收数据时,只接收发往IP地址224.0.0.1的端口为5000的数据。
程序中,紧接着bind有一个setsockopt操作,它的作用是将socket加入一个组播组,因为socket要接收组播地址224.0.0.1的数据,它就必须加入该组播组。结构体struct ip_mreq mreq是该操作的参数,下面是其定义:
struct ip_mreq
{
struct in_addr imr_multiaddr; // 组播组的IP地址。
struct in_addr imr_interface; // 本地某一网络设备接口的IP地址。
};
一台主机上可能有多块网卡,接入多个不同的子网,imr_interface参数就是指定一个特定的设备接口,告诉协议栈只想在这个设备所在的子网中加入某个组播组。有了这两个参数,协议栈就能知道:在哪个网络设备接口上加入哪个组播组。为了简单起见,我们的程序中直接写明了IP地址:在172.16.48.2所在的设备接口上加入组播组224.0.1.1。
这个操作是在网络层上的一个选项,所以级别是SOL_IP,IP_ADD_MEMBERSHIP选项把用户传入的参数拷贝成了struct ip_mreqn结构体:
struct ip_mreqn
{
struct in_addr imr_multiaddr;
struct in_addr imr_address;
int imr_ifindex;
};
多了一个输入接口的索引,暂时被拷贝成零。
该操作最终引发内核函数myip_mc_join_group执行加入组播组的操作。首先检查imr_multiaddr是否为合法的组播地址,然后根据imr_interface的值找到对应的struct in_device结构。接下来就要为socket加入到组播组了,在inet_sock的结构体中有一个成员mc_list,它是一个结构体struct ip_mc_socklist的链表,每一个节点代表socket当前正加入的一个组播组,该链表是有上限限制的,缺省值为IP_MAX_MEMBERSHIPS(20),也就是说一个socket最多允许同时加入20个组播组。下面是struct ip_mc_socklist的定义:
struct ip_mc_socklist
{
struct ip_mc_socklist *next;
struct ip_mreqn multi;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip_sf_socklist *sflist;
};
struct ip_sf_socklist
{
unsigned int sl_max;
unsigned int sl_count;
__u32 sl_addr[0];
};
除了multi成员,它还有一个源过滤机制。如果我们新添加的struct ip_mreqn已经存在于这个链表中(表示socket早就加入这个组播组了),那么不做任何事情,否则,创建一个新的struct ip_mc_socklist:
struct ip_mc_socklist
{
.next = inet->mc_list; //新节点放到链表头。
.multi = 传入的参数; //这是关键的组信息。
.sfmode = MCAST_EXCLUDE; //过滤掉sflist中的所有源。
.sflist = NULL; //没有源需要过滤。
};
最后,调用myip_mc_inc_group函数在struct in_device和struct net_device的mc_list链表中都添上相应的组播组节点,关于这部分的细节可以在前一篇文章《初识组播2》中找到。不再重复。
到此为止,我们完成了最为简单的加入组播组的操作,对于同一子网内的情况,socket已经可以接收组播数据了,关于组播数据如何接收,下回分解。
接收组播数据报
前面我们讲到如何加入到一个组播组中,当一个客户端完成了加入一个组播组的操作后,就可以从该组接收数据了。下面我们看看组播数据报接收的详细流程。
通过加入组播组的操作后,网络设备接口已经知道要接收该组的数据报,所以组播数据会从网卡接收进来,一直到达myip_rcv函数,我们就从myip_rcv函数开始,跟踪整个组播数据报的接收流程。
同样,myip_rcv还是先检查数据报的类型(是否为本机需要接收的包),ip首部是否正确,然后调用myip_rcv_finish。myip_rcv_finish对任何数据报都要先查找输入路由,输入路由查找函数是myip_route_input,当该函数在路由缓存myrt_hash_table中找不到相应的路由项时,判断数据报的输入地址,如果发现是组播地址,就不能简单地查找FIB,而是要作特殊处理。
首先,调用myip_check_mc对这个组播数据报作检查,从网络设备接口的struct in_device中去匹配组播地址,如果匹配不到,表示这个不是我们希望接收的组播包,丢弃。匹配到了,则作下一步检查,如果这本身就是一个IGMP包,则接收,否则,查看这个组播组在我们的struct in_device中设置的过滤机制,如果该数据报的源地址在我们的过滤名单中,则丢弃,否则接收。
如果检查通过,准备接收这个组播包,则调用myip_route_input_mc查找组播输入路由,这是一个专门为组播设置的函数,它第一步要检查数据报源地址的有效性,即源地址不能是组播地址,不能是广播地址,也不能是回环地址,同时,该数据报必须是一个因特网协议包(ETH_P_IP)。如果源地址为0,那么只有当目的地址是224.0.0.0-224.0.0.255之间的值(只能在发送主机所在的一个子网内的传送,不会通过路由器转发。)时,系统可以自己选定一个scope为RT_SCOPE_LINK的源地址,否则出错。
当验证了源地址的有效性之后,我们建立路由项,即结构体struct rtable。该路由项的rt_type值是RTN_MULTICAST,表示这一条组播路由。对于本地接收的组播包,我们设置接收函数为myip_local_deliver。
有了这个路由项,我们可以通过调用myip_local_deliver,继续接收流程,这部分流程前面已有多次介绍,所以讲得简单一点,只注意组播特有的。同样,到myip_local_deliver_finish后,首先要检查是否有raw socket要接收这个组播包。然后根据IP首部里协议字段,调用相应协议的接收函数,我们这儿是一个UDP组播包,所以调用myudp_rcv。
myudp_rcv首先会对路由项的成员rt_flags作一个检查,如果发现它有RTCF_BROADCAST或者RTCF_MULTICAST,就不会走常规的从myudp_hash中匹配源和目的地址,找到socket,把数据报放入接收队列这么一个流程。而是调用函数myudp_v4_mcast_deliver,这是一个专用于接收UDP组播数据报的函数,它首先根据目的端口确定在哈希表mydup_hash中的位置,然后遍历找到的这个链表。与普通的UDP数据报接收相比,它多一个过滤检查,即在套接字结构体的成员mc_list中找到与该数据报所属组对应的ip_mc_socklist项,查看它的过滤配置,确认该数据报的源地址是否在过滤列表中。如果不在,则把数据放到该socket的接收队列中,完成组播数据报的接收。
关于组播的其它几个选项
前面我们已经讲到了加入一个组播组的IP选项IP_ADD_MEMBERSHIP,关于组播的IP选项,除了这个,还有总共四个,它们分别是IP_DROP_MEMBERSHIP,IP_MULTICAST_IF,IP_MULTICAST_TTL,IP_MULTICAST_LOOP,下面分别一一介绍。
IP_DROP_MEMBERSHIP表示退出一个组播组,该选项最终会调用内核函数myip_mc_leave_group。该函数首先拿到结构体struct in_device,取走要离开的组的源过滤机制,即从in_device->mc_list中找到对应的组struct ip_mc_list,将其成员sfcount[sfmode]减一,然后从其成员sources中取走相应的过滤源。然后将in_device->mc_list中该组所在的节点的引用计数减一,如果引用计数已经减为零了,则清struct net_device和struct in_device中该组的记录。最后,套接字结构体struct inet_sock的成员mc_list中有关该组的节点也被删除。至此,完成离开一个组播组的操作,该选项的参数是结构体struct ip_mreq,同IP_ADD_MEMBERSHIP。
IP_MULTICAST_IF是一个用于确定提交组播报文的接口,它的参数也是struct ip_mreq,通过该参数指定发送组播报文所使用的本地IP地址和本地网络设备接口的索引号,用于发送组播数据报,这两个值确定后放在套接字的结构体struct inet_sock的成员mc_addr和mc_index中,以备发送组播数据报时查询。
IP_MULTICAST_TTL指定提交的组播报文的TTL,有效的TTL在0到255之间,该选项提供的参数会被赋给套接字结构体struct inet_sock的成员mc_ttl。以备发送组播数据报时查询。
IP_MULTICAST_LOOP使组播报文环路有效或无效,如果环路有效,则在发送组播报文的时候,会给环回接口也发一份。该值存放在套接字的结构体struct inet_sock的成员mc_loop中。
以上IP_MULTICAST_IF,IP_MULTICAST_TTL和IP_MULTICAST_LOOP三项都是跟组播报文发送相关的选项,在接下来的发送组播数据报的分析中会再次提到。
发送组播数据报(1)
我们还是以发送UDP的组播数据为例。其实发送一个UDP的组播数据报跟发送一个单播UDP数据报的差别并不大。
首先是在myudp_sendmsg函数中,如果发送接口的源地址没有确定,并且目的地址是组播地址的话,则源地址使用inet_sock->mc_addr。而发送接口的源地址首先是通过inet_sock->saddr来确定的,如果发现inet_sock->saddr为零,才会采用inet_sock->mc_addr的值。
通过前面的文章,我们可以了解到bind系统调用的作用就是为一个本地套接口指定发送源地址和接收地址(即把一个本地套接口绑定在一个本地网络设备接口上)。而组播选项IP_MULTICAST_IF用于指定组播数据报的发送接口,两者的功能似乎有些重复。bind影响的是inet_sock的成员rcv_saddr, saddr, sport,分别表示接收地址(输入数据报首部中指定该地址为目的地址的,将被接收),发送源地址(本地某个网络设备接口的地址),发送和接收的端口。对于单播的情况,显然rcv_saddr==saddr,因为一般来讲 ,一个应用程序总是使用一个网络设备接口进行数据的收发的。但如果应用程序非要把一个组播地址和端口绑定到一个本地套接口上,则bind系统调用会让rcv_addr=组播地址,sport=端口,而saddr等于0,但协议栈发送组播数据报必须要有一个本地网络设备接口,没有saddr,协议栈就不知道通过那个设备发送数据报,这个任务就留给了IP_MULTICAST_IF选项,它为inet_sock的成员mc_addr和mc_index赋值,指定本地接口用于发送组播数据报。
由上可得,如果我们的应用希望通过本地一个网络设备接口向网络发送组播数据报,而不关心接收该组的数据报(可能来自其它主机,在启动环路的情况下,也可能是来自自己),我们可以简单地通过bind把这个发送套接字绑定到一个本地接口,然后再向组播地址发送数据报即可,但这样的话,感觉就像是自己站在组外(不属于这个组)向组内发送数据报,源代码如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "my_inet.h"
#include <linux/in.h>
#define MAXBUF 256
#define PUERTO 5000
#define GRUPO "224.0.1.1"
int main(void)
{
int fd;
struct sockaddr_in srv,local;
char buf[MAXBUF];
memset( &srv, 0, sizeof(srv) );
srv.sin_family = MY_AF_INET;
srv.sin_port = htons(PUERTO);
local.sin_family = AF_INET;
local.sin_port = htons(16000);
inet_aton("172.16.48.2", &(local.sin_addr) );
if( inet_aton(GRUPO, &srv.sin_addr) < 0 ){
perror("inet_aton");
return -1;
}
if( (fd = socket( MY_AF_INET, SOCK_DGRAM, MY_IPPROTO_UDP) ) < 0 ){
perror("socket");
return -1;
}
if( bind( fd, (struct sockaddr *)&local, sizeof(local) ) < 0 ){
perror("bind:");
return -1;
}
while( fgets(buf, MAXBUF, stdin) ){
if( sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&srv, sizeof(srv)) < 0 ){
perror("recvfrom");
}else{
fprintf(stdout, "Enviado a %s: %s", GRUPO, buf);
}
}
}
因为bind系统调用把inet_sock的成员rcv_addr也置成了本地网络设备接口的地址172.16.48.2,所以这个程序只能发送数据报,不能够接收到来自组224.0.1.1的数据报。如果想要发送者也能接收,应该这样改程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include "my_inet.h"
#define MAXBUF 256
#define PUERTO 5000
#define GROUP "224.0.1.1"
int main(void)
{
int fd;
struct sockaddr_in srv,local;
struct in_addr if_req;
char buf[MAXBUF];
srv.sin_family = MY_AF_INET;
srv.sin_port = htons(PUERTO);
inet_aton(GROUP, &srv.sin_addr);
local.sin_family =MY_ AF_INET;
local.sin_port = htons(16000);
inet_aton(GROUP, &(local.sin_addr) );
if( (fd = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP) ) < 0 ){
perror("socket");
return -1;
}
if( bind( fd, (struct sockaddr *)&local, sizeof(local) ) < 0 ){
perror("bind:");
return -1;
}
inet_aton("172.16.48.2", &(if_req) );
if( setsockopt( fd, SOL_IP, IP_MULTICAST_IF, &if_req, sizeof(struct in_addr) ) < 0 ){
perror("setsockopt:");
return -1;
}
while( fgets(buf, MAXBUF, stdin) ){
if( sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&srv, sizeof(srv)) < 0 ){
perror("sendto");
}else{
fprintf(stdout, "Enviado a %s: %s", GROUP, buf);
}
}
}
这回,本地套接口被绑定在了一个组播地址上了,这样,这个应用程序不仅能够发送组播数据,也能够接受同组中发往16000端口的数据报了(最好再加个IP_ADD_MEMBERSHIP选项的操作),但bind组播地址时,只会设定接收地址为该组播地址,不会设定发送源地址,所以,必须使用IP_MULTICAST_IF接口指定一个发送接口(程序中指定了172.16.48.2,即eth0接口)。
这两个程序在现在的my_inet模块中均能够正常工作,但是它们调用的实际发送代码是UDP单播的代码,这基本能正常工作,但是单播的代码少了很多对组播的特殊处理,比如组播路由验证,环路发送等。
在下一篇,我们将为模块添加组播数据报发送的代码,并给出分析。
注:严格来讲,上述两个程序都是有问题的,程序1的套接口会收到发往本机172.16.48.2接口的16000端口的数据,并阻塞在套接口的接收队列中,程序2会收到发往组224.0.1.1的16000端口的数据,并阻塞在套接口的接收队列中。
发送组播数据报(2)
前面一篇文章中提到的两个示例程序,它们虽然对外发送了组播数据报,但它们实际上调用的是协议栈中的单播发送的代码。一般情况下,它们不会有什么问题,但是它们不是标准的组播程序,下面我们看看协议栈究竟是如何发送组播数据报的。
我们还是以发送UDP的组播数据报为例。前面已经讲过,IP选项IP_MULTICAST_IF确定组播发送的接口,在通过系统调用设置该选项时,参数只需要一个本地网络接口的IP地址即可,myudp_sendmsg函数在发送组播数据报时,会以该选项设定的IP地址作为输出路由查询的源地址。
对于一个输出组播数据报,协议栈也要做检查,检查该组播发送的接口是否也加入了同一个组播组(即检查net_device->in_device->mc_list链表,查看是否存在跟输出组播数据报目的地址相同的组),如果检查结果确实加入了同一个组(本机可能有其它进程在同一网络设备口上,在该组中接收数据报),则把组播输出函数指定为myip_mc_output,该函数与普通的IP数据报输出函数相比,多了一个判断,如果启用了组播环路,则先向loopback接口发送一个组播数据报,确保本机需要接收该组中的数据的进程能收到数据。组播环路缺省是打开的,可以通过IP选项IP_MULTICAST_LOOP进行设置。
组播数据报的TTL的缺省值是1,这在很多情况下,显然是不适用的,我们必须能够修改它,IP选项IP_MULTICAST_TTL可用来修改这个TTL值,该选项把它的参数值赋给套接字结构体的成员mc_ttl。协议栈在为待发送数据报构建IP首部时,发现该数据的目的地址是一个组播地址时,就会把mc_ttl的值填入IP首部的ttl域。
下面是一个组播客户端和一个组播服务端程序,让它们运行在同一台主机上,试着修改一些参数,你就能得到各种不同的行为。我在调试中发现一个问题,就是现有的Linux TCP/IP协议栈代码在往组播环路发数据报时,本机接收进程会收到两个数据包,也就是本示例中,服务端开启了环路以后,发一个数据报,客户端会收到两个,很有趣的问题,暂时没想通,欢迎大家探讨。
服务端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include "my_inet.h"
#define MAXBUF 256
#define PUERTO 5000
#define GROUP "224.0.1.1"
int main(void)
{
int fd, mc_loop = 1;
struct sockaddr_in srv,local;
struct in_addr if_req;
char buf[MAXBUF];
srv.sin_family = MY_AF_INET;
srv.sin_port = htons(PUERTO);
inet_aton(GROUP, &srv.sin_addr);
if( (fd = socket( MY_AF_INET, SOCK_DGRAM, MY_IPPROTO_UDP) ) < 0 ){
perror("socket");
return -1;
}
inet_aton("172.16.48.2", &(if_req) );
if( setsockopt( fd, SOL_IP, IP_MULTICAST_IF, &if_req, sizeof(struct in_addr) ) < 0 ){
perror("setsockopt:");
return -1;
}
if( setsockopt( fd, SOL_IP, IP_MULTICAST_LOOP, &mc_loop, sizeof(int) ) < 0 ){
perror("setsockopt:");
return -1;
}
while( fgets(buf, MAXBUF, stdin) ){
if( sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&srv, sizeof(srv)) < 0 ){
perror("sendto");
}else{
fprintf(stdout, "Enviado a %s: %s", GROUP, buf);
}
}
}
客户端程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "my_inet.h"
#include <arpa/inet.h>
#define MAXBUF 256
#define PUERTO 5000
#define GROUP "224.0.1.1"
int main(void)
{
int fd, n, r;
struct sockaddr_in srv, cli;
struct ip_mreq mreq;
char buf[MAXBUF];
srv.sin_family = MY_AF_INET;
srv.sin_port = htons(PUERTO);
inet_aton(GROUP, &srv.sin_addr );
if( (fd = socket( MY_AF_INET, SOCK_DGRAM, MY_IPPROTO_UDP) ) < 0 ){
perror("socket");
return -1;
}
if( bind(fd, (struct sockaddr *)&srv, sizeof(srv)) < 0 ){
perror("bind");
return -1;
}
/*
inet_aton( GROUP, &mreq.imr_multiaddr );
inet_aton( "172.16.48.2", &mreq.imr_interface );
if( setsockopt(fd, SOL_IP, IP_ADD_MEMBERSHIP, &mreq,sizeof(mreq)) < 0 ){
perror("setsockopt");
return -1;
}
if( setsockopt(fd, SOL_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0 ){
perror("setsockopt");
return -1;
}
*/
n = sizeof(cli);
while(1){
if( (r = recv(fd, buf, MAXBUF, 0)) < 0 ){
perror("recv:");
}else{
buf[r] = 0;
fprintf(stdout, "Mensaje desde: %s", buf);
}
}
}
struct ip_mreqn
{
struct in_addr imr_multiaddr; /* IP多点传送组地址 */
struct in_addr imr_address; /* 本地接口的IP地址 */
int imr_ifindex; /* 接口索引 */
};
imr_multiaddr 包含应用程序希望加入或者退出的多点广播组的地址. 它必须是一个有效的多点广播地址. imr_address 指的是系统用来加入多点广播组的本地接口地址;如果它与 INADDR_ANY 一致,那么由系统选择一个合适的接口. imr_ifindex 指的是要加入/脱离 imr_multiaddr 组的接口索引,或者设为0表示任何接口.
由于兼容性的缘故,老的 ip_mreq 接口仍然被支持.它与 ip_mreqn 只有一个地方不同,就是没有包括 imr_ifindex 字段.这只在作为一个 setsockopt(2) 时才有效.
IP_DROP_MEMBERSHIP
脱离一个多点广播组.参数为 ip_mreqn 或者 ip_mreq 结构,这与 IP_ADD_MEMBERSHIP 类似. T P IP_MULTICAST_IF 为多点广播套接字设置本地设备.参数为 ip_mreqn 或者 ip_mreq 结构,它与 IP_ADD_MEMBERSHIP 类似.
当传递一个无效的套接字选项时,返回 ENOPROTOOPT .