组播原理及 C语言编程


源自:http://blog.163.com/mh_333333333/blog/static/3585491220095199419684/

摘要:  

  本文可做为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位保留用做组播的组标识,如下图所示:   

组播 - mh_333333333 - mh_333333333的博客

  图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位来填充。该转换过程如下图所示: 

组播 - mh_333333333 - mh_333333333的博客

  图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  <sys/types.h>

  #include  <sys/socket.h>

  #include  <netinet/in.h>

  #include  <string.h>

  #include  <stdio.h>

  #include  <arpa/inet.h>

  #define MAXBUF 256

  #define PUERTO 5000

  #define GROUP "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);

  }

  }

  }

 客户端程序

    #include  <sys/types.h>

  #include  <sys/socket.h>

  #include  <netinet/in.h>

  #include  <string.h>

  #include  <stdio.h>

  #include  <arpa/inet.h>

  #define MAXBUF 256

  #define PUERTO 5000

  #define GROUP "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()函数完成。


  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C语言编程可以模拟地址变换机构原理,主要是通过指针和数组来实现。 先介绍一下地址变换机构原理:当CPU执行指令访问内存时,需要通过地址变换机构将逻辑地址(也就是程序中使用的地址)转换为物理地址(实际在内存中对应的地址)。这个过程中,需要使用页表、段表等数据结构来实现地址转换。 在C语言中,我们可以使用指针来模拟地址变换机构。例如,定义一个指针变量p,它指向一个数组a。当我们使用p[i]来访问数组元素时,实际上是将逻辑地址i转换为物理地址a+i,然后再访问对应的内存单元。 下面是一个简单的示例代码,模拟了地址变换机构的过程: ```c #include <stdio.h> int main() { int a[4] = {1, 2, 3, 4}; int *p = a; // p指向数组a的首地址 printf("a[0] = %d\n", a[0]); printf("p[0] = %d\n", p[0]); // 指针p可以访问数组a的元素 // 模拟地址变换机构的过程 int i = 2; // 逻辑地址 int *page_table = a; // 假设页表就是数组a int physical_address = page_table[i] + (int)p; // 物理地址 int value = *(int *)physical_address; // 访问物理地址对应的内存单元 printf("a[%d] = %d\n", i, value); return 0; } ``` 在这个示例代码中,我们假设页表就是数组a,并且通过指针p来模拟CPU访问内存的过程。通过逻辑地址i、页表和指针p,我们可以计算出物理地址physical_address,然后使用指针来访问对应的内存单元。 当然,这只是一个简单的示例,实际上地址变换机构的实现要复杂得多。但是通过这种方式,我们可以更加深入地理解地址变换机构的原理。 ### 回答2: C语言编程可以模拟地址变换机构原理。地址变换机构是计算机中的一项重要功能,它负责将逻辑地址转换为物理地址,进而实现内存的访问和管理。 在C语言中,我们可以使用指针来模拟地址变换机构的功能。指针是一种变量,它存储的是内存地址。通过操作指针,我们可以实现内存地址之间的转换。 首先,我们需要定义一个指针变量,用来存储逻辑地址。可以使用C语言中的指针类型来声明指针变量,并使用取地址符号"&"来获取变量的内存地址。 然后,我们可以通过指针变量来获取或修改指定内存地址上的值。可以使用C语言中的解引用符号"*"来访问指针指向的内存地址上的值。 接着,我们可以使用位运算来模拟地址转换的过程。地址变换机构通常使用位运算来实现逻辑地址到物理地址的转换,例如使用位移操作来映射内存块的起始地址。 最后,我们可以通过打印指针变量的值来模拟地址变换的结果。可以使用C语言中的printf函数来打印指针变量的值,进而观察逻辑地址到物理地址的转换过程。 通过以上步骤,我们可以使用C语言编程来模拟地址变换机构的原理。这种模拟能够帮助我们更加深入地理解地址变换机构的工作原理,以及内存管理的过程。 ### 回答3: C语言编程可以模拟地址变换机构的原理。地址变换机构是计算机系统中用于将虚拟地址转换成物理地址的重要组成部分。 在C语言中,我们可以使用指针来模拟地址变换机构的功能。首先,我们可以声明一个指针变量来保存虚拟地址和物理地址的对应关系。通过对指针变量进行赋值操作,我们可以将虚拟地址映射到对应的物理地址上。 接下来,我们可以使用指针解引用的方式来模拟地址变换机构的查找功能。当我们根据虚拟地址访问内存时,可以通过解引用指针来获取对应的物理地址,从而访问到实际存储在内存中的数据。 此外,我们还可以使用指针的加减运算来模拟地址变换机构的地址映射功能。通过执行指针的加减运算,我们可以实现虚拟地址到物理地址的转换。 需要注意的是,在C语言中,地址变换机构的各种功能都需要我们手动进行模拟和实现,而在实际的计算机系统中,这些功能是由硬件实现的。因此,在实际的地址变换机构中,我们不能直接使用C语言来进行模拟。 综上所述,通过使用指针和相关的操作,我们可以在C语言中模拟地址变换机构的原理,实现虚拟地址到物理地址的转换和访问。但需要注意的是,这仅仅是一种模拟和抽象,实际的地址变换机构操作是由硬件实现的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值