http://blog.chinaunix.net/uid-27197466-id-3477784.html
http://www.cnblogs.com/my_life/articles/6085848.html
IP选项IP_MULTICAST_IF确定组播发送的接口,在通过系统调用设置该选项时,参数只 需要一个本地网络接口的IP地址即可,myudp_sendmsg函数在发送组播数据报时,会以该选项设定的IP地址作为输出路由查询的源地址。
组播环路缺省是打开的,可以通过IP选项IP_MULTICAST_LOOP进行设置。
播数据报的TTL的缺省值是1,这在很多情况下,显然是不适用的,我们必须能够修改它,IP选项IP_MULTICAST_TTL可用来修改这个TTL 值,该选项把它的参数值赋给套接字结构体的成员mc_ttl。协议栈在为待发送数据报构建IP首部时,发现该数据的目的地址是一个组播地址时,就会把 mc_ttl的值填入IP首部的ttl域。
IP_MULTICAST_IF是一个用于确定提交/发送组播报文的接口,它的参数也是struct ip_mreq,通过该参数指定发送组播报文所使用的本地IP地址和本地网络设备接口的索引号,用于发送组播数据报,这两个值确定后放在套接字的结构体 struct inet_sock的成员mc_addr和mc_index中,以备发送组播数据报时查询。
IP_MULTICAST_LOOP使组播报文环路有效或无效,如果环路有效,则在发送组播报文的时候,会给环回接口也发一份。该值存放在套接字的结构体struct inet_sock的成员mc_loop中。
IP_ADD_MEMBERSHIP :通过加入组播组的操作后,网络设备接口已经知道要接收该组的数据报,所以组播数据会从网卡接收进来
bind系统调用的作用就是为一个本地套接口socket指定发送源地址和接收地址(即把一个本地套接口绑定在一个本地网络设备接口 上)。而组播选项IP_MULTICAST_IF用于指定组播数据报的发送接口,两者的功能似乎有些重复。
bind影响的是inet_sock的成员 rcv_saddr, saddr, sport,分别表示接收地址(输入数据报首部中指定该地址为目的地址的,将被接收),发送源地址(本地某个网络设备接口的地址,用于构造输出数据包的源地址),发送和接收的端口。
rcv_saddr:我将只从本地地址为rcv_saddr的网卡接收数据包。 存在于服务端的代码servaddr.sin_addr.s_addr = htonl(INADDR_ANY),servaddr.sin_port = htons(SERV_PORT); 代表服务器将接收任何网卡发向SERV_PORT的数据
saddr: 我向外发送数据包时,将由该地址所代表的网卡向外发送数据。存在于客户端的代码clientaddr.sin_addr.s_addr = htonl(“CLIENT_ADDR”),clientaddr.sin_port = htons(0); 代表客户端发出去的报文,其地址是CLIENT_ADDR,端口是系统任意分配的
对 于单播的情况,显然rcv_saddr==saddr,因为一般来讲 ,一个应用程序总是使用同一个网络设备接口进行数据的收发的。
http://stackoverflow.com/questions/10692956/what-does-it-mean-to-bind-a-multicast-udp-socket
但如果应用程序非要把一个组播地址和端口绑定到一个本地套接口上,则bind系统调用会让 rcv_addr=组播地址(我只接收该地址发来的组播数据),saddr = 0,sport=端口,而saddr等于0,但协议栈发送组播数据报必须要有一个本地网络设备接口(组播地址肯定不等于本机网卡地址),没有saddr,协议栈就不 知道通过那个设备发送数据报,这个任务就留给了IP_MULTICAST_IF选项,它为inet_sock的成员mc_addr和mc_index赋 值,指定本地接口用于发送组播数据报。
对于UDP组播而言,bind(本地IP,port)不再适用,不是绑定本地local地址的效果,而是一种过滤规则,只接收来自该地址和端口的组播数据。
bind之后,必须要有 setsockopt(fd, SOL_IP, IP_ADD_MEMBERSHIP, &mreq,sizeof(mreq))该动作,因为bind()操作对于组播,它会把saddr置为0,此时系统就无从下手,不知道选择哪个网卡来接收组播数据。
对于接收方socket,如果我们将其bind(239.255.1.2:8888),我们就只能接收发往地址为239.255.1.2,端口为8888的组播数据;而不能接收到地址为239.255.1.3端口为8888的组播数据,即使IP_ADD_MEMBERSHI加入了该组播地址。
同样,对于组播数据的发送方,如果也调用了bind()操作,会导致saddr = 0; 这会使用系统默认的网卡发送组播数据。如果需要指定某个网卡来发送数据,需要setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof(addr))来设置saddr.
To bind a UDP socket when receiving multicast means to specify an address and port from which to receive data (NOT a local interface, as is the case for TCP acceptor bind). The address specified in this case has a filtering role, i.e. the socket will only receive datagrams sent to that multicast address & port, no matter what groups are subsequently joined by the socket. This explains why when binding to INADDR_ANY (0.0.0.0) I received datagrams sent to my multicast group, whereas when binding to any of the local interfaces I did not receive anything, even though the datagrams were being sent on the network to which that interface corresponded.
Quoting from UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking API by W.R Stevens. 21.10. Sending and Receiving
[...] We want the receiving socket to bind the multicast group and port, say 239.255.1.2 port 8888. (Recall that we could just bind the wildcard IP address and port 8888, but binding the multicast address prevents the socket from receiving any other datagrams that might arrive destined for port 8888.) We then want the receiving socket to join the multicast group. The sending socket will send datagrams to this same multicast address and port, say 239.255.1.2 port 8888.
binding to "0.0.0.0" helped me receive traffic, as opposed to binding to the local interface IP for the network over which traffic was sent, which did not help.
http://www.cnblogs.com/my_life/articles/4366085.html
#include
#include
#include
#include
#include "my_inet.h"
#include
#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 ){ //应用socket本地绑定规则,由指定的网卡接收数据,由系统默认的网卡发送数据; rcv_saddr = saddr = local.addr
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
#include
#include
#include
#include
#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 ){ //应用组播源地址过滤规则,只接收发向本地端口16000,且地址是224.0.1.1的组播数据。这会导致saddr = 0; rcv_saddr = local.addr。
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单播的代码,这基本能正常工作,但是单播的代码少了很多对组播的特殊处理,比如组播路由验证,环路发送等。
在下一篇,我们将为模块添加组播数据报发送的代码,并给出分析。
注:
我们先从一个组播客户端的应用程序入手来解析组播的工作过程:
#include
#include
#include
#include
#include "my_inet.h"
#include
#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 ){ //应用组播源地址过滤规则,只接收发向端口PUERTO,且地址是GRUPO的组播数据。这会导致saddr = 0; rcv_saddr = local.addr。根据上面的英文资料,可能会收不到其他组发往PUERTO端口的组播数据。 最好还是bind(INADDR_ANY:组播端口)
perror("bind");
return -1;
}
if (inet_aton(GRUPO, &mreq.imr_multiaddr) < 0) { //在 172.16.48.2所在的设备接口上加入组播组224.0.1.1。
perror("inet_aton");
return -1;
}
inet_aton( "172.16.48.2", &(mreq.imr_interface) ); //在 172.16.48.2所在的设备接口上加入组播组224.0.1.1。
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);
}
}
}
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) 保证可以绑定同一端口
bind(INADDR_ANY: 组播端口)
struct ip_mreq {
struct in_addr imr_multiaddr; /* multicast group to join */ //待加入的组播地址
struct in_addr imr_interface; /* interface to join on */ INADDR_ANY:使用安装系统时指定默认网卡作为的组播网卡,或者指定一个本地网卡的地址覆盖默认值
}
setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) //
这是一个非常简单的组播客户端,它指定从组播组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; //发出去的数据包由该地址所代表的网卡发出去。为0,系统(在进行路由选择时)任意选一个地址填充发出去的数据包
.sport = 5000; //通过该端口发送接收数据
.daddr = 0.0.0.0; //目的地址不确定,connect()或sendto()时会被填充
.dport = 0;
}
这五个数据表示,该套接字在发送数据包时,本地使用端口5000,本地可以使用任意一个网络设备接口,发往的目的地址不指定。在接收数据时,只接收发往IP地址224.0.0.1的端口为5000的数据。
程序中,紧接着bind有一个setsockopt操作,它的作用是将socket加入一个组播组(用来指示使用哪个网卡接收哪个组播组的数据),因为socket要接收组播地址224.0.0.1的数据,它就必须加入该组播组。
对于UDP组播而言,bind(本地IP,port)不再适用,不是绑定本地local地址的效果,而是一种过滤规则,只接收来自该地址和端口的组播数据。
bind之后,必须要有setsockopt该动作,因为bind()操作对于组播,它会把saddr置为0,此时系统就无从下手,不知道选择哪个网卡来接收组播数据。
同样,对于组播数据的发送方,如果也调用了bind()操作,会导致saddr = 0; 这会使用系统默认的网卡发送组播数据。如果需要指定某个网卡来发送数据,需要setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof(addr))来设置saddr.
程序中,紧接着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已经可以接收组播数据了,关于组播数据如何接收,下回分解。