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), // 发送方IP
ntohs(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.255
bcast_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) |
| 适用场景 | 点对点通信 | 局域网服务发现 | 视频会议、流媒体 |

被折叠的 条评论
为什么被折叠?



