C语言组播的使用

目录

一、组播的初始化和关闭

1、multicast_util.h

2、multicast_util.c

使用示例:

3、绑定本地地址时的区别

二、相关函数和介绍

1、setsockopt函数介绍

a、setsockopt函数原型

b、setsockopt函数参数介绍

[SOL_SOCKET 层(通用套接字选项)](#SOL_SOCKET 层(通用套接字选项))

[IPPROTO_IP 层(IPv4 选项)](#IPPROTO_IP 层(IPv4 选项))

[IPPROTO_TCP 层(TCP 选项)](#IPPROTO_TCP 层(TCP 选项))

[2、inet_ntop 函数和 inet_pton函数详解](#2、inet_ntop 函数和 inet_pton函数详解)

a、函数原型

参数说明:

返回值:

b、使用示例


一、组播的初始化和关闭

1、multicast_util.h

cpp 复制代码
#ifndef MULTICAST_UTIL_H
#define MULTICAST_UTIL_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
 * @brief 初始化 UDP 组播套接字
 * @param group_ip   组播地址字符串,如 "227.0.80.44"
 * @param port       本地绑定端口
 * @param iface      网络接口名,如 "eth0"。若为 NULL 则使用 INADDR_ANY
 * @param timeout_sec 接收超时秒数,0 表示不设置超时
 * @param buffer_size 接收缓冲区大小(字节),0 表示使用系统默认值
 * @return 成功返回套接字描述符,失败返回 -1
 */
int multicast_init(const char *group_ip, uint16_t port,
                   const char *iface, int timeout_sec,
                   int buffer_size);

/**
 * @brief 关闭组播套接字并离开组播组(可选,直接 close 也可)
 * @param sock 套接字描述符
 * @param group_ip 组播地址(用于离开组播组,可传入 NULL,但最好提供)
 */
void multicast_close(int sock, const char *group_ip);

#ifdef __cplusplus
}
#endif

#endif // MULTICAST_UTIL_H

2、multicast_util.c

cpp 复制代码
#include "multicast_util.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <sys/ioctl.h>

/**
 * @brief 根据网络接口名获取 IPv4 地址
 * @param ifname 接口名,如 "eth0"
 * @param ip_buf 输出缓冲区,至少 INET_ADDRSTRLEN 大小
 * @return 0 成功,-1 失败
 */
static int get_iface_ip(const char *ifname, char *ip_buf)
{
    int fd;
    struct ifreq ifr;
    struct sockaddr_in *sin;

    if (!ifname || !ip_buf)
        return -1;

    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0)
        return -1;

    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
    ifr.ifr_name[IFNAMSIZ - 1] = '\0';

    if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) {
        close(fd);
        return -1;
    }

    sin = (struct sockaddr_in *)&ifr.ifr_addr;
    inet_ntop(AF_INET, &sin->sin_addr, ip_buf, INET_ADDRSTRLEN);
    close(fd);
    return 0;
}

int multicast_init(const char *group_ip, uint16_t port,
                   const char *iface, int timeout_sec,
                   int buffer_size)
{
    int sock;
    struct sockaddr_in local_addr;  //本地ip信息
    struct ip_mreq mreq;            //组播信息
    int reuse = 1;
    struct timeval tv;
    int ret;

    if (!group_ip)
        return -1;

    // 初始化 UDP 套接字
    sock = socket(AF_INET, SOCK_DGRAM, 0);  
    if (sock < 0) {
        perror("socket");
        return -1;
    }

    // 允许地址重用
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt SO_REUSEADDR");
        close(sock);
        return -1;
    }

    // 绑定本地端口
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;  //指定地址族为 IPv4。
    /*套接字将绑定到本机所有可用的网络接口 一般建议绑定到INADDR_ANY,然后通过IP_ADD_MEMBERSHIP指定接口,这样任何到达该端口的组播数据(无论哪个接口)都会被接收*/
    local_addr.sin_addr.s_addr = htonl(INADDR_ANY); 
    local_addr.sin_port = htons(port);

    if (bind(sock, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
        perror("bind");
        close(sock);
        return -1;
    }

    // 准备加入组播组
    memset(&mreq, 0, sizeof(mreq));
    mreq.imr_multiaddr.s_addr = inet_addr(group_ip);

    if (iface && iface[0] != '\0') {
        // 获取指定接口的 IP 地址
        char ip_str[INET_ADDRSTRLEN];
        if (get_iface_ip(iface, ip_str) == 0) {
            inet_pton(AF_INET, ip_str, &mreq.imr_interface.s_addr);
        } else {
            // 获取失败,回退到 INADDR_ANY
            mreq.imr_interface.s_addr = htonl(INADDR_ANY);
            fprintf(stderr, "Warning: failed to get IP of interface %s, using INADDR_ANY\n", iface);
        }
    } else {
        // 未指定接口,使用默认
        mreq.imr_interface.s_addr = htonl(INADDR_ANY);
    }

    // 加入组播组
    if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
        perror("setsockopt IP_ADD_MEMBERSHIP");
        // 尝试用 INADDR_ANY 重试一次
        mreq.imr_interface.s_addr = htonl(INADDR_ANY);
        if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
            perror("setsockopt IP_ADD_MEMBERSHIP (second attempt)");
            close(sock);
            return -1;
        }
    }

    // 设置接收超时(如果需要)
    if (timeout_sec > 0) {
        tv.tv_sec = timeout_sec;
        tv.tv_usec = 0;
        if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
            perror("setsockopt SO_RCVTIMEO");
            // 超时非必须,继续
        }
    }

    // 设置接收缓冲区大小(如果需要)
    if (buffer_size > 0) {
        if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size)) < 0) {
            perror("setsockopt SO_RCVBUF");
            // 缓冲区非必须,继续
        }
    }

    return sock;
}

