一、导言


  在网络中,主机间可以用三种不同的地址进行通信:


  单播地址(unicast):即在子网中主机的唯一地址(接口)。如IP地址:192.168.100.9或MAC地址:80:C0:F6:A0:4A:B1。


  广播地址:这种类型的地址用来向子网内的所有主机(接口)发送数据。如广播IP地址是192.168.100.255,MAC广播地址:FF:FF:FF:FF:FF。


  组播地址:通过该地址向子网内的多个主机即主机群(接口)发送数据。


  如果只是向子网内的部分主机发送报文,组播地址就很有用处了;在需要向多个主机发送多媒体信息(如实时音频、视频)的情况下,考虑到其所需的带宽,分别向每一客户端主机发送数据并不是个好办法,如果发送主机与某些接收端的客户主机不在子网之内,采用广播方式也不是一个好的解决方案。


  二、组播地址


  大家知道,IP地址空间被划分为A、B、C三类。第四类即D类地址被保留用做组播地址。在第四版的IP协议(IPv4)中,从224.0.0.0到239.255.255.255间的所有IP地址都属于D类地址。


  组播地址中最重要的是第24位到27位间的这四位,对应到十进制是224到239,其它28位保留用做组播的组标识,如下图所示:


  


  图1 组播地址示意图


  IPv4的组播地址在网络层要转换成网络物理地址。对一个单播的网络地址,通过ARP协议可以获取与IP地址对应的物理地址。但在组播方式下ARP协议无法完成类似功能,必须得用其它的方法获取物理地址。在下面列出的RFC文档中提出了完成这个转换过程的方法:


  RFC1112:Multicast IPv4 to Ethernet physical address correspondence


  RFC1390:Correspondence to FDDI


  RFC1469:Correspondence to Token-Ring networks


  在最大的以太网地址范围内,转换过程是这样的:将以太网地址的前24位最固定为01:00:5E,这几位是重要的标志位。紧接着的一位固定为0,其它23位用IPv4组播地址中的低23位来填充。该转换过程如下图所示:


  


  图2 地址转换示意图


  例如,组播地址为224.0.0.5其以太网物理地址为01:00:5E:00:00:05。


  还有一些特殊的IPv4组播地址:


  224.0.0.1:标识子网中的所有主机。同一个子网中具有组播功能的主机都是这个组的成员。


  224.0.0.2:该地址用来标识网络中每个具有组播功有的路由器。


  224.0.0.0----224.0.0.255范围内的地址被分配给了低层次的协议。向这些范围内的地址发送数据包,有组播功能的路由器将不会为其提供路由。


  239.0.0.0----239.255.255.255间的地址分配用做管理用途。这些地址被分配给局部的每一个组织,但不可以分配到组织外部,组织内的路由器不向在组织外的地址提供路由。


  除了上面列出的部分组播地址外,还有许多的组播地址。在最新版本的RFC文档“Assinged Numbers”中有完整的介绍。


  下面的表中列出了全部的组播地址空间,同时还列出了相应的地址段的常用名称及其TTL(IP包的存活时间)。在IPv4组播方式下,TTL有双重意义:正如大家所知的,TTL原本用来控制数据包在网络中的存活时间,防止由于路由器配置错误导致出现数据包传播的死循环;在组播方式下,它还代表了数据包的活动范围,如:数据包在网络中能够传送多远?这样就可以基于数据包的分类来定义其传送范围。


  范围 TTL 地址区间 描述


  节点(Node) 0 只能向本机发送的数据包,不能向网络中的其它接口传送


  链路(Link) 1 224.0.0.0-224.0.0.255 只能在发送主机所在的一个子网内的传送,不会通过路由器转发。


  部门 32 239.255.0.0-239.255.255.255 只在整个组织下的一个部门内(Department) 传送


  组织 64 239.192.0.0--239.195.255.255 在整个组织内传送(Organization)


  全局(Global)255 224.0.1.0--238.255.255.255 没有限制,可全局范围内传送


  三、组播的工作过程


  在局域网内,主机的网络接口将到目的主机的数据包发送到高层,这些数据包中的目的地址是物理接口地址或广播地址。


  如果主机已经加入到一个组播组中,主机的网络接口就会识别出发送到该组成员的数据包。


  因此,如果主机接口的物理地址为80:C0:F6:A0:4A:B1,其加入的组播组为224.0.1.10,则发送给主机的数据包中的目的地址必是下面三种类型之一:


  接口地址:80:C0:F6:A0:4A:B1


  广播地址:FF:FF:FF:FF:FF:FF:FF:FF


  组播地址:01:00:5E:00:01:0A


  广域网中,路由器必须支持组播路由。当主机中运行的进程加入到某个组播组中时,主机向子网中的所有组播路由器发送IGMP(Internet分组管理协议)报文,告诉路由器凡是发送到这个组播组的组播报文都必须发送到本地的子网中,这样主机的进程就可以接收到报文了。子网中的路由器再通知其它的路由器,这些路由器就知道该将组播报文转发到哪些子网中去。


  子网中的路由器也向224.0.0.1发送一个IGMP报文(224.0.0.1代表组中的全部主机),要求组中的主机提供组的相关信息。组中的主机收到这个报文后,都各将计数器的值设为随机值,当计数器递减为0时再向路由器发送应答。这样就防止了组中所有的主机同时向路由器发送应答,造成网络拥塞。主机向组播地址发送一个报文做为对路由器的应答,组中的其它主机一旦看到这个应答报文,就不再发送应答报文了,因为组中的主机向路由器提供的都是相同的信息,所以子网路由器只需得到组中一个主机提供的信息就可以了。


  如果组中的主机都退出了,路由器就收不到应答,因此路由器认为该组目前没有主机加入,遂停止到该子网报文的路由。IGMPv2的解决方案是:组中的主机在退出时向224.0.0.2 发送报文通知组播路由器。


  四、应用编程接口(API)


  如果你有套接字编程的经验,就会发现,对组播选项所进行的操作只需五个新的套接字操作。函数setsockopt()及getsockopt()用来建立和读取这五个选项的值。下表中列出了组播的可选项,并列出其数据类型和描述:


  IPv4 选项 数据类型 描 述


  IP_ADD_MEMBERSHIP struct ip_mreq 加入到组播组中


  IP_ROP_MEMBERSHIP struct ip_mreq 从组播组中退出


  IP_MULTICAST_IF struct ip_mreq 指定提交组播报文的接口


  IP_MULTICAST_TTL u_char 指定提交组播报文的TTL


  IP_MULTICAST_LOOP u_char 使组播报文环路有效或无效


  在头文件中定义了ip_mreq结构:


  struct ip_mreq {


  struct in_addr imr_multiaddr; /* IP multicast address of group */


  struct in_addr imr_interface; /* local IP address of interface */


  };


  在头文件中组播选项的值为:


  #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


  IP_ADD_MEMBERSHIP


  若进程要加入到一个组播组中,用soket的setsockopt()函数发送该选项。该选项类型是ip_mreq结构,它的第一个字段imr_multiaddr指定了组播组的地址,第二个字段imr_interface指定了接口的IPv4地址。


  IP_DROP_MEMBERSHIP


  该选项用来从某个组播组中退出。数据结构ip_mreq的使用方法与上面相同。


  IP_MULTICAST_IF


  该选项可以修改网络接口,在结构ip_mreq中定义新的接口。


  IP_MULTICAST_TTL


  设置组播报文的数据包的TTL(生存时间)。默认值是1,表示数据包只能在本地的子网中传送。


  IP_MULTICAST_LOOP


  组播组中的成员自己也会收到它向本组发送的报文。这个选项用于选择是否激活这种状态。




组播客户端代码如下:

#include <sys/types.h> 
 #include <sys/socket.h> 
 #include <arpa/inet.h> 
 #include <stdio.h> 
 #include <stdlib.h> 
 #include <string.h> 
 #define BUFLEN 255 
 int main(int argc, char **argv) 
 { 
 struct sockaddr_in peeraddr, myaddr; 
 int sockfd; 
 char recmsg[BUFLEN + 1]; 
 unsigned int socklen; 
 /* 创建 socket 用于UDP通讯 */ 
 sockfd = socket(AF_INET, SOCK_DGRAM, 0); 
 if (sockfd < 0) { 
 printf("socket creating error/n"); 
 exit(1); 
 } 
 socklen = sizeof(struct sockaddr_in); 
 /* 设置对方的端口和IP信息 */ 
 memset(&peeraddr, 0, socklen); 
 peeraddr.sin_family = AF_INET; 
 if (argv[2]) 
 peeraddr.sin_port = htons(atoi(argv[2])); 
 else 
 peeraddr.sin_port = htons(7838); 
 if (argv[1]) { 
 /* 注意这里设置的对方地址是指组播地址,而不是对方的实际IP地址 */ 
 if (inet_pton(AF_INET, argv[1], &peeraddr.sin_addr) <= 0) { 
 printf("wrong group address!/n"); 
 exit(0); 
 } 
 } else { 
 printf("no group address!/n"); 
 exit(0); 
 } 
 /* 设置自己的端口和IP信息 */ 
 memset(&myaddr, 0, socklen); 
 myaddr.sin_family = AF_INET; 
 if (argv[4]) 
 myaddr.sin_port = htons(atoi(argv[4])); 
 else 
 myaddr.sin_port = htons(23456); 
 if (argv[3]) { 
 if (inet_pton(AF_INET, argv[3], &myaddr.sin_addr) <= 0) { 
 printf("self ip address error!/n"); 
 exit(0); 
 } 
 } else 
 myaddr.sin_addr.s_addr = INADDR_ANY; 
 /* 绑定自己的端口和IP信息到socket上 */ 
 if (bind 
 (sockfd, (struct sockaddr *) &myaddr, 
 sizeof(struct sockaddr_in)) == -1) { 
 printf("Bind error/n"); 
 exit(0); 
 } 
 /* 循环接受用户输入的消息发送组播消息 */ 
 for (;;) { 
 /* 接受用户输入 */ 
 bzero(recmsg, BUFLEN + 1); 
 if (fgets(recmsg, BUFLEN, stdin) == (char *) EOF) 
 exit(0); 
 /* 发送消息 */ 
 if (sendto 
 (sockfd, recmsg, strlen(recmsg), 0, 
 (struct sockaddr *) &peeraddr, 
 sizeof(struct sockaddr_in)) < 0) { 
 printf("sendto error!/n"); 
 exit(3); 
 } 
 printf("'%s' send ok/n", recmsg); 
 } 
 } 
 组播服务器端程序源代码为: 
 #include <sys/types.h> 
 #include <sys/socket.h> 
 #include <arpa/inet.h> 
 #include <stdio.h> 
 #include <stdlib.h> 
 #include <string.h> 
 #include <netdb.h> 
 #include <errno.h> 
 #define BUFLEN 255 
 int main(int argc, char **argv) 
 { 
 struct sockaddr_in peeraddr; 
 struct in_addr ia; 
 int sockfd; 
 char recmsg[BUFLEN + 1]; 
 unsigned int socklen, n; 
 struct hostent *group; 
 struct ip_mreq mreq; 
 /* 创建 socket 用于UDP通讯 */ 
 sockfd = socket(AF_INET, SOCK_DGRAM, 0); 
 if (sockfd < 0) { 
 printf("socket creating err in udptalk/n"); 
 exit(1); 
 } 
 /* 设置要加入组播的地址 */ 
 bzero(&mreq, sizeof(struct ip_mreq)); 
 if (argv[1]) { 
 if ((group = gethostbyname(argv[1])) == (struct hostent *) 0) { 
 perror("gethostbyname"); 
 exit(errno); 
 } 
 } else { 
 printf 
 ("you should give me a group address, 224.0.0.0-239.255.255.255/n"); 
 exit(errno); 
 } 
 bcopy((void *) group->h_addr, (void *) &ia, group->h_length); 
 /* 设置组地址 */ 
 bcopy(&ia, &mreq.imr_multiaddr.s_addr, sizeof(struct in_addr)); 
 /* 设置发送组播消息的源主机的地址信息 */ 
 mreq.imr_interface.s_addr = htonl(INADDR_ANY); 
 /* 把本机加入组播地址,即本机网卡作为组播成员,只有加入组才能收到组播消息 */ 
 if (setsockopt 
 (sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, 
 sizeof(struct ip_mreq)) == -1) { 
 perror("setsockopt"); 
 exit(-1); 
 } 
 socklen = sizeof(struct sockaddr_in); 
 memset(&peeraddr, 0, socklen); 
 peeraddr.sin_family = AF_INET; 
 if (argv[2]) 
 peeraddr.sin_port = htons(atoi(argv[2])); 
 else 
 peeraddr.sin_port = htons(7838); 
 if (argv[1]) { 
 if (inet_pton(AF_INET, argv[1], &peeraddr.sin_addr) <= 0) { 
 printf("Wrong dest IP address!/n"); 
 exit(0); 
 } 
 } else { 
 printf("no group address given, 224.0.0.0-239.255.255.255/n"); 
 exit(errno); 
 } 
 /* 绑定自己的端口和IP信息到socket上 */ 
 if (bind 
 (sockfd, (struct sockaddr *) &peeraddr, 
 sizeof(struct sockaddr_in)) == -1) { 
 printf("Bind error/n"); 
 exit(0); 
 } 
 /* 循环接收网络上来的组播消息 */ 
 for (;;) { 
 bzero(recmsg, BUFLEN + 1); 
 n = recvfrom(sockfd, recmsg, BUFLEN, 0, 
 (struct sockaddr *) &peeraddr, &socklen); 
 if (n < 0) { 
 printf("recvfrom err in udptalk!/n"); 
 exit(4); 
 } else { 
 /* 成功接收到数据报 */ 
 recmsg[n] = 0; 
 printf("peer:%s", recmsg); 
 } 
 } 
 }