<audio id="audio" title="07 | 性能好，效率高的一对多通讯该如何实现？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/99/b9c73eae64c2fc4965eb3496960b5699.mp3"></audio>

你好，我是陶辉。从这一讲开始，我们将从单机进入网络层面的性能优化。

我们接触过的绝大多数通讯方式，无论是面向连接的HTTP协议，还是无连接的DNS协议，都是一对一收发消息的。其实，除了一对一，还有一对多的通讯方式，它在网络资源的利用上效率要比一对一高得多。这种一对多的通讯方式，在局域网中有很广泛的应用，常见的ARP欺骗、泛洪攻击等，都是通过一对多通讯进行的。

当应用场景中用一对多代替一对一通讯时，发送方性能会获得很大的提升，整个局域网的效率也会提高。比如，源主机的带宽只有1Gbps，如果采用一对一的方式向100个客户端发送流媒体，这100台主机的带宽之和不会超过1Gbps。但采用一对多传输时，总带宽就可以达到100Gbps。

除了能提升性能以外，由于一对多通讯可同时向所有主机发送消息，这就在功能层面上可以替换许多人工操作。比如分布式系统的服务发现，使用人工配置既容易出错，速度也慢，而用广播就可以轻松实现自动化服务发现。

一对多通讯协议一直在发展，在运营商的IPTV网络的视频直播中，它就得到了广泛的应用。即使你暂时不会用到一对多这种方式，也应当了解下它是怎么工作的，熟悉它的工作原理后，还能更深入地理解一对一通讯协议。

这一讲，我们就来学习如何实现一对多通讯。

## 广播是怎么实现的？

一对多通讯分为两种：对局域网内所有主机发送消息的叫做**广播**，而对部分主机发送消息的，则叫做**组播**。我们先来看一下广播是怎么实现的。

使用广播要改用UDP协议。可能你会问，为什么不能使用最熟悉的TCP协议呢？这要从TCP协议的分层说起。

1978年在TCP协议迭代了3个版本后，才被Jon Postel（IANA创始人）提出违反了网络分层原则，网络层和传输层耦合在一起很难扩展。于是在TCP的第4个迭代版本中把协议一分为二，包括网络层IP协议和传输层TCP协议（这也是今天的IP协议被称为IPv4的原因）。

<img src="https://static001.geekbang.org/resource/image/95/02/95d8fe24b6343fef34579f240696dd02.jpg" alt="">

当你访问Internet站点时，IP协议会将数据通过网络设备穿越多个卫星、光纤等网络，才能送到服务器。而网络设备天然就拥有广播能力，当它在一个网络端口上收到主机发来的报文时，可以向其他端口上的所有主机重发一遍，这就是广播，如下图所示：

<img src="https://static001.geekbang.org/resource/image/ce/f0/ce45d52f5c87bcae9c4adae8056c21f0.jpg" alt="">

**虽然IP协议已经具有广播功能，但实际编程中并不会直接使用IP协议发送广播，因为它很难与进程关联起来。**

根据网络分层模型，上层协议可以使用下层协议的功能，所以传输层协议拥有IP协议的广播能力。同时，传输层通过端口号把网络报文和进程关联在了一起，就像TCP的80端口，把HTTP消息与Nginx等Web Server关联在一起。

<img src="https://static001.geekbang.org/resource/image/13/ac/13d968cbacd229f242764557957d3bac.jpg" alt="">

然而，传输层的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类私有地址（局域网中的地址不能在公网中使用，统称为私有地址或者内网地址）。

<img src="https://static001.geekbang.org/resource/image/4f/e1/4f9c58b60404e2f874802b6a2ec207e1.jpg" alt="">

类似的，若前2个比特位是10，则是B类地址，它的主机ID是后16位；如果前3个比特位为110，这就是C类地址，它的主机ID是后8位。显然，以192打头的地址，前3位正是110，所以这是C类地址。

<img src="https://static001.geekbang.org/resource/image/d6/5e/d67b7a52601f80027576c866cf80e25e.jpg" alt="">

此外还有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，如下图所示：

<img src="https://static001.geekbang.org/resource/image/65/08/6586d4ec875f63b19993b78c7a11e808.jpg" alt="">

到这里，我们设置好IP地址后，再把socket句柄设置SO_BROADCAST属性，就可以发送广播了。广播虽然有很多优点，可是一旦被滥用，很容易产生网络风暴，所以路由器默认是不转发广播报文的。

## 用更精准的组播来做服务发现

当你用UDP广播来做分布式系统的服务发现，会遇到这样一个问题：若并非网络内的所有主机都属于分布式系统，那么，当指定了端口的UDP广播报文到达其他主机时，会怎么样呢？这些广播报文在这3个步骤后会被丢弃：

- 第1步，网卡设备收到报文后，查看报文中的目标MAC地址是否与本机的MAC地址匹配，如果不匹配就会丢弃。广播MAC地址默认匹配，继续交由上层的IP协议栈处理；
- 第2步，IP协议栈查看目标IP地址是否为本机IP地址，不匹配也会丢弃报文。上文介绍过的广播IP地址同样默认匹配，交由传输层协议继续处理。
- 第3步，传输层检查目标端口是否有进程在监听，如果没有则丢弃报文，反之则交付给进程处理。不属于集群的主机自然不会启动服务监听端口，在这一步才会丢弃广播报文。

<img src="https://static001.geekbang.org/resource/image/1c/b3/1c8b6032474debdd2a4d4569a1752ab3.jpg" alt="">

可见，对于不属于分布式集群的主机而言，广播报文既占用了它们的带宽，这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协议时，组播就可以跨越多个网络实现更广泛的一对多通讯。

广播和组播能够充分地使用全网带宽，也通过交换机等网络设备分散了发送主机的负载。但它很难对每台接收主机提供定制化服务，这样可靠传输就很难实现。这使得它们在更关注及时性、对丢包不敏感的流媒体直播中更有应用前景。

这一讲我们介绍了许多网络概念，这些也是理解后续内容的基础。从下一讲开始，我们将进入更复杂的一对一通讯协议。

## 思考题

最后，请你思考下，你使用或者了解过哪些一对多的通讯协议？它们的优缺点，以及未来的发展方向又是什么？欢迎你留言与我探讨。

感谢阅读，如果你觉得今天学习的内容对你有帮助，也欢迎把它分享给你的朋友。
