UNIX网络编程卷一 学习笔记 第二十一章 多播

单播地址标识单个IP接口,广播地址标识某个子网的所有IP接口,而多播地址标识一组IP接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折衷方案。多播数据报只应由对它感兴趣的接口接收,即由运行相应多播会话应用系统的主机上的接口接收。广播一般局限于局域网内使用,而多播既可用于局域网,也可跨广域网使用。事实上,基于MBone(Multicast Backbone,一种基于IP多播(IP Multicast)的网络基础设施,旨在支持多播数据传输,基于MBone构建的应用系统利用多播技术,在互联网上实现了实时音频、视频和会议等多媒体应用)的应用系统每天都在跨整个因特网多播。

套接字API为支持多播增加了9个套接字选项,其中3个影响目的地址为多播地址的UDP数据报的发送,另外6个影响主机对于多播数据报的接收。

IPv4的D类地址(224.0.0.0到239.255.255.255)是IPv4多播地址。D类地址的低28位构成多播组ID,整个32位地址称为组地址。

下图是从IPv4和IPv6多播地址映射到以太网地址的方法:
在这里插入图片描述
IPv4多播地址到以太网地址的映射见RFC 1112,到FDDI(Fiber Distributed Data Interface,光纤分布式数据接口,它是一种基于光纤的局域网(LAN)技术)网络地址的映射见RFC 1390,到令牌环网地址的映射见RFC 1469。

对于IPv4广播地址映射到的以太网地址,高24位总是01:00:5e,然后下一位总是0,低序23位复制自多播组ID的低序23位。多播组的高序5位在映射过程中被忽略,这意味着32个多播地址映射成一个以太网地址,映射关系不是一对一的。

广播(全1的以太网地址)和多播的以太网地址的首字节中最低位为1。

IPv4广播地址映射的以太网地址高位的24位被分配为01:00:5e,这样接收接口会特殊识别和处理该类组地址。

以下是几个特殊的IPv4多播地址:
1.224.0.0.1是所有主机组,子网上所有具有多播能力的节点(主机、路由器、打印机等)必须在所有具有多播能力的接口上接入该组。

2.224.0.0.2是所有路由器组,子网上所有多播路由器必须在具有多播能力的接口上加入该组。

介于224.0.0.0到224.0.0.255之间的地址(也可写作224.0.0.0/24)称为链路局部的多播地址,这些地址是为低级拓扑发现(指在局域网(LAN)或特定网络段内,识别网络设备之间的物理(链路层)连接关系的过程)和维护协议(用于网络管理和维护的一类协议或机制)保留的。多播路由器不转发以这些地址为目的地址的数据报。

IPv6多播地址的高序2字节的值为ff。上图也给出了把16字节IPv6多播地址映射成6字节以太网地址的方法。112位组ID的低序32位复制到以太网地址的低序32位。以太网地址的高序2字节为33:33。IPv6多播地址到以太网地址的映射见RFC 2464,到FDDI网络地址的映射见RFC 2467,到令牌环网的映射见RFC 2470。

IPv6广播地址映射的以太网地址高位的16位被分配为33:33,这样接收接口会特殊识别和处理该类组地址。

IPv6多播地址有两种格式:
在这里插入图片描述
上图中有印刷错误,第2个字节的高4位上图中显示是范围,应该是标志。

如上图,当P标志为0时,T标志区分众所周知多播组(T为0)还是临时多播组(T为1);P标志为1时,表示多播地址是基于某个单播前缀的(见RFC 3306),此时T的值也必须为1(也就是说基于单播的多播地址总是临时的),此时plen和prefix两个字段分别设置为单播前缀长度和单播前缀的值。4位标志字段的高2位是被保留的。

以下是特殊的IPv6多播地址:
1.ff01::1ff02::1是所有节点组。子网上所有具备多播能力的节点(主机、路由器、打印机等)必须在所有具有多播能力的接口上加入该组,类似于IPv4的多播地址224.0.0.1。多播是IPv6的组成部分,但在IPv4中是可选的。

尽管对应的IPv4组称为所有主机组,IPv6组称为所有节点组,它们的含义是一致的。IPv6重新命名意在更清晰地指出本组包括子网上的任何IP设备。

2.ff01::2ff02::2是所有路由器组。子网上所有多播路由器必须在所有具备多播能力的接口上加入该组,类似于IPv4的多播地址224.0.0.2

IPv6多播地址存在一个4位的范围字段,用于指定多播数据报的传输范围。IPv6分组还有一个跳限字段,用于限制分组被路由器转发的次数。以下是一些已经分配给范围字段的值:
1:接口局部的(interface-local)。

2:链路局部的(link-local)。

4:管区局部的(admin-local)。

5:网点局部的(site-local)。

8:组织机构局部的(organization-local)。

14:全球或全局的(global)。

其余值或者未分配,或者保留。接口局部多播数据报不允许由接口输出。链路局部数据报不可由路由器转发。管区、网点、组织机构的具体定义由该网点或组织机构的多播路由器管理员决定。仅仅是范围字段不同的IPv6多播地址代表不同的多播组。

IPv4多播数据报没有单独的范围字段,因历史沿用关系,IPv4首部的TTL字段兼用作多播范围字段:0表示接口局部,1表示链路局部,2~32表示网点局部,33~64表示地区局部(region-lcoal),65~128表示大洲局部(continent-local),129~255表示无范围限制。

TTL字段的这种双重用途已经导致一些困难,RFC 2365对此有详细描述。

尽管把IPv4的TTL字段用作多播范围控制已被接受且是受推荐的做法,但如果可能的话,可管理的范围划分更为可取,即把239.0.0.0~239.255.255.255之间的地址定义为可管理的划分范围的IP多播空间(administratively scoped IPv4 multicast space,RFC 2365),它占据多播地址空间的高端,该范围内的地址由组织机构内部分配,不保证跨组织机构边界的唯一性。组织机构需要把它的边界多播路由器配置成禁止转发以这些地址为目的地址的多播数据报。

可管理的划分范围的IPv4多播地址空间被进一步划分为本地范围(local scope)和组织机构局部范围(organization-local scope):
在这里插入图片描述
特别是在流式多媒体应用中,一个多播地址(IPv4或IPv6)和一个传输层端口(通常是UDP端口)的组合称为一个会话。例如,一个视频电话会议可能由两个会话构成,一个用于音频,一个用于视频。这些会话几乎总是使用不同端口,有时还使用不同多播组,以便接收时灵活地选取,例如有的用户可能选择只接收音频会话,而有的客户可能选择同时接收音频和视频会话,如果不同会话选择相同的组地址,这种选择就不大可能做到。

局域网上的IPv4多播情形如下,IPv6涉及的步骤与之类似:
在这里插入图片描述
上图中,右侧主机上接收应用进程启动,并创建一个UDP套接字,捆绑端口123到该套接字上,然后加入多播组224.0.1.1,这种加入操作是通过调用setsockopt完成的。上述操作完成后,IPv4层内部保存这些信息,并告知合适的数据链路接收目的以太网地址为01:00:5e:00:01:01的以太网帧,该地址是接收应用进程刚加入的多播地址对应的以太网地址。

