02-udp-broadcast-programming

本文详细讲解如何在Linux环境下使用UDP协议实现广播通信,包括广播地址概念、核心函数`setsockopt()`的用法、完整的发送端和接收端实现,以及常见问题的解决方案。

本文详细讲解如何在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 选项的关键作用。

✅ 成功实现了广播的发送端与接收端程序。

✅ 了解了桥接模式对局域网通信的重要性。

导航

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
使用 Hugo 构建
主题 StackJimmy 设计