广播与多播:广播地址、多播组管理、setsockopt()多播选项
广播和多播,说白了就是「一对多」的通信方式。单播就像两个人打电话,广播就像在广场上用大喇叭喊话,多播则像拉了个微信群——只有群里的人能收到消息。
我在做视频流推送项目时,就深刻体会过它们的区别。当时要在局域网内分发监控画面,用广播吧,所有机器都得处理数据包,CPU 直接拉满;改用多播后,只有订阅了视频流的客户端才处理,性能瞬间就上来了。
广播地址:谁都能听到的大喇叭
广播分两种:本地广播和直接广播。本地广播的 IP 地址是 255.255.255.255,数据包只在当前子网内传播,路由器不会转发。直接广播则是把主机位全置为 1,比如 192.168.1.255,理论上路由器可以转发,但实际中很多路由器默认禁止。
广播的代码实现很简单,核心就两步:
// 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 | 指定从哪个网卡发送多播数据 |
| 设置 TTL | IP_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 控制多播包的「寿命」。每经过一个路由器,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 台机器上同时用广播推送日志,结果交换机直接挂了。后来改成多播 + 限速,问题才解决。