组播编程文档格式.docx
《组播编程文档格式.docx》由会员分享,可在线阅读,更多相关《组播编程文档格式.docx(13页珍藏版)》请在冰豆网上搜索。
structtimer_listmr_gq_timer;
/*generalquerytimer*/
structtimer_listmr_ifc_timer;
/*interfacechangetimer*/
structneigh_parms*arp_parms;
structipv4_devconfcnf;
structrcu_headrcu_head;
};
我们暂时还无法完全理解这个结构体,目前只需关注的是mc_list成员,这是关于组播的一个最为关键的数据结构。
mc_list是一个链表,链表的一个结点代表一个组播地址(也就是一个多播组的组号),代表这个网络设备接口已经加入了这个组播组,需要接收来自这个组的数据报。
下面是该节点的结构体定义:
structip_mc_list
structin_device*interface;
unsignedlongmultiaddr;
structip_sf_list*sources;
structip_sf_list*tomb;
unsignedintsfmode;
unsignedlongsfcount[2];
structip_mc_list*next;
structtimer_listtimer;
intusers;
spinlock_tlock;
chartm_running;
charreporter;
charunsolicit_count;
charloaded;
unsignedchargsquery;
unsignedcharcrcount;
同样,我们把其中的大部分成员留待以后理解。
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组,结果当然是没有。
则,首先创建一个新的结构体structip_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,加入一个网络设备级的组播地址。
也就是说,代表网络设备接口的结构体structnet_device有一个成员mc_list,它是一个链表,每个结点代表一个组播组的mac地址。
与in_device的mc_list中的组播IP地址对应。
loaded为0时,我们要做的事情就是把IP地址224.0.0.1映射成一个mac地址加到net_device的mc_list链表中去,然后把loaded置1,该成员的结点定义如下:
structdev_mc_list
structdev_mc_list*next;
__u8dmi_addr[MAX_ADDR_LEN];
unsignedchardmi_addrlen;
intdmi_users;
intdmi_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>
sys/types.h>
sys/socket.h>
string.h>
#include"
my_inet.h"
arpa/inet.h>
#defineMAXBUF256
#definePUERTO5000
#defineGRUPO"
224.0.1.1"
intmain(void)
intfd,n,r;
structsockaddr_insrv,cli;
structip_mreqmreq;
charbuf[MAXBUF];
memset(&
srv,0,sizeof(structsockaddr_in));
cli,0,sizeof(structsockaddr_in));
mreq,0,sizeof(structip_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){
socket"
if(bind(fd,(structsockaddr*)&
srv,sizeof(srv))<
bind"
if(inet_aton(GRUPO,&
mreq.imr_multiaddr)<
0)
inet_aton("
172.16.48.2"
&
(mreq.imr_interface));
if(setsockopt(fd,SOL_IP,IP_ADD_MEMBERSHIP,&
mreq,sizeof(mreq))<
0)
setsockopt"
n=sizeof(cli);
while
(1)
if((r=recvfrom(fd,buf,MAXBUF,0,(structsockaddr*)&
cli,(socklen_t*)&
n))<
recvfrom"
else
buf[r]=0;
fprintf(stdout,"
Mensajedesde%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本身,下列值受影响:
structinet_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的数据,它就必须加入该组播组。
结构体structip_mreqmreq是该操作的参数,下面是其定义:
structip_mreq
structin_addrimr_multiaddr;
//组播组的IP地址。
structin_addrimr_interface;
//本地某一网络设备接口的IP地址。
一台主机上可能有多块网卡,接入多个不同的子网,imr_interface参数就是指定一个特定的设备接口,告诉协议栈只想在这个设备所在的子网中加入某个组播组。
有了这两个参数,协议栈就能知道:
在哪个网络设备接口上加入哪个组播组。
为了简单起见,我们的程序中直接写明了IP地址:
在172.16.48.2所在的设备接口上加入组播组224.0.1.1。
这个操作是在网络层上的一个选项,所以级别是SOL_IP,IP_ADD_MEMBERSHIP选项把用户传入的参数拷贝成了structip_mreqn结构体:
structip_mreqn
structin_addrimr_address;
intimr_ifindex;
多了一个输入接口的索引,暂时被拷贝成零。
该操作最终引发内核函数myip_mc_join_group执行加入组播组的操作。
首先检查imr_multiaddr是否为合法的组播地址,然后根据imr_interface的值找到对应的structin_device结构。
接下来就要为socket加入到组播组了,在inet_sock的结构体中有一个成员mc_list,它是一个结构体structip_mc_socklist的链表,每一个节点代表socket当前正加入的一个组播组,该链表是有上限限制的,缺省值为IP_MAX_MEMBERSHIPS(20),也就是说一个socket最多允许同时加入20个组播组。
下面是structip_mc_socklist的定义:
structip_mc_socklist
structip_mc_socklist*next;
structip_mreqnmulti;
/*MCAST_{INCLUDE,EXCLUDE}*/
structip_sf_socklist*sflist;
structip_sf_socklist
unsignedintsl_max;
unsignedintsl_count;
__u32sl_addr[0];
除了multi成员,它还有一个源过滤机制。
如果我们新添加的structip_mreqn已经存在于这个链表中(表示socket早就加入这个组播组了),那么不做任何事情,否则,创建一个新的structip_mc_socklist:
.next=inet->
mc_list;
//新节点放到链表头。
.multi=传入的参数;
//这是关键的组信息。
.sfmode=MCAST_EXCLUDE;
//过滤掉sflist中的所有源。
.sflist=NULL;
//没有源需要过滤。
最后,调用myip_mc_inc_group函数在structin_device和structnet_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对这个组播数据报作检查,从网络设备接口的structin_device中去匹配组播地址,如果匹配不到,表示这个不是我们希望接收的组播包,丢弃。
匹配到了,则作下一步检查,如果这本身就是一个IGMP包,则接收,否则,查看这个组播组在我们的structin_device中设置的过滤机制,如果该数据报的源地址在我们的过滤名单中,则丢弃,否则接收。
如果检查通过,准备接收这个组播包,则调用myip_route_input_mc查找组播输入路由,这是一个专门为组播设置的函数,它第一步要检查数据报源地址的有效性,即源地址不能是组播地址,不能是广播地址,也不能是回环地址,同时,该数据报必须是一个因特网协议包(ETH_P_IP)。
如果源地址为0,那么只有当目的地址是224.0.0.0-224.0.0.255之间的值(只能在发送主机所在的一个子网内的传送,不会通过路由器转发。
)时,系统可以自己选定一个scope为RT_SCOPE_LINK的源地址,否则出错。
当验证了源地址的有效性之后,我们建立路由项,即结构体structrtable。
该路由项的rt_type值是RTN_MULTICAST,表示这一条组播路由。
对于本地接收的组播包,我们设置接收函数为myip_local_deliver。
有了这个路由项,我们可以通过调用myip_local_deliver,继续接收流程,这部分流程前面已有多次介绍,所以讲得简单一点,只注意组播特有的。
同样,到myip_local_deliver_finish后,首先要检查是否有rawsocket要接收这个组播包。
然后根据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。
该函数首先拿到结构体structin_device,取走要离开的组的源过滤机制,即从in_device->
mc_list中找到对应的组structip_mc_list,将其成员sfcount[sfmode]减一,然后从其成员sources中取走相应的过滤源。
然后将in_device->
mc_list中该组所在的节点的引用计数减一,如果引用计数已经减为零了,则清structnet_device和structin_device中该组的记录。
最后,套接字结构体structinet_sock的成员mc_list中有关该组的节点也被删除。
至此,完成离开一个组播组的操作,该选项的参数是结构体structip_mreq,同IP_ADD_MEMBERSHIP。
IP_MULTICAST_IF是一个用于确定提交组播报文的接口,它的参数也是structip_mreq,通过该参数指定发送组播报文所使用的本地IP地址和本地网络设备接口的索引号,用于发送组播数据报,这两个值确定后放在套接字的结构体structinet_sock的成员mc_addr和mc_index中,以备发送组播数据报时查询。
IP_MULTICAST_TTL指定提交的组播报文的TTL,有效的TTL在0到255之间,该选项提供的参数会被赋给套接字结构体structinet_sock的成员mc_ttl。
以备发送组播数据报时查询。
IP_MULTICAST_LOOP使组播报文环路有效或无效,如果环路有效,则在发送组播报文的时候,会给环回接口也发一份。
该值存放在套接字的结构体structinet_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