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

嗯,这里要注意:广播地址只在当前子网内有效。路由器默认不会转发广播包,所以别指望它能跨网段传播。

核心要点:广播地址 = 子网网络号 + 全1的主机号。

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,就是关闭。

我曾经踩过这个坑:有一次写设备发现程序,忘了设置SO_BROADCAST,结果sendto一直返回成功,但其他机器死活收不到数据。查了半天才发现是内核把广播包给拦截了。从那以后,我每次写UDP广播,第一件事就是检查setsockopt有没有调用。

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,意思是绑定到所有网络接口。这样不管广播从哪个网卡进来,都能收到。

关键区别:发送端需要setsockopt设置SO_BROADCAST,接收端不需要。接收端只需要bind到对应端口即可。

25.5 广播通信的完整流程

我把整个流程画了张图,方便你理解:

发送端 1. socket() 创建UDP套接字 2. setsockopt(SO_BROADCAST) 3. sendto(广播地址:端口) 广播数据包 目标: 192.168.1.255:8888 接收端1 接收端2 接收端3 bind(端口8888) recvfrom() 接收

从图上可以看得很清楚:发送端需要三步,接收端只需要两步。广播数据包会同时到达所有接收端。

25.6 避坑指南

我做了这么多年网络编程,在广播上栽过的跟头可不少。给你列几个最常见的:

问题 现象 原因 解决办法
收不到广播 发送端sendto返回成功,接收端没反应 防火墙拦截了广播包 检查防火墙规则,放行对应端口
只能收到自己发的 同一台机器收发没问题,其他机器收不到 没有设置SO_BROADCAST 加上setsockopt设置
跨网段收不到 不同子网的机器收不到广播 路由器不转发广播 改用组播或单播
端口冲突 bind失败,提示Address already in use 多个程序绑定了同一端口 使用SO_REUSEADDR选项
我曾经遇到过最诡异的问题:在Windows上写的广播程序,拿到Linux上一跑,死活收不到数据。查了半天发现是Windows默认允许接收广播,而Linux需要显式设置SO_BROADCAST。嗯,跨平台开发时一定要注意这个差异。

25.7 什么时候用广播?

广播不是万能的,它有明显的优缺点:

  • 优点:简单高效,一条消息覆盖整个子网
  • 缺点:所有机器都得处理,浪费CPU;不能跨网段;容易被滥用导致网络风暴

我个人建议在以下场景使用广播:

  1. 设备发现:新设备上线时广播宣告自己的存在
  2. 服务宣告:服务器定期广播自己的服务信息
  3. 网络唤醒:发送魔术包唤醒休眠的设备
  4. 时间同步:局域网内广播当前时间

如果设备数量超过几十台,或者需要跨网段通信,我建议改用组播(UDP Multicast)。组播更优雅,也更可控。

一个小经验:广播发送频率不要太高。我一般控制在每秒不超过1次。如果频率太高,网络负载会明显上升,尤其是在设备多的网络里。

好了,UDP广播的核心内容就这些。说白了就是三步:开权限、填广播地址、发数据。接收端更简单,bind之后等着收就行。动手写个demo试试,你很快就能掌握。


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