接下来左侧主机上的发送应用进程创建一个UDP套接字,往IP地址224.0.1.1的123端口发送一个数据报。发送多播数据报无需任何特殊处理,发送应用进程不必为此加入多播组。发送主机把该IP地址转换成相应的以太网目的地址,再发送承载该数据报的以太网帧,该帧中同时含有目的以太网地址和目的IP地址。

我们假设中间主机不具备IPv4多播能力(因为IPv4多播支持是可选的),它将完全忽略该帧,因为:
1.该帧的目的以太网地址不匹配该主机的接口地址。

2.该帧的目的以太网地址不是以太网广播地址。

3.该主机的接口未被告知接收组地址(组地址是以太网地址的第一个字节的最低位为1的地址)。

该帧基于我们所称的不完备过滤被右侧主机的数据链路接收,其中的过滤操作由相应接口使用该帧的以太网目的地址执行。我们称其为不完备过滤是因为尽管我们告知该接口接收以某个以太网组地址为目的地址的帧,通常它也会接收以其他以太网组地址为目的地址的帧。

当我们告知一个以太网接口接收目的地址为某个特定以太网组地址的帧时,许多当前的以太网接口卡对这个地址应用某个散列函数,计算出一个介于0~511之间的值,然后把该值在一个512个数位的数组中对应的位置1。当有一个目的地为某个组地址的帧在线缆上经过时,接口对其目的地址应用同样的散列函数,也计算出一个0~511之间的值,如果该值在数组中对应的位为1,就接收这个帧,否则就忽略这个帧。较老的网络接口卡所用数位数组仅有64位,把它增加到512位可以减少接口接收非关注帧的可能性。随着时间推移,越来越多的应用使用多播,数位数组的大小可能进一步增加,当今有些接口已经实现完备过滤。有些接口卡没有多播过滤,当告知它们接收某个特定组地址时,它们必须接收所有广播帧(有时称为多播混杂模式,multicast promiscuous)。有一款流行的接口卡既具备容量为16个组地址的完备过滤能力,又有一个512位的散列结果数位数组作为补充(当要接收的组地址超过16个时)。另有一款接口卡能为80个组地址执行完备过滤,但超出容量后只能进入多播混杂模式。即使接口执行完备过滤,IP层的完备软件过滤也是必需的,因为从IP多播地址到硬件地址的映射不是一对一的(如32个IPv4多播地址对应1个以太网地址)。

右侧主机的数据链路收取该帧后,把由该帧承载的分组传递到IP层,因为该以太网帧的类型为IPv4。既然收到的分组以某个多播IP地址作为目的地址,IP层于是比较该地址和本机的接收应用进程已经加入的所有多播地址,如果都不匹配则丢弃该分组,我们称这个操作为完备过滤,因为它基于IPv4报头中完整的32位D类地址执行。上例中,右侧主机IP层接受该分组并把承载在其中的UDP数据报传递到UDP层,UDP层再把承载在UDP数据报中的应用数据报传递到绑定了端口123的套接字。

上图没有展示的情形:
1.运行加入了多播地址为225.0.1.1的某个应用进程的一个主机。既然IPv4多播地址组ID的高5位在到以太网地址的映射中被忽略,该主机的接口也将接收目的以太网地址为01:00:5e:00:01:01的帧,此时由该帧承载的分组将由IP层中的完备过滤丢弃。

2.运行所加入多播地址符合以下条件的某个应用的一个主机:由这个多播地址映射成的以太网地址恰好和01:00:5e:00:01:01一样被该主机执行非完备过滤的接口散列到同一个结果,该接口也将接收该帧。该帧会由数据链路层(执行完备过滤的接口)或IP层(执行非完备过滤的接口)丢弃。

3.目的地为相同多播组(224.0.1.1)但端口不同的数据报,上图中右侧主机仍将接收该数据报,并由IP层接收并传递给UDP层,但UDP层会丢弃它(假设绑定该端口的套接字不存在)。

让一个进程接收某个多播数据报的先决条件是该进程加入相应多播组并绑定相应端口。

以上是单个局域网上的多播,广域网也可进行多播,如下图,5个局域网通过5个多播路由器互连:
在这里插入图片描述
假如在其中5个主机上启动了某个程序,而这5个进程加入了一个给定多播组,另外假设每个多播路由器与其邻居多播路由器采用某个多播路由协议(MRP,Multicast Routing Protocol):
在这里插入图片描述
当某个主机上的一个进程加入一个多播组时,该主机向所有直接连接的多播路由器发送一个IGMP消息,告知它们本主机已加入了那个广播组。多播路由器通过MRP交换这些信息,这样每个多播路由器就知道收到目的地为所加入多播地址的分组时该如何处理。

多播路由仍是一个活跃的研究课题,单纯讨论它就可能耗费一本书的容量。

接着假设左上方主机上的一个进程开始发送目的地为那个给定多播地址的分组,假如该进程发送的是那些多播进程正等着接收的音频分组:
在这里插入图片描述
我们可以跟踪这些多播分组从发送进程传输到所有接收进程所经历的步骤:
1.这些分组在左上方局域网上由发送进程多播发送。接收主机H1接收这些分组(因为它加入了给定多播组),多播路由器MR1也接收这些分组(因为多播路由器必须接收所有多播分组)。

2.MR1把这些多播分组转发到MR2,因为MRP已经通告了MR1需要把接收目的地为给定多播组的分组传给MR2。

3.MR2在直接连接的局域网上多播发送这些分组,因为该局域网上的H2和H3属于该多播组。此外,MR2还向MR3发送这些分组的一个副本。

4.像步骤3中的那样(MR2对分组进行复制)是多播转发特有的,单播分组在被路由器转发时从不被复制。

5.MR3把这些多播分组发送到MR4,但不在直接连接的局域网上多播这些分组,因为该局域网上没有主机加入该多播组。

6.MR4在直接连接的局域网上多播这些分组,因为该局域网上主机H4和H5属于该多播组。它不向MR5发送这些分组的一个副本,因为直接连接MR5的局域网上没有主机属于该多播组,MR4已经根据与MR5交换的多播路由信息知道了这一点。

广域网上有两个不太好的方法可以替代多播:
1.广播泛滥(broadcast flooding):分组由发送进程广播发送,每个路由器在除分组到达接口外的所有其他接口广播发送这些分组,这个方法会增加对这些分组不感兴趣的主机和路由器的数目。路由器可配置成转发广播的,如果我们的多播目标都在一个网络,就可对这个网络进行广播来替代多播。

2.给每个接收者发送单个副本:发送进程必须知道所有接收进程的IP地址,且需要给每个接收进程发送一个副本。

广域网上的多播难以部署,原因如下:
1.最大的问题是运行MRP要求每个多播路由器接收来自所有本地接收主机的多播组的加入或其他请求,并在所有多播路由器之间交换这些信息。多播路由器的转发功能要求把来自网络中任何发送主机的多播数据报复制并发送到网络中的任何接收主机上(只要网络中有主机加入了对应多播组)。

2.另一个大问题是多播地址的分配,IPv4没有足够的多播地址可以静态地分配给想用的多播应用系统,要在广域范围发送多播分组而又不与其他多播发送进程冲突,多播应用系统就得使用唯一的地址,但全球性的多播地址分配机制尚未出现。

