# 07 | 性能好,效率高的一对多通讯该如何实现? 你好,我是陶辉。从这一讲开始,我们将从单机进入网络层面的性能优化。 我们接触过的绝大多数通讯方式,无论是面向连接的HTTP协议,还是无连接的DNS协议,都是一对一收发消息的。其实,除了一对一,还有一对多的通讯方式,它在网络资源的利用上效率要比一对一高得多。这种一对多的通讯方式,在局域网中有很广泛的应用,常见的ARP欺骗、泛洪攻击等,都是通过一对多通讯进行的。 当应用场景中用一对多代替一对一通讯时,发送方性能会获得很大的提升,整个局域网的效率也会提高。比如,源主机的带宽只有1Gbps,如果采用一对一的方式向100个客户端发送流媒体,这100台主机的带宽之和不会超过1Gbps。但采用一对多传输时,总带宽就可以达到100Gbps。 除了能提升性能以外,由于一对多通讯可同时向所有主机发送消息,这就在功能层面上可以替换许多人工操作。比如分布式系统的服务发现,使用人工配置既容易出错,速度也慢,而用广播就可以轻松实现自动化服务发现。 一对多通讯协议一直在发展,在运营商的IPTV网络的视频直播中,它就得到了广泛的应用。即使你暂时不会用到一对多这种方式,也应当了解下它是怎么工作的,熟悉它的工作原理后,还能更深入地理解一对一通讯协议。 这一讲,我们就来学习如何实现一对多通讯。 ## 广播是怎么实现的? 一对多通讯分为两种:对局域网内所有主机发送消息的叫做**广播**,而对部分主机发送消息的,则叫做**组播**。我们先来看一下广播是怎么实现的。 使用广播要改用UDP协议。可能你会问,为什么不能使用最熟悉的TCP协议呢?这要从TCP协议的分层说起。 1978年在TCP协议迭代了3个版本后,才被Jon Postel(IANA创始人)提出违反了网络分层原则,网络层和传输层耦合在一起很难扩展。于是在TCP的第4个迭代版本中把协议一分为二,包括网络层IP协议和传输层TCP协议(这也是今天的IP协议被称为IPv4的原因)。 ![](https://static001.geekbang.org/resource/image/95/02/95d8fe24b6343fef34579f240696dd02.jpg) 当你访问Internet站点时,IP协议会将数据通过网络设备穿越多个卫星、光纤等网络,才能送到服务器。而网络设备天然就拥有广播能力,当它在一个网络端口上收到主机发来的报文时,可以向其他端口上的所有主机重发一遍,这就是广播,如下图所示: ![](https://static001.geekbang.org/resource/image/ce/f0/ce45d52f5c87bcae9c4adae8056c21f0.jpg) **虽然IP协议已经具有广播功能,但实际编程中并不会直接使用IP协议发送广播,因为它很难与进程关联起来。** 根据网络分层模型,上层协议可以使用下层协议的功能,所以传输层协议拥有IP协议的广播能力。同时,传输层通过端口号把网络报文和进程关联在了一起,就像TCP的80端口,把HTTP消息与Nginx等Web Server关联在一起。 ![](https://static001.geekbang.org/resource/image/13/ac/13d968cbacd229f242764557957d3bac.jpg) 然而,传输层的TCP协议为了保证可靠性,建立了逻辑上的连接概念,由于一个连接上只能有两方,所以TCP无法进行一对多通讯。而传输层的UDP协议无需建立连接,所以我们常用UDP协议发送广播。 广播的性能高有两个原因:首先,交换机直接转发给接收方,要比从发送方到接收方的传输路径更短。其次,原本需要发送方复制多份报文再逐一发送至各个接受者的工作,被交换机完成了,这既分担了发送方的负载,也充分使用了整个网络的带宽。 那么,交换机收到消息后,怎么知道这是广播报文并转发给整个网络呢?我们知道,以太网中的数据链路层,通过硬件的MAC地址来传播消息,交换机就通过报文的MAC地址来确定是否需要广播。**当交换机收到目标MAC地址是ff:ff:ff:ff:ff:ff的报文时,便知道这是一个广播报文,**才会将它转发给局域网中的所有主机,否则只会转发给MAC地址对应端口上的主机。 不过,我们写代码时无法控制底层的MAC地址,只能填写目标IP地址。什么样的目标IP地址,会生成广播MAC地址呢?**如果只是对所在子网进行广播,那么使用受限广播地址255.255.255.255就可以了;如果局域网划分了多个子网,主机需要向其他子网广播,则需要正确地设置直接广播地址(路由器需要打开直接广播功能)。** ## 如何正确地设置直接广播IP地址? 怎么设置直接广播的IP地址呢?我们首先得了解IP地址的构成。 由于IP协议需要跨越多个网络工作,所以IP地址被一分为二,包括前边的网络ID和后边的主机ID,其中,**网络ID用于不同网络间的寻址,而主机ID则用于在本地局域网内通讯。** 举个例子,如果你的局域网IP地址是192.168.0.101,那么网络ID就是192.168.0,而主机ID则是101(这里假定网络管理员没有继续划分C类子网)。 这是因为,以192.168打头的IP地址,被称为C类地址,而C类地址中,最后1个十进制数字代表主机ID。如果IP地址是172.16.20.227,这就是B类地址,此时,172.16是网络ID,而20.227才是主机ID。 所以,IP地址的前缀数字不同,主机ID的划分位置也不同。事实上,IP地址一共被划分为A、B、C、D、E,5个类别,它的划分依据正是IP地址转换为二进制后,用前4个比特位作为依据的。如果第1个比特位为0,这就是A类地址,它的网络ID是IP地址的第1到第8比特位,而主机ID则是随后的24个比特位。如果局域网IP地址第1个数字是10,这就是A类私有地址(局域网中的地址不能在公网中使用,统称为私有地址或者内网地址)。 ![](https://static001.geekbang.org/resource/image/4f/e1/4f9c58b60404e2f874802b6a2ec207e1.jpg) 类似的,若前2个比特位是10,则是B类地址,它的主机ID是后16位;如果前3个比特位为110,这就是C类地址,它的主机ID是后8位。显然,以192打头的地址,前3位正是110,所以这是C类地址。 ![](https://static001.geekbang.org/resource/image/d6/5e/d67b7a52601f80027576c866cf80e25e.jpg) 此外还有D类组播地址和E类预留实验地址。 清楚了划分方法后,再来看如何通过修改主机ID将IP地址改为直接广播地址。如果你留心观察IP地址,会发现主机ID不会出现全0和全1这两种情况,这是因为全0和全1有特殊用途,其中全0特指它自己(所以0.0.0.0可以指代本机IP),而全1表示全部主机。 **所以,主机ID的比特位全部设为1后就是广播地址。**比如,192.168.0.101是C类地址,把主机ID从101改为255后,就可以用192.168.0.255发送广播了。 然而,事情到这并没有完。一个A类网络可以容纳千万台主机,B类网络则只能容纳6万多台主机,C类网络则最多容纳254台主机。仅有的三类网络中主机数量差距太大,而世界上存在各种规模的企业,它们所需网络中的主机规模千差万别,上述划分方式太过单一,无法满足各类企业的需求,**于是诞生了CIDR这种新的划分方式,它通过子网掩码(或者叫Netmask),可以在任意的位置将IP地址拆分为网络ID和主机ID,扩展了A、B、C三类网络的用法。** 当你查看主机的IP地址时,就会看到其后跟着一个类似IP地址的子网掩码。子网掩码必须把它展开成二进制才能使用,这样,掩码前N位为1时,就表示IP地址的前N位是网络ID,而掩码后面剩余的位全是0,表示IP地址对应的位是主机ID。 比如,若192.168.0.101的子网掩码是255.255.255.192,就表示IP地址的前26位是网络ID,后6位是主机ID,将主机ID置为全1后,就得到了它的广播地址192.168.0.127,如下图所示: ![](https://static001.geekbang.org/resource/image/65/08/6586d4ec875f63b19993b78c7a11e808.jpg) 到这里,我们设置好IP地址后,再把socket句柄设置SO\_BROADCAST属性,就可以发送广播了。广播虽然有很多优点,可是一旦被滥用,很容易产生网络风暴,所以路由器默认是不转发广播报文的。 ## 用更精准的组播来做服务发现 当你用UDP广播来做分布式系统的服务发现,会遇到这样一个问题:若并非网络内的所有主机都属于分布式系统,那么,当指定了端口的UDP广播报文到达其他主机时,会怎么样呢?这些广播报文在这3个步骤后会被丢弃: * 第1步,网卡设备收到报文后,查看报文中的目标MAC地址是否与本机的MAC地址匹配,如果不匹配就会丢弃。广播MAC地址默认匹配,继续交由上层的IP协议栈处理; * 第2步,IP协议栈查看目标IP地址是否为本机IP地址,不匹配也会丢弃报文。上文介绍过的广播IP地址同样默认匹配,交由传输层协议继续处理。 * 第3步,传输层检查目标端口是否有进程在监听,如果没有则丢弃报文,反之则交付给进程处理。不属于集群的主机自然不会启动服务监听端口,在这一步才会丢弃广播报文。 ![](https://static001.geekbang.org/resource/image/1c/b3/1c8b6032474debdd2a4d4569a1752ab3.jpg) 可见,对于不属于分布式集群的主机而言,广播报文既占用了它们的带宽,这3步协议栈的操作也消耗了CPU的计算力。有什么办法能缩小广播的范围,消除它加在无关主机上的负载呢? 组播可以做到。组播是一种“定向广播”,它设定了一个虚拟组,用组播IP来标识。这个虚拟组中可以包含多个主机的IP,当向对应的组播IP发送消息时,仅在这个组内的主机才能收到消息。 组播IP与常见的单播IP不同,它是前文介绍过5类IP地址中的D类地址,32位IP地址的前4位必须是1110,因此组播IP地址的范围是从224.0.0.0到239.255.255.255。 当设置好组播IP地址后,还要通过管理组播地址的IGMP协议(Internet Group Management Protocol),将主机IP地址添加进虚拟组中。编程语言提供的setsockopt函数,就可以操作IGMP协议管理组播地址。比如,使用参数IP\_ADD\_MEMBERSHIP就能够向虚拟组中增加IP,而IP\_DROP\_MEMBERSHIP则可以从组中去除某个主机的IP。 [这里](https://github.com/russelltao/geektime-webprotocol/tree/master/python%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81)有一个可运行的python测试代码,供你参考。如果你想进一步了解组播的细节,可以观看[《Web协议详解与抓包实战》第117课](https://time.geekbang.org/course/detail/175-134405)。 组播相对于广播而言 ,除了能够更精准的管理组播范围,还能够跨越多个网络工作。当然,如果将多个网络中的IP加入同一虚拟组时,需要涉及到的路由器都可以正确地处理这些IP地址,且都能支持IGMP协议。 ## 小结 最后我们对这一讲做一个总结。 由于一对多通讯能够充分利用整体网络的性能,而且通过交换机能够同时向许多主机发送消息,所以在局域网内有广泛的应用。 在TCP协议分层后,IP协议天然就支持一对多通讯方式。TCP协议面向连接的特性使它放弃了一对多的通讯方式,而UDP协议则继承了IP协议的这一功能。所以,在一对多通讯场景中,我们会选择UDP协议。 正确输入广播地址的前提,是理解IP地址如何划分为网络ID和主机ID。当主机ID所有的比特位改为全1时,IP地址就表示该网络下的所有主机,这就是广播地址。当向广播地址发送UDP消息时,网络中的所有主机都会收到。广播在局域网中有广泛的应用,转换IP地址与MAC地址的ARP协议就是用广播实现的。 广播对无关的主机增加了不必要的负担,而组播可以更精准地“定向”广播。组播地址也被称为D类地址,它描述的虚拟组要通过IGMP协议管理。网络API中的setsockopt函数可以通过IGMP协议,向虚拟组中添加或者删除IP地址。当路由器支持IGMP协议时,组播就可以跨越多个网络实现更广泛的一对多通讯。 广播和组播能够充分地使用全网带宽,也通过交换机等网络设备分散了发送主机的负载。但它很难对每台接收主机提供定制化服务,这样可靠传输就很难实现。这使得它们在更关注及时性、对丢包不敏感的流媒体直播中更有应用前景。 这一讲我们介绍了许多网络概念,这些也是理解后续内容的基础。从下一讲开始,我们将进入更复杂的一对一通讯协议。 ## 思考题 最后,请你思考下,你使用或者了解过哪些一对多的通讯协议?它们的优缺点,以及未来的发展方向又是什么?欢迎你留言与我探讨。 感谢阅读,如果你觉得今天学习的内容对你有帮助,也欢迎把它分享给你的朋友。