25、UDP广播:广播地址、setsockopt设置SO_BROADCAST、广播接收
UDP广播,说白了就是一台机器往整个局域网里“吼一嗓子”。
你想想看,平时我们写UDP通信,都是点对点的——我知道你的IP,我把数据包发给你。但广播不一样,它不关心谁在听,只要在同一个网段里,所有机器都能收到这份数据。
我在项目中遇到过好几次这种场景:设备发现、服务宣告、网络唤醒……这些功能用单播一个个发,效率太低,广播才是正解。
25.1 广播地址长什么样?
广播地址其实很简单。拿最常见的C类地址192.168.1.x来说,它的广播地址就是192.168.1.255。
规则是这样的:把IP地址的网络部分保持不变,主机部分全部置为1。比如:
- IP 192.168.1.100,子网掩码255.255.255.0 → 广播地址 192.168.1.255
- IP 10.0.0.5,子网掩码255.0.0.0 → 广播地址 10.255.255.255
嗯,这里要注意:广播地址只在当前子网内有效。路由器默认不会转发广播包,所以别指望它能跨网段传播。
25.2 发送广播前必须做的一件事:setsockopt
默认情况下,操作系统不允许你发送UDP广播包。这是出于安全考虑——万一哪个程序乱吼,整个网络都得遭殃。
想发广播?你得先告诉内核:“我确实要发广播,请放行。” 这个操作就是通过setsockopt设置SO_BROADCAST选项。
我个人习惯在创建socket之后,bind之前就把这个选项设好。省得后面忘了。
int sockfd;
int broadcast = 1;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 设置SO_BROADCAST选项
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) {
perror("setsockopt");
close(sockfd);
exit(1);
}
printf("广播权限已开启\n");
这段代码里,broadcast变量赋值为1,表示开启。如果赋值为0,就是关闭。
25.3 发送广播数据
设置好权限之后,发送广播和发送普通UDP数据包几乎一样。唯一的区别是:目标地址填广播地址。
struct sockaddr_in broadcast_addr;
char *message = "Hello, everyone! This is a broadcast.";
memset(&broadcast_addr, 0, sizeof(broadcast_addr));
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(8888);
broadcast_addr.sin_addr.s_addr = inet_addr("192.168.1.255");
int ret = sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
if (ret < 0) {
perror("sendto");
} else {
printf("广播消息已发送,长度:%d\n", ret);
}
你看,代码结构和普通UDP发送一模一样。只是目标IP换成了广播地址。
先获取本机IP和子网掩码,然后IP | (~子网掩码) 就能得到广播地址。
25.4 接收广播数据
接收端就更有意思了。你不需要做任何特殊设置,只要bind到一个端口,就能收到发往该端口的广播数据。
为什么会这样?因为广播包到达网卡时,网卡会把它当作普通数据包交给协议栈。只要你的socket绑定了对应端口,就能收到。
int sockfd;
struct sockaddr_in my_addr;
char buffer[1024];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 接收所有网卡的广播
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
perror("bind");
close(sockfd);
exit(1);
}
printf("等待广播数据...\n");
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&from_addr, &from_len);
if (n > 0) {
buffer[n] = '\0';
printf("收到来自 %s:%d 的广播:%s\n",
inet_ntoa(from_addr.sin_addr),
ntohs(from_addr.sin_port),
buffer);
}
注意这里我用了INADDR_ANY,意思是绑定到所有网络接口。这样不管广播从哪个网卡进来,都能收到。
25.5 广播通信的完整流程
我把整个流程画了张图,方便你理解:
从图上可以看得很清楚:发送端需要三步,接收端只需要两步。广播数据包会同时到达所有接收端。
25.6 避坑指南
我做了这么多年网络编程,在广播上栽过的跟头可不少。给你列几个最常见的:
| 问题 | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| 收不到广播 | 发送端sendto返回成功,接收端没反应 | 防火墙拦截了广播包 | 检查防火墙规则,放行对应端口 |
| 只能收到自己发的 | 同一台机器收发没问题,其他机器收不到 | 没有设置SO_BROADCAST | 加上setsockopt设置 |
| 跨网段收不到 | 不同子网的机器收不到广播 | 路由器不转发广播 | 改用组播或单播 |
| 端口冲突 | bind失败,提示Address already in use | 多个程序绑定了同一端口 | 使用SO_REUSEADDR选项 |
25.7 什么时候用广播?
广播不是万能的,它有明显的优缺点:
- 优点:简单高效,一条消息覆盖整个子网
- 缺点:所有机器都得处理,浪费CPU;不能跨网段;容易被滥用导致网络风暴
我个人建议在以下场景使用广播:
- 设备发现:新设备上线时广播宣告自己的存在
- 服务宣告:服务器定期广播自己的服务信息
- 网络唤醒:发送魔术包唤醒休眠的设备
- 时间同步:局域网内广播当前时间
如果设备数量超过几十台,或者需要跨网段通信,我建议改用组播(UDP Multicast)。组播更优雅,也更可控。
好了,UDP广播的核心内容就这些。说白了就是三步:开权限、填广播地址、发数据。接收端更简单,bind之后等着收就行。动手写个demo试试,你很快就能掌握。