源特定多播(SSM,Source-Specific multicast)给出了在有限程度上解决以上问题的方法,SSM把应用系统的源地址结合到组地址上:
1.接收进程向多播路由器提供发送进程的源地址作为多播组加入操作的一部分,从而消除了网络中的约会问题,因为每个接收进程都必须知道源地址。“rendezvous problem”(约会问题)问题涉及到如何确定多播组中的成员如何找到彼此,以便进行通信,而不需要事先知道对方的详细信息,在进行多播通信时,发送者和接收者需要在某个地方"约会",以便建立连接并进行通信。这个约会点通常被称为"rendezvous point"(约会点)。传统的多播通信中,约会点允许发送者和接收者在不必互相知道彼此的情况下找到彼此,以便进行数据传输。这种约会点通常是网络中的特定设备或节点。发送者需要知道约会点的位置,并向其发送数据。接收者也需要知道约会点的位置,并从约会点接收数据。

2.把多播组的标识从单纯多播组地址细化为单播源地址和多播目的地址的组合(SSM称之为通道(channel)),这意味着发送进程可以挑选任何多播地址,因为源地址和目的地址的组合是唯一的,只要在源端范围内多播地址唯一即可。SSM会话由源地址、目的地址、端口三者的组合标识。

SSM还提供一定的反窃听能力,因为源端2在源端1上的通道发送较为困难,因为源端1上的通道包含了源端1的地址。当然窃听还是有可能的,不过要困难得多。

传统意义的多播API只需要5个套接字选项,SSM所需的源过滤额外要求多播API支持新增4个套接字选项。以下是与组成员无关(指与加入组、离开组等无关)的3个套接字选项:
在这里插入图片描述
以下是与组成员相关的6个套接字:
在这里插入图片描述
IPv4的TTL和回馈(loopback)选项对应u_char类型参数,而IPv6的跳限和回馈选项分别对应int和u_int类型参数。所有套接字选项中,大多都取int作为参数,因此使用IPv4多播选项的一个常见错误是使用int参数指定TTL或回馈选项的值来调用setsockopt,这是不允许的。IPv6所做的改动使它们与其他选项更为一致。

以下是多播相关套接字选项的详细信息:
1.IP_ADD_MEMBERSHIPIPV6_JOIN_GROUPMCAST_JOIN_GROUP:在一个指定的本地接口上加入一个不限源的多播组。对于IPv4版本,本地接口使用某个单播地址指定;对于IPv6和与协议无关的API,本地接口使用某个接口索引指定。以下是这3个选项对应的参数结构:
在这里插入图片描述
如果本地接口指定为IPv4通配地址INADDR_ANY或IPv6的值为0的接口索引,就由内核选择一个本地接口。

一个主机在某个给定接口上属于一个给定多播组的前提是该主机上当前有一个或多个进程在那个接口上属于该组。

一个给定套接字上可以多次加入多播组,但每次加入的必须是不同多播地址,或是在不同接口上的同一个多播地址。多次加入多播组可用于多宿主机,如创建一个套接字后对一个给定多播地址在每个接口上执行一次加入。

NTP的工作方式有两种,NTP客户端可通过向其所选时间源发送时间同步请求来获取响应,NTP服务器也可进行多播或广播来同步时间。

IPv6多播地址中显式存在一个范围字段,仅仅范围有差异的IPv6多播地址也代表不同的多播组。因此如果某个NTP实现想要不论范围接收所有NTP分组,它就必须加入ff01::101(接口局部)、ff02::101(链路局部)、ff05::101(网点局部)、ff08::101(组织机构局部)、ff0e::101(全球)这些多播组,所有这些加入都可以在单个套接字上执行,且可以通过设置IPV6_PKTINFO套接字选项让recvmsg函数返回每个数据报的目的地址。

IP协议无关的套接字选项(MCAST_JOIN_GROUP)与IPv6版本几乎相同,差别只是改用一个sockaddr_storage结构代替in6_addr结构传递多播组地址。sockaddr_storage结构足以存放系统支持的任何类型的地址。

大多数实现对于每个套接字上允许执行加入的次数有一个限制,IPv4的该限制通常由常值IP_MAX_MEMBERSHIPS指定,源自Berkeley的实现该值通常是20。

加入多播组时,如果不指定在套接字上执行加入操作的接口,源自Berkeley的内核在普通的IP路由表中查找给定多播地址并使用找出的接口。为处理此情形,有些系统在初始化阶段为所有多播地址安装一个路径(对于IPv4就是目的地址为224.0.0.0/4的路径)。

IPv6和协议无关版本改用接口索引指定接口,以取代IPv4版本使用本地单播地址指定接口的做法,意图在于,允许在未指定网络地址的接口或隧道端点(通常是点对点链路端点)上执行加入。

原始的IPv6多播API使用IPV6_ADD_MEMBERSHIP而非IPV6_JOIN_GROUP。后面的mcast_join函数隐藏了这两个版本的差异。

2.IP_DROP_MEMBERSHIPIPV6_LEAVE_GROUPMCAST_LEAVE_GROUP:离开指定的本地接口上不限源的多播组,1中给出的参数结构同样适用于本套接字选项的各种版本,如果未指定本地接口(对于IPv4指定INADDR_ANY,对于IPv6指定值为0的接口索引),就抹除首个匹配的多播组成员关系。

如果一个进程加入某个多播组后不显式离开该组,当套接字关闭时,该成员关系也自动抹除。单个主机上可能有多个套接字各自加入相同多播组,此时单个套接字成员关系的抹除不影响该主机继续作为该多播组的成员,直到最后一个套接字也离开该多播组。

原始的IPv6多播API使用IPV6_DROP_MEMBERSHIP而不是IPV6_LEAVE_GROUP。后面的mcast_leave函数隐藏了这两个版本的差异。

3.IP_BLOCK_SOURCEMCAST_BLOCK_SOURCE:对于一个本地接口上已存在的一个不限源的多播组,在本套接字上阻塞接收来自某个源的分组。如果加入同一个多播组的所有套接字都阻塞了相同的源,那么主机系统可以通知多播路由器这种分组流通不再需要,并可能由此影响网络中的多播路由。该套接字选项可用于忽略来自无赖发送进程的分组流通。对于IPv4版本,本地接口由某个单播地址指定;对于与IP协议无关的API,本地接口由某个接口索引指定。这两个套接字选项对应的参数结构:
在这里插入图片描述
如果本地接口指定为IPv4的INADDR_ANY或与协议无关的API的值为0的接口索引,就由内核选择与首个匹配的多播组成员关系对应的本地接口。

源阻塞请求修改已存在的组成员关系,因此必须已经使用IP_ADD_MEMBERSHIP、IPV6_JOIN_GROUP、MCAST_JOIN_GROUP在对应接口上加入对应的多播组。

4.IP_UNBLOCKMCAST_UNBLOCK_SOURCE:开通一个先前被阻塞的源,3中给出的参数结构同样适用于本选项的各种版本。

如果未指定本地地址(IPv4的INADDR_ANY或与协议无关的API的值为0的接口索引),那么开通首个匹配的被阻塞源。

5.IP_ADD_SOURCE_MEMBERSHIPMCAST_JOIN_SOURCE_GROUP:在一个指定的本地接口上加入一个特定于源的多播组。3中给出的参数结构同样适用于本选项的各种版本。加入该特定于源的组之前,不能加入不限源的该多播组(使用1中的套接字选项)。

如果本地接口指定为IPv4的INADDR_ANY或与协议无关的API的值为0的接口索引,就由内核选择一个本地接口。

