本文详细讲解如何在Linux环境下使用UDP协议实现广播通信,包括广播地址概念、核心函数setsockopt()的用法、完整的发送端和接收端实现,以及常见问题的解决方案。
一、实验目的
掌握UDP广播通信的原理,实现一个向局域网内所有主机发送广播消息的程序,并实现接收广播消息的程序。
二、核心原理/知识点
2.1 广播概述
广播是一种一对所有的通信方式。在局域网内,发送到一个广播地址的数据包,会被该网段内的所有主机接收和处理(如果该主机程序监听在了相应端口)。
2.2 广播地址
受限广播地址:255.255.255.255。该地址不会被路由器转发,仅限本网络内部。
定向广播地址:网络号.主机号全为1,例如,对于 192.168.1.0/24 网段,其广播地址是 192.168.1.255。传统上,路由器可转发此类广播,但现代网络常默认禁止。
三、核心函数详解
广播编程在单播基础上,主要增加了一个关键的系统调用。
setsockopt() - 设置套接字选项
1
2
3
4
|
#include<sys/types.h>
#include<sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
|
作用:用于控制套接字的各种行为。在本实验中,用于启用套接字的广播发送权限。
参数解释:
sockfd:由 socket() 创建的套接字描述符。
level:选项定义的层次。设置为 SOL_SOCKET,表示在套接字API层面操作。
optname:要设置的选项名。启用广播需设置为 SO_BROADCAST。
optval:指向包含新选项值的缓冲区的指针。对于布尔选项,指向一个 int 型变量,1 表示开启。
optlen:optval 缓冲区的大小,例如 sizeof(int)。
返回值:成功返回 0,失败返回 -1 并设置 errno。
头文件:#include <sys/socket.h>
备注:只有发送方需要设置此选项。接收方只需像普通UDP程序一样,绑定到广播地址或 INADDR_ANY 并监听特定端口即可。
四、实现步骤分解
发送端
1.创建UDP套接字
1
|
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
2.启用套接字广播选项 (关键步骤)
1
2
3
4
5
|
int optval = 1; // 1表示“启用”
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)) < 0) {
perror(“Setsockopt SO_BROADCAST failed”);
exit(EXIT_FAILURE);
}
|
3.设置目标广播地址
1
2
3
4
5
6
|
struct sockaddr_in broadcast_addr;
memset(&broadcast_addr, 0, sizeof(broadcast_addr));
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(BROADCAST_PORT); // 定义一个广播端口,如 8888
// 使用定向广播地址,例如 192.168.1.255这个要使用本机的广播地址
inet_pton(AF_INET, “192.168.1.255”, &broadcast_addr.sin_addr);
|
4.循环发送广播消息
1
2
3
4
5
6
7
|
while (1) {
char msg[256];
sprintf(msg, “Broadcast message at %ld”, time(NULL));
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr*)&broadcast_addr, sizeof(broadcast_addr));
sleep(2); // 间隔2秒发送一次
}
|
接收端
1.创建UDP套接字
同发送
2.绑定到任意地址和广播端口
1
2
3
4
5
6
7
|
struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_family = AF_INET;
recv_addr.sin_port = htons(BROADCAST_PORT); // 端口必须与发送端一致
recv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 接收所有网卡的数据
bind(sockfd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
|
3.循环接收广播消息
1
2
3
4
5
6
7
8
9
10
11
12
|
while (1) {
char buf[256];
struct sockaddr_in src_addr;
socklen_t addr_len = sizeof(src_addr);
ssize_t n = recvfrom(sockfd, buf, sizeof(buf)-1,0,(struct sockaddr*)&src_addr, &addr_len);
if (n > 0) {
buf[n] = '\0';
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &src_addr.sin_addr, ip_str, sizeof(ip_str));
printf(“[%s:%d] %s\n”, ip_str, ntohs(src_addr.sin_port), buf);
}
}
|
五、完整代码实现
发送端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BROAD_PORT 8888
#define BROAD_ADDR "192.168.245.255"
#define BUF_LEN 256
int main()
{
int sock;
struct sockaddr_in broad_addr;
char buf[BUF_LEN];
int count = 37;
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket err");
exit(1);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
// 初始化广播地址
memset(&broad_addr, 0, sizeof(broad_addr));
broad_addr.sin_family = AF_INET;
broad_addr.sin_port = htons(BROAD_PORT);
inet_pton(AF_INET, BROAD_ADDR, &broad_addr.sin_addr);
// 循环发送广播
while (1)
{
snprintf(buf, BUF_LEN, "The villagers have noticed that everyone's fund-raising has reached %d yuan.", count);
// 发送广播
sendto(sock, buf, strlen(buf), 0, (struct sockaddr*)&broad_addr, sizeof(broad_addr));
count++;
sleep(1);
}
close(sock);
return 0;
}
|
接收端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BROAD_PORT 8888
#define BUF_LEN 256
int main()
{
int sock;
struct sockaddr_in local_addr;
socklen_t addr_len = sizeof(local_addr);
char buf[BUF_LEN];
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket err");
exit(1);
}
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(BROAD_PORT);
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sock, (struct sockaddr*)&local_addr, sizeof(local_addr));
// 循环接收广播
while (1)
{
memset(buf, 0, BUF_LEN);
recvfrom(sock, buf, BUF_LEN-1, 0, NULL, NULL);
printf("%s\n", buf);
}
close(sock);
return 0;
}
|
六、编译与运行
1
2
3
4
5
6
7
8
9
10
|
# 1. 分别编译发送端和接收端程序
gcc -o broadcast_sender broadcast_sender.c -Wall
gcc -o broadcast_receiver broadcast_receiver.c -Wall
# 2. 运行(需要两台在同一局域网的主机,或一台主机开两个终端模拟)
# 在主机A(或终端1)运行接收端,监听广播:
./broadcast_receiver
# 在主机B(或终端2)运行发送端,开始广播:
./broadcast_sender
|
注意:要先将两个主机ping通如果在一台机器测试,请确保防火墙允许该端口的UDP数据包。
七、遇到的问题与解决
问题一:虚拟机无法接收到广播消息
现象:发送端正常发送,但另一台虚拟机的接收端无任何输出。
原因分析:虚拟机的网络适配器模式可能设置为NAT,该模式通常不支持主机间的广播。需要改为桥接模式,使虚拟机直接接入物理局域网。
解决方案:关闭虚拟机,修改其网络设置,将网络连接从“NAT”改为“桥接模式”,然后重启虚拟机并重新获取IP。
问题二:bind error: Address already in use
现象:接收端程序启动失败。
原因分析:之前运行的程序未完全退出,端口仍被占用。
解决方案:
1
2
3
4
|
# 查找占用端口的进程
sudo netstat -tulnp | grep <端口号>
# 终止该进程
kill -9 <进程PID>
|
八、实验总结与思考
✅ 掌握了UDP广播的通信模型和广播地址的概念。
✅ 理解了 setsockopt() 函数及其 SO_BROADCAST 选项的关键作用。
✅ 成功实现了广播的发送端与接收端程序。
✅ 了解了桥接模式对局域网通信的重要性。
导航: