如何创建组播应用程序(一)



 本文试图成为学习TCP/IP网络组播技术的入门材料。文中介绍了组播通信的概念及原理,以及用于组播应

用编程的 Linux API的详细资料。为了使读者更加完整的了解Linux 组播的整体概念,文中对实现该技术的核

心函数也做了介绍。在文章的最后给出了一个简单的C语言套接字编程例子,说明如何创建组播应用程序。

  
    一、导言
  
    在网络中,主机间可以用三种不同的地址进行通信:
  
    单播地址(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
  
    组播组中的成员自己也会收到它向本组发送的报文。这个选项用于选择是否激活这种状态。
  
    五、一个组播通信的例子
  
    下面给出一个简单的例子实现文中阐述的思想:由一个进程向一个组播组发送报文,组播组中

的相关进程接收报文,并将报文显示到屏幕上。
  
    下面的代码实现了一个服务进程,它将标准输入接口输入的信息全部发送到组播组224.0.1.1。

你会发现,将信息发送到组播组不需要特别的操作,只要设置好组播组的目的地址就足够了。若在开发过

程中,Loopback和TTL这两个选项的默认值不适合应用程序,可以加以调整。
  
    服务程序
  
    将标准输入端口的输入发送到组播组224.0.1.1。
  
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #define MAXBUF 256
  #define PUERTO 5000
  #define GRUPO "224.0.1.1"
  int main(void) {
  int s;
  struct sockaddr_in srv;
  char buf;
  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
  ", GRUPO, buf);
  }
  }
  }
  
    客户端程序
  
    下面的代码是客户端程序,它负责接收由服务程序发送到组播组中的信息,将收到的报文在标准

输出设备中显示。程序中唯一与接收UDP报文过程不同是它设置了IP_ADD_MEMBERSHIP选项。
  
  #include 
  #include 
  #include 
  #include 
  #include 
  #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;
  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 = 0;
  fprintf(stdout, "Mensaje desde %s: %s
  ",
  inet_ntoa(cli.sin_addr), buf);
  }
  }
  }
  
    六、内核与组播
  
    在上面的例子中我们看到:如果一个进程要加入到组播组中,就要使用setsockopt()函数在IP

层设置IP_ADD_MEMBERSHIP。 
  
  在/usr/src/linux/net/ipv4/ip_sockglue.c文件中可以找见该函数的源代码。 其中设置IP_ADD_MEMBERSHIP

和IP_DROP_MEMBERSHIP的部分代码如下:
  
  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);
  
    程序一开始先检查输入参数ip_mreq结构的长度是否正确,并将其从用户区复制到内核区。在得到参数

的值后,接着调用ip_mc_join_group()加入到组

播组或调用ip_mc_leave_group()退出组播组。
  
  在/usr/src/linux/net/ipv4/igmp.c中可以找到这些函数的代码。加入组播组的源程序代码如下:
  
  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;
  
    在开始部分用MULTICAST宏检查组的地址,确认其在保留的组播组地址范围内。只要检查IP地

址的第一部分是不是224就可以确认地址是否有效:
  
  if (!MULTICAST(addr))
  return -EINVAL;
  rtnl_shlock();
  
    检查完组播地址后,接着就要设置网络接口了。如果不能通过接口索引获得网络接口(如在IPv6下)

,在这种情况下可以调用函数 ip_mc_find_dev()获取网络接口。在本文中不存在这个问题,因为我们的工作

都是在IPv4下进行的。若地址的格式是INADDR_ANY,内核就依照路由表的定义,按照组地址在路由表中

查找网络接口。
  
  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;
  }
  
    接着给ip_mc_socklist结构分配内存,然后比较套接字的每个组地址和接口。只要发现了一个匹配项就

跳出该函数,因为有一个匹配项就可以了。若网络接口地址不是INADDR_ANY,相应的计数器值就要增加。
  
  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;
  
    到这里,就可以用新创建的套接字与组播组建立链接了,这时还必须创建一个新的记录,记录下属于

该套接字的组的列表。首先还是要预先分配内存,然后只要给相关结构中的几个字段赋值,就完成了这个操作:
  
  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;
  }
  
    用函数ip_mc_leave_group()从一个组播组中退出,它的工作过程比前面的函数要来得简单。首先在

套接字记录中查找组播组及接口地址,找到后,将调用这个接口地址的进程数的值递减,若该值为0,就删除

该计数器,因为与组播组相关的进程至少要有一个。
  
  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;
  }
  
    其它的组播选项都很简单,只要给当前套接字内的字段赋值就可以了,赋值的过程由ip_setsockopt()函数完成。


你可能感兴趣的:(如何创建组播应用程序(一))