void multicast_close(int sock, const char *group_ip)
{
    if (sock < 0)
        return;

    if (group_ip) {
        struct ip_mreq mreq;
        memset(&mreq, 0, sizeof(mreq));
        mreq.imr_multiaddr.s_addr = inet_addr(group_ip);
        mreq.imr_interface.s_addr = htonl(INADDR_ANY);
        // 忽略错误,因为套接字即将关闭
        (void)setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
    }

    close(sock);
}

使用示例:

cpp 复制代码
#include "multicast_util.h"
#include <stdio.h>
#include <unistd.h>

int main()
{
    //组播初始化 指定端口38888,指定加入的网卡为:eth0, 接收5s超时,缓存区大小设置 4M
    int sock = multicast_init("227.0.85.33", 38888, "eth0", 5, 4*1024*1024);
    if (sock < 0) {
        fprintf(stderr, "Failed to init multicast\n");
        return 1;
    }

    char buf[4096];
    struct sockaddr_in src;
    socklen_t srclen = sizeof(src);
    while(1)
    { 
        ssize_t n = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&src, &srclen);
        if (n > 0) {
            printf("Received %zd bytes from %s:%d\n", n, inet_ntoa(src.sin_addr), ntohs(src.sin_port));
            //这里进行具体的数据操作或者传入队列等》》》》》》

    
        }
    }

    multicast_close(sock, "227.0.81.22");
    return 0;
}