6.IP_DROP_SOURCE_MEMBERSHIPMCAST_LEAVE_SOURCE_GROUP:在一个指定的本地接口上离开一个特定于源的多播组。3中给出的参数接口结构同样适用于本选项的各种版本。如果本地接口指定为IPv4的INADDR_ANY或与协议无关的API的值为0的接口索引,就抹除首个匹配的特定于源的多播组成员关系。

如果一个进程加入某个特定于源的多播组后不显式离开该组,当相应套接字关闭时,该成员关系自动抹除。单个主机上可能有多个套接字各自加入相同的源特定多播组,此时单个套接字上成员抹除不影响该主机继续作为该多播组的成员,直到最后一个套接字也离开该多播组。

7.IP_MULTICAST_IFIPV6_MULTICAST_IF:指定通过本套接字发送的多播数据报的外出接口。对于IPv4,该接口由某个in_addr结构指定;对于IPv6,该接口由某个接口索引指定。如果其值对于IPv4为INADDR_ANY,对于IPv6为值为0的接口索引,那么先前通过本套接字选项指派的外出接口将被抹除,改为每次由系统选择外出接口。

源自Berkeley的内核通过在普通的IP路由表中查找通往目的多播地址的路径来选择多播数据报的默认外出接口和接收接口,前提是进程加入多播组时未指定接口。这里假定存在通往某个多播地址的一个路径(可能是路由表中的默认路径),那么该路径对应接口既用于输出,也用于输入。

8.IP_MULTICAST_TTLIPV6_MULTICAST_HOPS:给外出的多播数据报设置IPv4的TTL或IPv6的跳限。如不指定,默认值为1,从而把多播数据报限制在本地子网。

9.IP_MULTICAST_LOOPIPV6_MULTICAST_LOOP:开启或禁止多播数据报的本地环回(回馈),默认情况下回馈开启:如果一个主机在某个外出接口上属于某个多播组,那么该主机上由某个进程发送的目的地为该多播组的每个数据报都有一个副本回馈,被该主机作为一个收取的数据报处理,即,如果所发送的多播数据报本机上有进程想收取(在本机的某个接口上加入了该多播组),那么也会送往本机上的那个想接收的进程。

这类似于广播,但对广播而言,这种回馈无法禁止。这一点意味着如果一个进程同时属于它自己所发送的数据报的目的多播组,默认它就会收到自己发送的数据报。

此处讨论的回馈是在IP层或更高层进行的内部回馈,如果接口监听到了自己发送的多播比特位流,RFC 1112要求驱动程序丢弃这些副本。该RFC同时声明本回馈套接字选项默认是开启的原因为:对主机上那些只能有一个进程属于一个给定多播组的上层协议(如路由协议)进行性能优化(由于只有一个进程能加入该多播组,且通常该进程会向该多播组发消息,有了IP层或更高层的内部环回,发完后不用通过链路层就可收到,省去了下层协议的开销)。

以上9个套接字选项中,前6个影响多播数据报的接收,后3个影响多播数据报的发送(外出接口、TTL、回馈)。多播数据报的发送无需任何特殊处理,如果发送多播数据报前没有指定影响发送的多播套接字选项,那么数据报的外出接口将由内核选择,TTL或跳限为1,并有一个副本自环回来。

为了接收目的地址为某个组地址且目的端口为某个端口的多播数据报,进程必须加入该多播组,并捆绑该端口到某个套接字。这是两个不同的操作,但都是必需的。多播组加入操作告知所在主机的IP层和数据链路层接收发往该组的多播数据报。端口捆绑操作是应用进程告知UDP它想接收发往该端口的数据报。有些应用进程除端口外还把多播地址也捆绑到某个套接字,从而防止所在主机IP层把为该端口收取的目的地址为其他单播、广播、多播地址的数据报递送到该套接字。

为了接收目的地址为某个多播组,目的端口为某个端口的数据报,历史上源自Berkeley的实现曾只要求某个套接字加入该多播组,而这个套接字不必进行捆绑端口操作。但这些实现存在把多播数据报递送到不参与多播的应用进程的潜在可能性。新的多播内核要求进程为接收多播数据报的套接字捆绑到相应端口并任意设置一个多播套接字选项,设置一个多播套接字选项的作用是指示该应用进程参与多播,最常设置的多播套接字选项是多播组的加入。Solaris的做法不同,它把收到的多播数据报递送到既加入了多播组又绑定了相应端口的套接字。为便于移植,所有多播应用都应加入组并捆绑端口。

较新的多播API,IP层只把多播数据报递送给已经加入相应多播组和单播源的套接字。这个做法是随着IGMPv3(RFC 3376)而引入的,意在允许源过滤和源特定多播。它强调加入组这个需求,而放松捆绑(bind)组地址的需求(该需求本来就是非必要的),但为了便于移植,多播应用应加入组并捆绑端口和组地址。

有些较老的具备多播能力的主机不允许把多播地址捆绑到套接字,为了便于移植,应用可忽略bind多播地址时返回的错误,并用INADDR_ANY或in6addr_any再次尝试bind函数。

多播套接字选项的IPv4和IPv6版本很相似,但还是有差别,会造成使用多播的协议无关代码因插入大量#ifdef而变得凌乱不堪,一个较好的解决方式是编写以下函数隐藏这些区别:
在这里插入图片描述
在这里插入图片描述
mcast_join函数加入一个不限源的多播组,该组的IP地址存放在由grp参数指向的长度为grplen的套接字地址结构中。通过使用接口名字(通过非空的ifname参数)或使用非0的接口索引(通过ifindex参数),我们可以指定加入该组的接口,如果两者都没有指定,则由内核选择这个接口。对于IPv6,指定接口时需要接口索引,如果给的是接口名,可通过if_nametoindex函数获取对应索引。对于IPv4,指定接口时需要接口单播IP地址,如果给定的是接口名,可以SIOCGIFADDR为参数调用ioctl来获取对应单播地址;如果给定的是接口索引,就先调用if_indextoname函数获取接口名,再以SIOCGIFADDR为参数调用ioctl来获取对应单播地址。

用户通常会指定接口的名字(如le0、ether0),而不是接口的IP地址或索引。tcpdump程序允许用户指定接口名,它的-i选项以一个接口名作为参数。

mcast_leave函数离开一个不限源的多播组,该组的IP地址存放在由grp参数指定的长度为grplen参数的套接字地址结构中。mcast_leave函数不能指定接口,它总是抹除首个匹配的多播组成员关系,这样做简化了库函数接口,需要针对接口控制组成员关系的程序需要直接使用setsockopt函数。

mcast_block_source函数阻塞从给定单播源到给定多播组数据报,单播源和多播组分别由src和grp参数指向的长度分别为参数srclen和grplen的两个套接字地址结构给出。套接字参数fd上必须已为给定多播组调用过mcast_join。

mcast_unblock_source函数解阻塞从给定单播源到给定多播组的数据报接收,所指定的参数必须与早先某个mcast_block_source调用一致。

mcast_join_source_group函数加入一个特定于源的多播,该源和该组分别由参数src和grp指向的长度分别为参数srclen和grplen的两个套接字地址结构给出。在给定接口上加入该特定于源的多播组,该接口由一个接口名(非空的ifname参数)或非0的接口索引(ifindex参数)指定,若两者都未指定则由内核选择接口。

