广播与多播:广播地址、多播组管理、setsockopt()多播选项

广播和多播,说白了就是「一对多」的通信方式。单播就像两个人打电话,广播就像在广场上用大喇叭喊话,多播则像拉了个微信群——只有群里的人能收到消息。

我在做视频流推送项目时,就深刻体会过它们的区别。当时要在局域网内分发监控画面,用广播吧,所有机器都得处理数据包,CPU 直接拉满;改用多播后,只有订阅了视频流的客户端才处理,性能瞬间就上来了。

广播地址:谁都能听到的大喇叭

广播分两种:本地广播直接广播。本地广播的 IP 地址是 255.255.255.255,数据包只在当前子网内传播,路由器不会转发。直接广播则是把主机位全置为 1,比如 192.168.1.255,理论上路由器可以转发,但实际中很多路由器默认禁止。

注意:IPv6 没有广播这个概念,取而代之的是多播。所以如果你在搞新项目,建议直接上多播。

广播的代码实现很简单,核心就两步:

// 1. 设置 socket 选项,允许广播
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

// 2. 发送到广播地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("192.168.1.255");
sendto(sockfd, buf, len, 0, (struct sockaddr*)&addr, sizeof(addr));

嗯,这里有个坑。我曾经在虚拟机里测试广播,死活收不到数据,后来发现是虚拟网卡没开广播选项。所以记得检查一下 ifconfig 输出里有没有 BROADCAST 标志。

多播组管理:拉个微信群

多播比广播优雅得多。它用 D 类 IP 地址(224.0.0.0 ~ 239.255.255.255),其中 224.0.0.1 是所有主机的组地址,224.0.0.2 是所有路由器的组地址。你想想看,如果广播是「所有人必须听」,那么多播就是「感兴趣的人自己来听」。

多播组管理主要涉及三个操作:加入组离开组设置接口。这些操作都通过 setsockopt() 完成。

操作选项说明
加入多播组IP_ADD_MEMBERSHIP让 socket 加入指定多播组
离开多播组IP_DROP_MEMBERSHIP让 socket 离开指定多播组
设置多播接口IP_MULTICAST_IF指定从哪个网卡发送多播数据
设置 TTLIP_MULTICAST_TTL控制多播包的传播范围
环回控制IP_MULTICAST_LOOP是否允许本机收到自己发的多播

核心结构体: struct ip_mreq 用于加入/离开多播组,包含两个字段:imr_multiaddr(多播组地址)和 imr_interface(本地接口地址,用 INADDR_ANY 表示任意接口)。

setsockopt() 多播选项:实战代码

我个人习惯把多播的发送端和接收端分开写,这样逻辑更清晰。先看接收端(加入多播组):

// 接收端:加入多播组
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.88");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);

if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, 
               &mreq, sizeof(mreq)) < 0) {
    perror("加入多播组失败");
    // 我曾经在这里卡了半天,后来发现是防火墙没关
}

// 绑定端口(注意:多播接收端必须绑定)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

再看发送端:

// 发送端:设置 TTL 和接口
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

// 设置 TTL,默认是 1,只能在本子网传播
int ttl = 64;
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));

// 设置环回,1 表示本机也能收到自己发的多播
int loop = 1;
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop));

// 发送到多播组
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = inet_addr("224.0.0.88");
sendto(sockfd, "hello", 5, 0, (struct sockaddr*)&addr, sizeof(addr));
避坑指南:我曾经在发送端忘记设置 TTL,结果多播包只能在同一个交换机下传播,跨网段就丢了。TTL 值建议设大一点(比如 64),但别超过 255,否则路由器会直接丢弃。

多播与广播的核心区别

我用一张图来总结它们的关系:

广播 vs 多播 核心对比 广播 IP 地址:255.255.255.255 或子网广播地址 传播范围:本子网 性能:所有主机都要处理 安全性:低,容易被监听 适用场景:DHCP、ARP setsockopt: SO_BROADCAST 多播 IP 地址:224.0.0.0 ~ 239.255.255.255 D 类地址 传播范围:可跨网段(TTL 控制) 性能:只有组成员处理 安全性:较高,需加入组 适用场景:视频流、数据分发 setsockopt: IP_ADD_MEMBERSHIP 等

多播的 TTL 与环回

TTL 控制多播包的「寿命」。每经过一个路由器,TTL 减 1,减到 0 就丢弃。默认值是 1,意味着只能在同一个子网内传播。如果你想让多播跨网段,记得设大一点。

环回控制则决定本机是否收到自己发的多播包。默认是开启的(值为 1)。我在调试时经常关掉它,否则日志里会看到自己发的包又被自己收回来,容易混淆。

// 关闭环回,避免自己收到自己发的包
int loop = 0;
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop));
注意:多播的环回和广播不同。广播默认不会环回,而多播默认会环回。如果你写的是客户端-服务器模式,服务器发多播时,记得考虑环回的影响。

多播组管理的高级话题

除了基本的加入/离开组,还有几个选项值得关注:

  • IP_MULTICAST_IF:指定从哪个网卡发送多播。多网卡机器上必须设置,否则可能从错误的接口发出。
  • IP_DROP_MEMBERSHIP:离开多播组。程序退出时 socket 会自动离开,但显式调用更安全。
  • IP_MULTICAST_ALL:Linux 3.5+ 新增,控制是否接收所有多播组的数据。默认是 0,只接收已加入的组。

我记得有一次在生产环境排查问题,发现多播数据偶尔丢失。后来定位到是网卡开启了 rx-flow-hash 导致多播包被哈希到不同队列,而应用程序只绑定了一个队列。解决办法是关闭网卡的 RSS 哈希,或者用 setsockopt() 设置 SO_ATTACH_FILTER 过滤。

调试技巧:tcpdump -i eth0 igmp 可以抓取 IGMP 报文,观察多播组的加入和离开过程。用 netstat -g 可以查看当前系统加入的多播组。

广播和多播,说白了就是「一对多」的两种实现。广播简单粗暴,适合局域网内的发现服务;多播灵活可控,适合大规模数据分发。我个人更推荐多播,尤其是 IPv6 时代,广播已经被彻底抛弃了。

嗯,最后提醒一句:不管用哪种方式,记得考虑网络拥塞。我曾经在 100 台机器上同时用广播推送日志,结果交换机直接挂了。后来改成多播 + 限速,问题才解决。


公众号:蓝海资料掘金营,微信deep3321