以上组播初始化中,INADDR_ANY 表示套接字绑定到所有可用的本地网络接口(即监听或接收来自任何网络接口的数据,对于UDP,这意味着它可以接收发送到该端口的数据包,无论数据包到达哪个本地IP地址(即任意源IP都可以发送数据到这个端口,但目的IP必须是本机的某个IP)。因此,它并不是"允许任意IP连接",而是"允许通过任意本地网络接口接收数据"。

之后,程序会查找本地网卡"eth0"的网卡信息,如果eth0网卡存在符合的ip配置信息,则程序会将该ip信息加入组播,如果获取失败则使用任意ip信息。

如果你想固定直接绑定本地IP地址可以进行如下修改:

cpp 复制代码
// 创建 UDP 套接字
.....

// 允许地址重用
.....

//先获取本地IP信息
char eth0_ip[INET_ADDRSTRLEN];
if (get_iface_ip("eth0", eth0_ip) != 0) {
    fprintf(stderr, "Failed to get IP of eth0\n");
    return -1;
}

//绑定本地地址到eth0的对应IP
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
//local_addr.sin_addr.s_addr = htonl(INADDR_ANY); //修改为如下
local_addr.sin_addr.s_addr = inet_addr(eth0_ip);  // 使用具体 IP
local_addr.sin_port = htons(port);
if (bind(sock, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
    perror("bind");
    close(sock);
    return -1;
}

//加入组播时直接使用eth0的IP
struct ip_mreq mreq;
memset(&mreq, 0, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr("227.0.81.22");
inet_pton(AF_INET, eth0_ip, &mreq.imr_interface.s_addr);
//或直接赋值
//mreq.imr_interface.s_addr = inet_addr(eth0_ip);

if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
    perror("IP_ADD_MEMBERSHIP");
    close(sock);
    return -1;
}

// 设置接收超时(如果需要)
......

// 设置接收缓冲区大小(如果需要)
......

3、绑定本地地址时的区别

绑定本地地址时 ,可以选择绑定到 INADDR_ANY(通配所有接口)或绑定到 eth0 的特定 IP。两者都能工作,但区别在于:

1、绑定到 INADDR_ANY:套接字可以接收发往本机任何 IP 地址(端口匹配)的 UDP 数据,包括组播和单播。组播数据只会从 eth0 加入的组播组到达。

2、绑定到 eth0 的特定 IP:套接字只接收目标 IP 为该特定 IP 的数据,组播数据仍需通过加入组播组才能接收。这通常用于限制只接收来自该接口的流量。

二、相关函数和介绍

1、setsockopt函数介绍

a、setsockopt函数原型

cpp 复制代码
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

b、setsockopt函数参数介绍

sockfd 套接字描述符,由 socket() 返回
level 选项所属的协议层,决定 optname 的解释范围。常见值: - SOL_SOCKET:通用套接字层 - IPPROTO_IP:IPv4 层 - IPPROTO_IPV6:IPv6 层 - IPPROTO_TCP:TCP 层 详细值见表:《常见level及optname》
optname 具体的选项名称,依赖于 level 详细值见表:《常见level及optname》
optval 指向存放选项值的缓冲区。选项的数据类型取决于选项(可以是整数、结构体等)
optlen optval 指向的数据长度(字节数)。通常用 sizeof(类型) 获得。
[setsockopt函数参数介绍]

|----------------------|------------------|----------------------------------------------------------------------|---------------------|
| #### SOL_SOCKET 层(通用套接字选项) ||||
| SO_REUSEADDR | int | 允许重用本地地址和端口,避免 bind 失败(如 TIME_WAIT 状态)。 | 服务器快速重启、多实例绑定相同端口 |
| SO_REUSEPORT | int | 允许多个套接字绑定到同一端口,内核负载均衡分发数据。 | 多进程/多线程服务器 |
| SO_RCVBUF | int | 设置接收缓冲区大小(字节)。 | 提高接收能力,防止丢包 |
| SO_SNDBUF | int | 设置发送缓冲区大小。 | 优化发送性能 |
| SO_RCVTIMEO | struct timeval | 设置接收超时时间,recv/recvfrom 超时返回 -1,errnoEAGAIN/EWOULDBLOCK。 | 避免无限阻塞 |
| SO_SNDTIMEO | struct timeval | 设置发送超时时间。 | 避免发送无限阻塞 |
| SO_BROADCAST | int | 允许发送广播消息(需先设置)。 | UDP 广播通信 |
| SO_KEEPALIVE | int | 开启 TCP 保活探测,检测连接是否存活。 | TCP 长连接 |
| SO_LINGER | struct linger | 控制 close 关闭时如何处理未发送数据。 | 确保数据发送完成或立即关闭 |
| SO_DEBUG | int | 开启调试信息(需内核支持)。 | 开发调试 |
| #### IPPROTO_IP 层(IPv4 选项) ||||
| IP_ADD_MEMBERSHIP | struct ip_mreq | 加入组播组,指定组播地址和本地接口。 | 接收组播数据 |
| IP_DROP_MEMBERSHIP | struct ip_mreq | 离开组播组。 | 停止接收组播数据 |
| IP_MULTICAST_IF | struct in_addr | 设置组播数据发送的默认网络接口。 | 指定从哪个网卡发送组播 |
| IP_MULTICAST_TTL | int | 设置组播数据包的生存时间(TTL)。默认 1,限制在本地子网。 | 控制组播传播范围 |
| IP_MULTICAST_LOOP | int | 是否允许组播回环(本机发送的组播是否被本机接收)。0 禁止,1 允许。 | 避免收到自己发送的组播 |
| IP_TTL | int | 设置单播数据包的 TTL。 | 控制 IP 包跳数 |
| #### IPPROTO_TCP 层(TCP 选项) ||||
| TCP_NODELAY | int | 禁用 Nagle 算法,立即发送小数据包,减少延迟。 | 实时性要求高的应用(如游戏、远程控制) |
| TCP_MAXSEG | int | 获取或设置 TCP 最大分段大小(MSS)。 | 优化传输性能 |
| TCP_KEEPIDLE | int | 保活探测开始前的空闲时间(秒)。 | 调整 TCP 保活参数 |
| TCP_KEEPINTVL | int | 保活探测间隔(秒)。 | 调整 TCP 保活参数 |
| TCP_KEEPCNT | int | 保活探测失败前最大尝试次数。 | 调整 TCP 保活参数 |
[《常见level及optname》]

2、inet_ntop 函数和 inet_pton函数详解

a、函数原型

cpp 复制代码
#include <arpa/inet.h>

// 将二进制地址转换为字符串(网络地址到表示形式)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

// 将字符串转换为二进制地址(表示形式到网络地址)
int inet_pton(int af, const char *src, void *dst);
参数说明:

af :地址族,可选 AF_INET(IPv4)或 AF_INET6(IPv6)。

src :对于 inet_ntop,是指向存储二进制地址的缓冲区(例如 struct in_addrstruct in6_addr);对于 inet_pton,是指向要转换的字符串。

dst :对于 inet_ntop,是存放转换后字符串的缓冲区;对于 inet_pton,是存放转换后二进制地址的缓冲区。

size :仅用于 inet_ntop,指定 dst 缓冲区的大小(字节数),以避免缓冲区溢出。通常使用 INET_ADDRSTRLEN(IPv4)或 INET6_ADDRSTRLEN(IPv6)定义的大小。

返回值:

inet_ntop

成功:返回指向 dst 的指针。

失败:返回 NULL 并设置 errno。常见错误:

EAFNOSUPPORTaf 不是有效的地址族。

ENOSPCsize 指定的缓冲区太小。

inet_pton

成功:返回 1(地址有效且转换成功)。

失败:若输入字符串不是有效的地址格式,返回 0(不设置 errno)。

错误:返回 -1 并设置 errno,通常是因为 af 不支持。

b、使用示例

IPV4转换

cpp 复制代码
#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in_addr addr4;          // 存储 IPv4 二进制地址
    char str[INET_ADDRSTRLEN];      // 足够大的缓冲区

    // 1. 将点分十进制字符串转换为二进制
    if (inet_pton(AF_INET, "192.168.1.1", &addr4) == 1) {
        printf("inet_pton success. Binary: 0x%08x\n", ntohl(addr4.s_addr));
    } else {
        printf("Invalid IPv4 address\n");
        return 1;
    }

    // 2. 将二进制转换回字符串
    if (inet_ntop(AF_INET, &addr4, str, sizeof(str)) != NULL) {
        printf("inet_ntop success. String: %s\n", str);
    } else {
        perror("inet_ntop");
        return 1;
    }

    return 0;
}

输出:

inet_pton success. Binary: 0xc0a80101

inet_ntop success. String: 192.168.1.1

IPV6转换

cpp 复制代码
#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in6_addr addr6;
    char str[INET6_ADDRSTRLEN];

    // 1. 将 IPv6 字符串转换为二进制
    const char *ipv6_str = "2001:db8::1";
    if (inet_pton(AF_INET6, ipv6_str, &addr6) == 1) {
        printf("inet_pton success (IPv6)\n");
    } else {
        printf("Invalid IPv6 address\n");
        return 1;
    }

    // 2. 二进制转回字符串
    if (inet_ntop(AF_INET6, &addr6, str, sizeof(str)) != NULL) {
        printf("inet_ntop success: %s\n", str);
    } else {
        perror("inet_ntop");
        return 1;
    }

    return 0;
}