mcast_leave_source_group函数离开一个特定于源的多播组,该源和该组分别由参数src和grp指向的长度分别为参数srclen或grplen的两个套接字地址结构给出。与mcast_leave函数一样,它总是抹除首个匹配的多播组成员关系。

mcast_set_if函数设置外出多播数据报的默认接口索引,如果ifname参数非空,则由它指定接口名,否则如果ifindex参数大于0,那么由它指定接口索引。对于IPv6,接口从名字到索引的映射由if_nametoindex函数完成。对于IPv4,指定接口时需要接口单播IP地址,如果给定的是接口名,可以SIOCGIFADDR为参数调用ioctl来获取对应单播地址;如果给定的是接口索引,就先调用if_indextoname函数获取接口名,再以SIOCGIFADDR为参数调用ioctl来获取对应单播地址。

mcast_set_loop函数把回馈套接字选项设为1或0。mcast_set_ttl函数设置IPv4的TTL或IPv6的跳限。3个mcast_get_XXX函数返回相应值。

以下是mcast_join函数:

#include "unp.h"
#include <net/if.h>

int mcast_join(int sockfd, const SA *grp, socklen_t grplen, const char *ifname, u_int ifindex) {
// 协议无关套接字API加入多播组过程
#ifdef MCAST_JOIN_GROUP
    struct group_req req;
    // 如果给定了索引,就直接使用它
    if (ifindex > 0) {
        req.gr_interface = ifindex;
    // 否则如果调用者给了接口名,就将其转换成索引
    } else if (ifname != NULL) {
        if ((req.gr_interface = if_nametoindex(ifname)) == 0) {
            // ENXIO错误含义为“没有该设备或地址”
		    errno = ENXIO;    /* i/f name not found */
		    return -1;
		}
    // 否则置0接口索引,让内核选择
    } else {
        req.gr_interface = 0;
    }
    // req.gr_group是一个sockaddr_storage结构,理论上能存放系统支持的任何地址结构
    // 但为了防止因代码编写不慎引起的缓冲区溢出,我们仍检查调用者给定的套接字地址结构大小
    if (grplen > sizeof(req.gr_group)) {
        errno = EINVAL;
		return -1;
    }
    memcpy(&req.gr_group, grp, grplen);
    // 执行加入组操作,setsockopt函数的level参数由我们自己编写的family_to_level函数
    // 根据组地址的地址族确定。有些系统支持level参数与套接字地址族不匹配,如为AF_INET6
    // 的套接字使用的level参数为IPPROTO_IP,但并非全部实现都支持这样
    return setsockopt(sockfd, family_to_level(grp->sa_family),
                      MCAST_JOIN_GROUP, &req, sizeof(req));
// IPv4套接字加入多播组过程
#else
    switch (grp->sa_family) {
    case AF_INET:
        struct ip_mreq mreq;
        struct ifreq ifreq;

        // 复制IPv4多播地址结构(in_addr结构)到ip_mreq.imr_multiaddr
        memcpy(&mreq.imr_multiaddr,
               &((const struct sockaddr_in *)grp)->sin_addr, sizeof(struct in_addr));
        
        // 获取IPv4本地单播地址,用它指定特定接口
        // 如果调用者给出接口索引,用接口索引获取接口名存入ifreq结构,然后直接跳到ioctl函数
        if (ifindex > 0) {
		    if (if_indextoname(ifindex, ifreq.ifr_name) == NULL) {
		        errno = ENXIO;    /* i/f index not found */
				return -1;
	    	}
	    	goto doioctl;
	    // 如果调用者给出了接口名,直接将其复制到ifreq结构
		} else if (ifname != NULL) {
		    strncpy(ifreq.ifr_name, ifname, IFNAMSIZ);
		doioctl:
		    // 通过接口名获取接口IP地址
		    if (ioctl(sockfd, SIOCGIFADDR, &ifreq) < 0) {
		        return -1;
		    }
		    memcpy(&mreq.imr_interface, 
		           &((struct sockaddr_in *)&ifreq.ifr_addr)->sin_addr, sizeof(struct in_addr));
	    // 否则把接口设置为通配地址,让内核选择接口
		} else {
		    mreq.imr_interface.s_addr = htonl(INADDR_ANY);
		}
	
		return setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
	}
// IPv6套接字加入多播组过程
#ifdef IPV6
    case AF_INET6:
        struct ipv6_mreq mreq6;
  
        // 把IPv6多播地址结构复制到ipv6_mreq.ipv6mr_multiaddr
		memcpy(&mreq6.ipv6mr_multiaddr,
		       &((const struct sockaddr_in6 *)grp)->sin6_addr, sizeof(struct in6_addr));
	    
	    // 如果调用者给定索引,则使用该索引
		if (ifindex > 0) {
		    mreq6.ipv6mr_interface = ifindex;
		// 否则如果调用者给定了接口名,调用if_nametoindex取得对应索引
		} else if (ifname != NULL) {
		    if ((mreq6.ipv6mr_interface = if_nametoindex(ifname)) == 0) {
		        errno = ENXIO;    /* i/f not found */
				return -1;
		    }
		// 否则把接口索引置0,由内核选择接口
		} else {
		    mreq6.ipv6mr_interface = 0;
		}
	
		return setsockopt(sockfd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6));
#endif 
    default:
        errno = EAFNOSUPPORT;
		return -1;
    }
#endif
}

mcast_set_loop函数:

#include "unp.h"

int mcast_set_loop(int sockfd, int onoff) {
    switch (sockfd_to_family(sockfd)) {
    case AF_INET:
        u_char flag;
	
		flag = onoff;
		return setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &flag, sizeof(flag));
#ifdef IPV6
    case AF_INET6:
        u_int flag;

		flag = onoff;
		return setsockopt(sockfd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &flag, sizeof(flag));
#endif
    default:
        errno = EAFNOSUPPORT;
		return -1;
    }
}

对于使用多播的dg_cli函数(由UDP回射客户程序使用),如果外出接口、TTL、回馈选项的默认设置可以接受,则发送多播数据报无需设置任何多播套接字选项,多播版dg_cli函数可以通过单纯地去掉广播版dg_cli函数的用于打开广播的setsockopt调用来实现,我们指定所有主机组为服务器地址运行使用多播版dg_cli函数的回射客户程序:
在这里插入图片描述
所在子网中有两个主机响应,它们具备多播能力,从而都加入了所有主机组,且都运行了多播的回射服务器。每个应答数据报都是单播的,因为请求数据报的单播源地址可被每个服务器用作应答数据报的目的地址。

大多系统不允许对广播数据报执行分片,但可对多播数据报进行分片,我们可以发送一个大小为2000字节的文件(文件中只有一行长为2000字节的文本行)来验证:
在这里插入图片描述
IP多播基础设施是具有跨域组播功能的互联网部分。组播并未在整个互联网上启用。IP多播基础设施的前身是作为一个层叠网络从1992年开始的MBone(层叠网络指在互联网基础设施之上建立的一种多播网络,MBone使用了一套自己的协议和软件,通过覆盖在现有的互联网之上,为用户提供了跨越不同域的多播功能),到1998年变成了作为因特网基础设施的一部分而部署的多播基础设施。多播技术在企业范围内部部署较广,但作为域间IP多播基础设施的多播技术不太常见。

