网络编程中UDP协议的广播和组播通信
1) 广播通讯 broadcast
原则: UDP广播通讯需要借助特殊IP(广播地址)
广播地址:
1. 直接广播地址:
主机ID各个二进制位均为1.
xxx.xxx.xxx.255
2. 本地广播地址
网络ID,主机ID各个二进制位均为1.
255.255.255.255
发送端的实现流程:
1. 创建数据报套接字
2. 设置套接字广播属性
setsockopt
头文件: #include <sys/types.h>
#include <sys/socket.h>
函数原型: int setsockopt(int sockfd,int level,int optname,
const void* optval, int optlen);
函数功能: 设置套接字属性
函数参数:
sockfd: 套接字描述符
level: 指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.
optname: 属性名
optval:指向属性值的指针
optlen:属性值长度
函数返回值: 成功返回0;
失败返回-1,错误码放在errno中
3. 设置广播地址与广播端口
4. 向广播地址与端口发送网络数据
5. 接收回复的信息
6. 关闭套接字
接收端的实现流程:
1. 创建数据报套接字
2. 绑定任一地址(INADDR_ANY)和广播端口到套接字
3. 接收广播信息
4. 回复数据
5. 关闭套接字
示例:
#include "header.h"int main(int argc, char** argv)
{// 检查命令行参数(只需要端口号)if(argc < 2){fprintf(stderr, "Usage:%s bcastPort\n", argv[0]);return -1;}// 创建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(-1 == sockfd){perror("socket");return -1;}// 设置服务器地址信息 - 监听所有网络接口sin_t any = {AF_INET};any.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口any.sin_port = htons(atoi(argv[1])); // 设置监听端口int len = sizeof(sin_t);// 绑定套接字到指定端口if(-1 == bind(sockfd, (sa_t*)&any, len)){perror("bind");close(sockfd);return -1;} printf("广播服务器启动在端口 %s,等待广播消息...\n", argv[1]);// 主循环:持续处理广播消息while(1){char szbuf[64] = {0}; // 接收缓冲区sin_t peer = {0}; // 客户端地址结构// 接收广播消息(阻塞等待)recvfrom(sockfd, szbuf, sizeof(szbuf)-1, 0, (sa_t*)&peer, &len);// 显示收到的广播消息printf("[%s:%d]广播:%s\n", inet_ntoa(peer.sin_addr), // 发送方IPntohs(peer.sin_port), // 发送方端口szbuf); // 消息内容// 清空缓冲区,准备回复bzero(szbuf, sizeof(szbuf)); // 将缓冲区清零// 获取用户输入的回复printf("请输入回复数据:");fgets(szbuf, sizeof(szbuf), stdin); // 从标准输入读取// 去除换行符szbuf[strcspn(szbuf, "\n")] = 0;// 发送回复给客户端sendto(sockfd, szbuf, strlen(szbuf), 0, (sa_t*)&peer, len);// 检查是否退出if(strncmp(szbuf, "bye", 3) == 0)break;}// 关闭套接字close(sockfd);printf("服务器已关闭\n");return 0;
}
关键特性详解
1. 广播监听设置
any.sin_addr.s_addr = htonl(INADDR_ANY); // 关键!
INADDR_ANY
(0.0.0.0) 表示监听所有网络接口这样可以接收到发送到本机任何IP的广播消息
2. 交互式回复机制
printf("请输入回复数据:");
fgets(szbuf, sizeof(szbuf), stdin); // 从键盘获取输入
服务器可以手动输入回复内容
实现了双向通信
3. 退出机制
if(strncmp(szbuf, "bye", 3) == 0)break;
当输入"bye"时退出循环
提供优雅的退出方式
对应的广播客户端程序
要让这个服务器正常工作,需要一个广播客户端:
#include "header.h"
#include <stdbool.h>int main(int argc, char** argv)
{if(argc < 3){fprintf(stderr, "Usage:%s bcastPort message\n", argv[0]);return -1;}// 创建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd == -1){perror("socket");return -1;}// 启用广播选项int broadcast = 1;if(setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) == -1){perror("setsockopt");close(sockfd);return -1;}// 设置广播地址sin_t bcast_addr = {AF_INET};bcast_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST); // 255.255.255.255bcast_addr.sin_port = htons(atoi(argv[1]));// 发送广播消息if(sendto(sockfd, argv[2], strlen(argv[2]), 0,(sa_t*)&bcast_addr, sizeof(bcast_addr)) == -1){perror("sendto");close(sockfd);return -1;}printf("已发送广播: %s\n", argv[2]);// 接收服务器回复char buffer[64] = {0};sin_t from_addr;socklen_t len = sizeof(from_addr);int n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,(sa_t*)&from_addr, &len);if(n > 0){buffer[n] = 0;printf("服务器回复: %s\n", buffer);}close(sockfd);return 0;
}
程序执行流程
服务器端:
启动服务器↓
绑定到指定端口,监听所有接口↓
等待接收广播消息 ←─── 阻塞↓
收到消息,显示发送方信息↓
等待用户输入回复↓
发送回复给客户端↓
检查是否退出? → 是 → 关闭服务器↓否
继续等待下一条消息
客户端:
启动客户端↓
设置广播选项↓
发送广播消息到255.255.255.255↓
等待服务器回复↓
显示回复内容↓
退出
编译和运行
编译服务器:
gcc -o bcast_server bcast_server.c
编译客户端:
gcc -o bcast_client bcast_client.c
运行示例:
终端1 - 启动服务器:
./bcast_server 8080
终端2 - 发送广播:
./bcast_client 8080 "Hello, Broadcast!"
终端3 - 另一个客户端:
./bcast_client 8080 "Test Message"
广播地址说明
常见的广播地址:
// 受限广播地址(只在本地网络)
INADDR_BROADCAST // 255.255.255.255// 定向广播地址(特定网络)
// 例如网络 192.168.1.0/24 的广播地址是 192.168.1.255
程序改进建议
1. 添加错误检查
// 检查recvfrom返回值
int n = recvfrom(sockfd, szbuf, sizeof(szbuf)-1, 0, (sa_t*)&peer, &len);
if(n == -1) {perror("recvfrom");continue; // 继续处理下一条消息
}
szbuf[n] = 0; // 添加字符串结束符
2. 支持网络广播地址
// 可以支持特定网络的广播
sin_t bcast_addr = {AF_INET};
bcast_addr.sin_addr.s_addr = inet_addr("192.168.1.255"); // 特定网络广播
bcast_addr.sin_port = htons(atoi(argv[1]));
3. 多线程处理
// 为每个客户端创建线程,实现并发处理
void* handle_client(void* arg) {// 处理客户端请求return NULL;
}while(1) {sin_t peer;socklen_t len = sizeof(peer);char buffer[1024];int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sa_t*)&peer, &len);if(n > 0) {pthread_t tid;client_info_t* info = malloc(sizeof(client_info_t));// 设置客户端信息...pthread_create(&tid, NULL, handle_client, info);}
}
广播通信的特点
优点:
一对多通信:一个消息可以发送给多个接收者
网络发现:常用于服务发现协议
简单高效:不需要维护多个连接
缺点:
网络负担:会增加网络流量
安全性:所有主机都能收到消息
可靠性:不保证所有主机都能收到
2) 组播通讯 multicast
原则: UDP组播通讯需要借助特殊IP(组播地址)
组播地址:
1. 永久组播地址 224.0.0.0 ~ 224.0.0.255
2. 公用组播地址 224.0.1.0 ~ 224.0.1.255
3. 临时组播地址 224.0.2.0 ~ 238.255.255.255
4. 本地组播地址 239.0.0.0 ~ 239.255.255.255
组播通讯的实现:
发送端的实现流程:
1. 创建数据报套接字
2. 设置组播地址与组播端口
4. 向组播地址与组播端口发送网络数据
5. 接收回复的信息
6. 关闭套接字
接收端的实现流程:
1. 创建数据报套接字
2. 绑定任一地址与组播端口
3. 将主机地址加入多播组
struct ip_mreqn reqn = {....}
reqn.imr_multiaddr.s_addr = inet_addr("224.0.2.100");
reqn.imr_address.s_addr = inet_addr("192.168.12.200");
reqn.imr_ifindex = if_nametoindex("ens33");
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&reqn,sizeof(reqn));
4. 接收发送到多播组中的信息
5. 回复网络数据
6. 退出多播组
setsockopt(sockfd,IPPROTO_IP,IP_DROP_MEMBERSHIP,&reqn,sizeof(reqn));
7. 关闭套接字
示例:
#include "header.h"int main(int argc, char** argv)
{// 检查命令行参数if(argc < 3){fprintf(stderr, "Usage: %s multiIP multiPort\n", argv[0]);return -1;}/*1.创建数据报套接字*/int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(-1 == sockfd){perror("socket");return -1;}/*2.绑定任一地址和组播端口到套接字上*/sin_t any = {AF_INET};any.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口//inet_pton(AF_INET,"0.0.0.0",&any.sin_addr); // 另一种写法any.sin_port = htons(atoi(argv[2])); // 组播端口socklen_t len = sizeof(sin_t);if(-1 == bind(sockfd, (sa_t*)&any, len)){perror("bind");close(sockfd);return -1;}/*3.加入组播组*/struct ip_mreqn reqn = {0}; // 组播请求结构体// 设置组播组地址 (D类地址: 224.0.0.0 - 239.255.255.255)inet_pton(AF_INET, argv[1], &reqn.imr_multiaddr); // 设置本地接口地址 (指定从哪个网络接口加入组播组)inet_pton(AF_INET, "192.168.255.132", &reqn.imr_address);// 设置网络接口索引 (通过接口名获取)reqn.imr_ifindex = if_nametoindex("ens33");// 加入组播组if(-1 == setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &reqn, sizeof(reqn))){perror("setsockopt");close(sockfd);return -1; }puts("加入组播组成功!");/*4.接收组播数据*/sin_t peer = {0}; char szbuf[64] = {0};// 接收组播数据 (阻塞等待)recvfrom(sockfd, szbuf, sizeof(szbuf)-1, 0, (sa_t*)&peer, &len);// 使用更安全的地址转换函数char peerIP[INET_ADDRSTRLEN];inet_ntop(AF_INET, &peer.sin_addr, peerIP, INET_ADDRSTRLEN);printf("[%s:%d]组播数据:%s\n", peerIP, ntohs(peer.sin_port), szbuf);/*5.回复组播数据*/bzero(szbuf, sizeof(szbuf)); // 清空缓冲区printf("请输入回复数据:");fgets(szbuf, sizeof(szbuf), stdin); // 获取用户输入szbuf[strcspn(szbuf, "\n")] = 0; // 去除换行符// 发送回复 (单播回复给发送者,不是组播)sendto(sockfd, szbuf, strlen(szbuf), 0, (sa_t*)&peer, len);/*6.退出组播组*/if(-1 == setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &reqn, sizeof(reqn))){perror("setsockopt");close(sockfd);return -1; }puts("退出组播组成功!");/*7.关闭套接字*/close(sockfd);return 0;
}
关键概念详解
1. 组播地址范围
// D类IP地址用于组播 (224.0.0.0 - 239.255.255.255)
// 示例组播地址:
// 224.0.0.1 所有主机
// 224.0.0.2 所有路由器
// 239.255.255.250 SSDP协议
2. struct ip_mreqn
结构体
struct ip_mreqn {struct in_addr imr_multiaddr; // 组播组IP地址struct in_addr imr_address; // 本地接口IP地址int imr_ifindex; // 接口索引
};
3. 网络接口操作
// 通过接口名获取接口索引
reqn.imr_ifindex = if_nametoindex("ens33");// 常见的网络接口名:
// "eth0" - Linux以太网接口
// "ens33" - VMware虚拟网卡
// "wlan0" - 无线网卡
// "lo" - 回环接口
对应的组播服务器程序
#include "header.h"int main(int argc, char** argv)
{if(argc < 3){fprintf(stderr, "Usage: %s multiIP multiPort\n", argv[0]);return -1;}// 创建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd == -1){perror("socket");return -1;}// 设置组播TTL (Time To Live)int ttl = 64; // 数据包可以经过的路由器数量if(setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) == -1){perror("setsockopt TTL");close(sockfd);return -1;}// 设置组播地址sin_t multi_addr = {0};multi_addr.sin_family = AF_INET;inet_pton(AF_INET, argv[1], &multi_addr.sin_addr);multi_addr.sin_port = htons(atoi(argv[2]));printf("组播服务器启动,组播地址: %s:%s\n", argv[1], argv[2]);while(1){char message[64] = {0};printf("请输入组播消息: ");fflush(stdout);if(fgets(message, sizeof(message), stdin) == NULL)break;message[strcspn(message, "\n")] = 0;if(strlen(message) == 0)continue;// 发送组播消息if(sendto(sockfd, message, strlen(message), 0,(sa_t*)&multi_addr, sizeof(multi_addr)) == -1){perror("sendto");break;}printf("已发送组播: %s\n", message);// 检查是否退出if(strcmp(message, "quit") == 0)break;}close(sockfd);return 0;
}
程序执行流程
启动客户端↓
创建UDP套接字↓
绑定到INADDR_ANY和指定端口↓
加入组播组 (IP_ADD_MEMBERSHIP)↓
等待接收组播数据 ←─── 阻塞↓
收到组播数据,显示发送者信息↓
输入回复数据↓
单播回复给发送者↓
退出组播组 (IP_DROP_MEMBERSHIP)↓
关闭套接字
编译和运行
编译客户端:
gcc -o multicast_client multicast_client.c
编译服务器:
gcc -o multicast_server multicast_server.c
运行示例:
终端1 - 启动客户端1:
./multicast_client 239.1.2.3 8080
终端2 - 启动客户端2:
./multicast_client 239.1.2.3 8080
终端3 - 启动服务器:
./multicast_server 239.1.2.3 8080
网络接口配置
查看网络接口:
# Linux
ip addr show
ifconfig# 查看接口索引
ip link show
常见接口名:
eth0
,eth1
- 以太网接口wlan0
,wlp2s0
- 无线接口ens33
,ens34
- VMware虚拟接口lo
- 回环接口
改进建议
1. 自动获取本地IP
// 自动获取本地IP地址,而不是硬编码
char local_ip[INET_ADDRSTRLEN] = {0};
get_local_ip(local_ip, sizeof(local_ip));
inet_pton(AF_INET, local_ip, &reqn.imr_address);
2. 错误处理改进
// 检查recvfrom返回值
int n = recvfrom(sockfd, szbuf, sizeof(szbuf)-1, 0, (sa_t*)&peer, &len);
if(n == -1) {perror("recvfrom");// 错误处理
} else {szbuf[n] = 0; // 添加字符串结束符// 处理数据...
}
3. 支持多个网络接口
// 尝试多个可能的接口名
const char* interfaces[] = {"ens33", "eth0", "wlan0", NULL};
for(int i = 0; interfaces[i] != NULL; i++) {reqn.imr_ifindex = if_nametoindex(interfaces[i]);if(reqn.imr_ifindex != 0) {printf("使用接口: %s\n", interfaces[i]);break;}
}
组播 vs 广播 vs 单播
特性 | 单播 (Unicast) | 广播 (Broadcast) | 组播 (Multicast) |
---|---|---|---|
目标主机 | 单个特定主机 | 同一网段所有主机 | 组播组内所有主机 |
网络负载 | 低 | 高 | 中等 |
地址范围 | A/B/C类 | 255.255.255.255 | D类 (224.x.x.x) |
适用场景 | 点对点通信 | 局域网服务发现 | 视频会议、流媒体 |