输出:

inet_pton success (IPv6)

inet_ntop success: 2001:db8::1

注意:

在 Windows 中,需要包含 ws2tcpip.h 并链接 Ws2_32.lib,且函数名相同,但可能需先调用 WSAStartup

在 POSIX 系统(Linux、macOS、BSD)中,直接包含 <arpa/inet.h> 即可

相关推荐
胖祥2 小时前
onnx之NodeComputeInfo
开发语言·c++·算法
MoonBit月兔2 小时前
报名仅剩 3 天|MoonBit 软件合成挑战赛已有数十个项目参赛!
开发语言·人工智能·编程·moonbit
sinat_255487812 小时前
为 System.out 编写我们自己的包装类
java·开发语言·算法
蓝天星空2 小时前
跨平台开发语言对比
开发语言·c#·.net
gihigo19982 小时前
距离角度解耦法的MIMO-OFDM雷达波束形成及优化MATLAB实现
开发语言·算法·matlab
独自破碎E2 小时前
【面试真题拆解】Java锁机制:synchronized、ReentrantLock、锁升级、可重入锁
java·开发语言·面试
努力往上爬de蜗牛2 小时前
extends
java·开发语言
2401_853576502 小时前
代码自动生成框架
开发语言·c++·算法
牢七2 小时前
PHP Debug配置记录
开发语言·php