为了在IP多播基础设施上接收一个多媒体会议,站点只需要知道该会议的多播地址以及会议数据流(音频、视频等)所用的UDP端口。会话声明协议(SAP,Session Announcement Protocol,见RFC 2974,是用于在IP网络上传输会话信息的协议)描述了如何让站点知道某会议的多播地址和所用UDP端口;会话描述协议(Session Description Protocol,SDP,见RFC 2327)是用于描述多媒体会话的文本格式的协议,通过使用SDP,参与者可以建立、协商和管理多媒体会话,并确保各方在会话中使用一致的参数和属性。希望在IP组播基础设施上宣布会话的站点定期发送一个包含会话描述的组播数据报到一个众所周知的组播组和UDP端口。在IP组播基础设施上的站点运行一个名为sdr(Session Description Repository, 会话描述存储)的程序来接收这些通告,这个程序功能强大:它不仅接收会话通告,还提供一个交互式用户界面来显示信息或让用户发送会话通告。

以下是接收定期多播的SAP/SDP声明的程序:

#include "unp.h"

// SAP通告通常使用的组播地址是224.2.127.254,其名称为sap.mcast.net
// 所有众所周知的组播地址(参见http://www.iana.org/assignments/multicast-addresses)
// 都在DNS中以mcast.net层次结构出现
#define SAP_NAME "sap.mcast.net"    /* default group name and port */
// SAP通告组播对应的众所周知的UDP端口号是9875
#define SAP_PORT "9875"

void loop(int, socklen_t);

int main(int argc, char **argv) {
    int sockfd;
    const int on = 1;
    socklen_t salen;
    struct sockaddr *sa;

    // 调用自己编写的udp_client函数查找名字和端口
    // 该函数会把结果填到合适的套接字地址结构中
    // 如果命令行参数未指定,就用默认名字和端口
    if (argc == 1) {
        sockfd = Udp_client(SAP_NAME, SAP_PORT, (void **)&sa, &salen);
    // 否则从命令行参数中取得多播地址、端口号
    } else if (argc == 4) {
        sockfd = Udp_client(argv[1], argv[2], (void **)&sa, &salen);
    } else {
        err_quit("usage: mysdr <mcast-addr> <port#> <interface-name>");
    }

    // 设置SO_REUSEADDR套接字选项以允许在单个主机上运行本程序的多个实例
    Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    // 通过将特定多播地址绑定到该套接字,防止该套接字接收目的端口为当前监听的多播端口的其他UDP数据报
    // 多播地址的捆绑并非必须,但这样是由内核过滤非所关注分组,而不用我们再过滤
    Bind(sockfd, sa, salen);

    // 加入多播组
    Mcast_join(sockfd, sa, salen, (argc == 4) ? argv[3] : NULL, 0);

    loop(sockfd, salen);    /* recvive and print */

    exit(0);
}

void loop(int sockfd, socklen_t salen) {
    socklen_t len;
    ssize_t n;
    char *p;
    struct sockaddr *sa;
    // sap_packet结构描述SAP分组:一个32位SAP首部,后跟一个32位源地址
    // 再跟真正的声明,声明是若干行OSI 8859-1文本,不得超过1024字节
    // 每个UDP数据报只能承载一个会话声明
    struct sap_packet {
        uint32_t sap_header;
		uint32_t sap_src;
		uint32_t sap_data[BUFFSIZE];
    } buf;

    sa = Malloc(salen);

    for (; ; ) {
        len = salen;
		n = Recvfrom(sockfd, &buf, sizeof(buf) - 1, 0, sa, &len);
		((char *)&buf)[n] = 0;
		// 修正首部字段的字节序
		buf.sap_header = ntohl(buf.sap_header);
	
	    // 显示发送者的IP地址、端口号、SAP散列值
		printf("From %s hash 0x%04x\n", Sock_ntop(sa, len), buf.sap_header & SAP_HASH_MASK);
		// 是否是我们要处理的SAP版本
		if (((buf.sap_header & SAP_VERSION_MASK) >> SAP_VERSION_SHIFT) > 1) {
		    err_msg("... version field not 1 (0x%08x)", buf.sap_header);
		    continue;
		}
		// 不处理使用IPv6地址的SAP
		if (buf.sap_header & SAP_IPV6) {
		    err_msg("... IPv6");
		    continue;
		}
		// 不处理压缩或加密的分组
		if (buf.sap_header & (SAP_DELETE | SAP_ENCRYPTED | SAP_COMPRESSED)) {
		    err_msg("... can't parse this packet type (0x%08x)", buf.sap_header);
		    continue;
		}
		p = buf.sap_data + ((buf.sap_header & SAP_AUTHLEN_MASK) >> SAP_AUTHLEN_SHIFT);
	    if (strcmp(p, "application/sdp") == 0) {
			p += 16;
		}
		printf("%s\n", p);
    }
}

以下是以上程序的一些典型输出:
在这里插入图片描述
以上声明描述的是NASA在IP多播基础设施上关于某次航天飞机任务的报道。SDP会话描述由多个type=value格式的文本行构成,其中type总是单个字符并区分大小写,value是一个依赖于type的结构化的文本串(指按照一定规则和格式组织的文本),等号两边不允许有空格。

v=0是版本。

o=是来源,上例中,-表示无确切用户名,60345是会话ID,0是这个声明的版本号,IN是网络类型,IP4是地址类型,128.223.214.198是地址。由用户名、会话ID、网络类型、地址类型、地址构成的五元组是本会话的一个全球唯一标识。

s=定义会话名字。

i=给出关于会话的信息,上例中,我们按每80字节一次作了折行处理。

u=给出一个统一资源标识(URI,Uniform Resource Identifier),该URI上提供关于本会话的更详细信息。

e=提供会议负责人的电子邮件地址。

p=提供会议负责人的电话号码。

b=提供本会话预期带宽的测算值。

t=提供开始时间和结束时间,这两个时间都使用NTP单位,即1900年1月1日UTC时间以来经过的秒数。上例会话是永久的,因此起止时间都是0。

a=是属性行,出现在m=行之前的a=行是会话属性,出现在某个m=行之后则为相应媒体的属性。

m=是媒体声明,上例中有两个,第一个指明视频在端口63096上,格式为实时传输协议(RTP,Real-time Transport Protocol),使用AVP(Audio/Video Profile,指音频/视频配置文件,它是特定的音频或视频编码参数和配置集合),可能的净荷类型为32(MPEG)、31(H.261)、96(WBIH)。

c=行提供本媒体连接信息,上例中指明该连接基于IP,使用IPv4,多播地址为224.2.245.25,TTL为127。虽然224.2.245.25/127像CIDR前缀,但这不是用来表示前缀或掩码的。

第二个m=行指明音频在31954端口,可能的净荷类型有多个,其中有些是标准的,有些由随后的a=rtpmap进一步说明。紧接的c=行提供本媒体的连接信息,上例中指明该连接基于IP,使用IPv4,多播地址为224.2.216.85,TTL为127。

以上IP多播基础设施会话声明程序只接收多播数据报,接下来开发一个既发送又接收多播数据报的简单程序,该程序包含两部分,第一部分每5秒发送一个目的地为指定组的多播数据报,其中含有发送进程的主机名和进程ID,第二部分是一个无限循环,先加入由第一部分发往的多播组,再显示接收到的每个数据报(其中含有发送进程的主机名和进程ID)。这样的程序使得我们可以在一个局域网内的多个主机上启动该程序,以便查看哪个主机在接收来自哪些发送进程的数据报。

我们创建两个套接字,一个用于发送,一个用于接收。我们会给接收套接字捆绑多播组和端口,如239.255.1.2和8888(我们也可以只捆绑通配IP地址和端口8888,但同时捆绑多播地址可以防止目的端口同为8888的其他数据报到达本套接字)。我们还会把接收套接字加入多播组。发送套接字将发送数据报到同一个多播地址和端口,即239.255.1.2和8888。如果我们试图用单个套接字进行发送和接收,则所收到数据报的源协议地址将是bind调用时的239.255.1.2:8888(netstat记法),目的协议地址也同样会是239.255.1.2:8888,而RFC 1122禁止出现源IP地址是广播地址或多播地址的IP数据报,因此我们必须创建两个套接字,一个用于发送,一个用于接收。

以下是该既发送又接收多播数据报的程序代码:

#include "unp.h"

void recv_all(int, socklen_t);
void send_all(int, SA *, socklen_t);

int main(int argc, char **argv) {
    int sendfd, recvfd;
    const int on = 1;
    socklen_t salen;
    struct sockaddr *sasend, *sarecv;

    if (argc != 3) {
        err_quit("usage: sendrecv <IP-multicast-address> <port#>");
    }

    // udp_client函数创建发送套接字且返回可用于sendto函数的一个套接字地址结构及其长度
    // udp_client函数的第3个参数是一个指向指针的指针,因为该函数会分配套接字地址结构的空间
    // 然后修改第三个参数指针的内容为刚分配的空间的地址
    sendfd = Udp_client(argv[1], argv[2], (void **)&sasend, &salen);

    // 创建接收套接字,所用地址族与创建发送套接字所用的一样
    recvfd = Socket(sasend->sa_family, SOCK_DGRAM, 0);

    // 设置SO_REUSEADDR套接字选项以允许此程序的多个实例同时在单一主机上运行
    Setsockopt(recvfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 给接收套接字分配一个套接字地址结构的空间
    sarecv = Malloc(salen);
    // 把发送套接字地址结构复制到接收套接字地址结构
    memcpy(sarecv, sasend, salen);
    // 把多播地址和端口bind在接收套接字上
    Bind(recvfd, sarecv, salen);

    // 在接收套接字上加入多播组,接口由内核选择
    Mcast_join(recvfd, sasend, salen, NULL, 0);
    // 禁止发送套接字的回馈特性
    Mcast_set_loop(sendfd, 0);

    if (Fork() == 0) {
        recv_all(recvfd, salen);    /* child -> recvives */
    }

    send_all(sendfd, sasend, salen);    /* parent -> sends */
}

#define SENDRATE 5    /* send one datagram every five seconds */

void send_all(int sendfd, SA *sadest, socklen_t salen) {
    char line[MAXLINE];    /* hostname and process ID */
    struct utsname myname;
    
    // uname函数返回主机名
    if (uname(&myname) < 0) {
        err_sys("uname error");
    }
    // 构造包含一个主机名和进程ID的输出行
    snprintf(line, sizeof(line), "%s, %d\n", myname.nodename, getpid());

    for (; ; ) {
        Sendto(sendfd, line, strlen(line), 0, sadest, salen);

		sleep(SENDRATE);
    }
}

void recv_all(int recvfd, socklen_t salen) {
    int n;
    char line[MAXLINE + 1];
    socklen_t len;
    struct sockaddr *safrom;

    safrom = Malloc(salen);

    for (; ; ) {
        len = salen;
		n = Recvfrom(recvfd, line, MAXLINE, 0, safrom, &len);
	
		line[n] = 0;    /* null terminate */
		printf("from %s: %s", Sock_ntop(safrom, len), line);
    }
}

分别在freebsd4和macosx系统上运行以上程序:
在这里插入图片描述
网络时间协议NTP是一个用于跨广域网或局域网同步时钟的复杂协议,往往能达到毫秒级精度。RFC 1305叙述了NTP协议。RFC 2030叙述了NTP的一个简化版本SNTP,用于不需要完整的NTP实现的复杂性的主机,SNTP的通常做法是,让局域网内少数几个主机跨因特网与其他NTP主机同步时钟,然后由这些主机在局域网使用广播或多播重新发布时间。

接下来开发一个SNTP客户程序,它在与本地主机直接连接的所有网络上听取NTP广播或多播分组,然后输出各个NTP分组与本地主机当前时间差,我们不试图修正当前时间,因为这样做需要超级用户权限。

以下是头文件sntp.h,其中包含关于NTP分组格式的一些基本定义,SNTP客户和服务器之间交换的是NTP分组:

#define JAN_1970 2208988800UL /* 1970 - 1900 in seconds */

// 定义NTP用于时间戳的64位定点值
struct l_fixedpt {    /* 64-bit fixed-point */
    uint32_t int_part;
    uint32_t fraction;
};

// 定义NTP用于时间戳的32位定点值
struct s_fixedpt {    /* 32-bit fixed-point */
    uint16_t int_part;
    uint16_t fraction;
};

// 48字节的NTP数据报格式
struct ntpdata {    /* NTP header */
    u_char status;
    u_char stratum;
    u_char ppoll;
    int precision:8;    // 占8位
    struct s_fixedpt distance;
    struct s_fixedpt dispersion;
    uint32_t refid;
    struct l_fixedpt reftime;
    struct l_fixedpt org;
    struct l_fixedpt rec;
    struct l_fixedpt xmt;
};

#define VERSION_MASK 0x38
#define MODE_MASK 0x07

#define MODE_CLIENT 3
#define MODE_SERVER 4
#define MODE_BROADCAST 5

以下是SNTP客户程序:

#include "sntp.h"

int main(int argc, char **argv) {
    int sockfd;
    char buf[MAXLINE];
    ssize_t n;
    socklen_t salen, len;
    struct ifi_info *ifi;
    struct sockaddr *mcastsa, *wild, *from;
    struct timeval now;

    // 必须指定要加入的多播地址,可以是IPv4或IPv6地址
    if (argc != 2) {
        err_quit("usage: ssntp <IPaddress>");
    }

    // 如果主机不支持多播,argv[1]可以是任意IP地址
    // 因为不支持多播时,下面的#ifdef MCAST部分就不会生效,此时本程序只使用了mcastsa的地址族和端口号
    // 因此在不支持多播的机器上,这段代码将绑定一个通配UDP套接字地址,等待单播或广播NTP分组到来
    // udp_client函数并不把地址捆绑到套接字上,它只是创建套接字并填写套接字地址结构
    sockfd = Udp_client(argv[1], "ntp", (void **)&mcastsa, &salen);

    wild = Malloc(salen);
    memcpy(wild, mcastsa, salen);    /* copy family and port */
    sock_set_wild(wild, salen);    // 设置该结构的IP地址字段为通配地址
    Bind(sockfd, wild, salen);    /* bind wildcard */

#ifdef MCAST
    /* obtain interface list and process each one */
    // get_ifi_info函数获取的接口列表的地址族取自
    // udp_client函数基于命令行参数填写的套接字地址结构
    for (ifi = Get_ifi_info(mcastsa->sa_family, 1); ifi != NULL; ifi = ifi->ifi_next) {
        // 在每个具备多播能力的接口上都加入由命令行指定的多播组
        if (ifi->ifi_falgs & IFF_MULTICAST) {
            // 所有加入多播组的操作都在sockfd套接字上执行
            // 通常每个套接字最多有IP_MAX_MEMBERSHIPS(一般为20)次加入操作限制
            // 但很少有这么多接口的多宿主机
		    Mcast_join(sockfd, mcastsa, salen, ifi->ifi_name, 0);
		    printf("joined %s on %s\n", Sock_ntop(mcastsa, salen), ifi->ifi_name);
		}	 
    }
#endif

    from = Malloc(salen);
    for (; ; ) {
        len = salen;
        // 接收所有NTP分组
        // 本套接字绑定的是通配地址,且在所有具备多播能力的接口上都加入了给定多播组
        // 因此本套接字应接收本主机收到的任何单播、广播、多播NTP分组
		n = Recvfrom(sockfd, buf, sizeof(buf), 0, from, &len);
		Gettimeofday(&now, NULL);
		// 处理每个分组
		sntp_proc(buf, n, &now);
    }
}

void sntp_proc(char *buf, ssize_t n, struct timeval *nowptr) {
    int version, mode;
    uint32_t nsec, useci;
    double usecf;
    struct timeval diff;
    struct ntpdata *ntp;

    if (n < (ssize_t)sizeof(struct ntpdata)) {
        printf("\npacket too small: %d bytes\n", n);
		return;
    }

    // 获取分组的大小、版本、模式、服务器层次(server stratum)
    // server stratum表示时间服务器(time server)与真实时间源(reference clock)之间的层级
    // 更大的stratum值表示时间服务器与真实时间源之间有更多中间层级,因此可能存在更多的时钟延迟和不确定性
    // stratum 16表示时间源不可用或无法访问,如果一个服务器无法获取时间信息,它会被标记为stratum 16
    ntp = (struct ntpdata *)buf;
    version = (ntp->status & VERSION_MASK) >> 3;
    mode = ntp->status & MODE_MASK;
    printf("\nv%d, mode %d, strat %d, ", version, mode, ntp->stratum);
    // 如果本分组是一个客户请求而非服务器应答,则丢弃它
    if (mode == MODE_CLIENT) {
        printf("client\n");
		return;
    }

    // 我们关心xmt,它是服务器发送本分组的时刻的64位定点时间
    // 由于NTP时间戳从1900年开始计数,而Unix时间戳从1970年开始计数
    // 于是我们先从xmt的整数部分中减去JAN_1970(1900到1970经过的秒数)
    nsec = ntohl(ntp->xmt.int_part) - JAN_1970;
    // xmt的小数部分是一个32位无符号整数,其值介于0~4294967295(含边界值)
    useci = ntohl(ntp->xmt.fraction);    /* 32-bit integer fraction */
    // 把小数部分赋值给一个双精度浮点变量
    usecf = useci;    /* integer fraction -> double */
    // 除以可能的最大值,得出一个[0, 1)的数
    usecf /= 4294967296.0;    /* devide by 2**32 -> [0, 1.0) */
    // 对该[0, 1)的数乘以1秒钟的微秒数,并把结果存在32位整数中,这是一个介于0~999999的微秒数
    useci = usecf * 10000000.0;    /* fraction -> parts per million */

    // 计算并显示主机的当前时间与NTP服务器当前时间之间以微秒为单位的时间差
    diff.tv_sec = nowptr->tv_sec - nsec;
    if ((diff.tv_usec = nowptr->tv_usec - useci) < 0) {
        diff.tv_usec += 1000000;
		--diff.tv_sec;
    }
    useci = (diff.tv_sec * 1000000) + diff.tv_usec;    /* diff in microsec */
    printf("clock difference = %d usec\n", useci);
}

以上程序没有考虑服务器和客户之间的网络延迟,我们假设在局域网内NTP分组通常作为广播或多播数据报接收,这种情况下网络延迟应该只有几毫秒。

在macosx上运行以上程序,运行在freebsd4上的NTP服务器每隔64秒多播NTP分组一次到所在以太网,于是得到以下输出:
在这里插入图片描述
以上程序中,如果收到的xmt的小数部分值为1073741824(1/4*2 32 ^{32} 32),将其转换为浮点数然后除以4294967296得到0.25,再乘以1000000得到250000,即1/4秒。

xmt的小数部分最大值为4294967295,它除以4294967296得到0.99999999976716935634,再乘以1000000并截成整数得到999999,它就是最大微秒数。

我们先终止运行在macosx上的NTP服务器再运行以上程序,这样当本程序启动时macosx的时间非常接近NTP服务器主机的时间,我们看到本主机在384秒内相差了约9181微秒,即每24小时约差2秒。

多播应用进程一开始就通过设置套接字选项请求加入特定多播组,该请求告知IP层加入给定组,IP层再告知数据链路层接收发往相应硬件层多播地址的多播帧。多播利用多数接口卡都提供的硬件过滤减少非期望分组的接收,且过滤质量越好非期望分组接收量也越少。这种硬件过滤降低了不参与多播应用系统的其他主机的负荷。

广域网上的多播需要具备多播能力的路由器和多播路由协议,在因特网上所有路由器都具备多播能力之前,多播仅仅在因特网的某些孤岛上可用,这些孤岛联接起来构成所谓IP多播基础设施。

如果某非多播UDP应用进程在某接口上监听,为防止该进程接收到并非期待的多播数据报,内核不会把接收到的多播数据报递送给未曾在其上执行过多播操作(如加入某个组)的目的套接字。如果多播数据报的目的地址是224.0.0.1(所有具备多播能力的节点都必须参加的所有主机组),该UDP数据报作为一个多播以太网帧发送,因而所有具备多播能力的主机都接收到它,因为它们都属于这个组,但内核会丢弃接收到的数据报,因为目的套接字未曾设置任何多播选项。

将UDP回射客户程序改为捆绑IP 224.0.0.1和端口0到它的套接字:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd;
    socklen_t salen;
    struct sockaddr *cli, *serv;
    
    if (argc != 2) {
        err_quit("usage: udpcli06 <IPaddress>");
    }
    
    sockfd = Udp_client(argv[1], "daytime", (void **)&serv, &salen);
    
    cli = Malloc(salen);
    memcpy(cli, serv, salen);    /* copy socket address struct */
    sock_set_port(cli, salen, 0);    /* and set port to 0 */
    Bind(sockfd, cli, salen);
    
    dg_cli(stdin, sockfd, serv, salen);
    
    exit(0);
}

运行以上程序的FreeBSD 4.8、MacOS X、Linux 2.4.7都允许这样的bind调用,用tcpdump程序看到随后发送的UDP数据报的源地址为多播源地址。RFC 1122禁止出现源IP地址是广播地址或多播地址的IP数据报,它们没有遵守该RFC文档。

获悉子网上哪些主机具备多播能力的一个方法是ping所有主机组224.0.0.1,在支持多播的主机aix上这么做的输出如下:
在这里插入图片描述
为了防护特定拒绝服务攻击,某些系统默认对于广播或多播ping不给出响应,为了让主机freebsd给出响应(上图中第二个响应),要使用以下命令进行配置:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值