网络编程中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),     // 发送方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

常见接口名:

  • eth0eth1 - 以太网接口

  • wlan0wlp2s0 - 无线接口

  • ens33ens34 - 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.255D类 (224.x.x.x)
适用场景点对点通信局域网服务发现视频会议、流媒体
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值