C语言组播的使用

目录

一、组播的初始化和关闭

        1、multicast_util.h

        2、multicast_util.c

        使用示例:

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

二、相关函数和介绍

         1、setsockopt函数介绍

                a、setsockopt函数原型

                 b、setsockopt函数参数介绍

 SOL_SOCKET 层(通用套接字选项)

 IPPROTO_IP 层(IPv4 选项)

IPPROTO_TCP 层(TCP 选项)

        2、inet_ntop 函数和 inet_pton函数详解 

                a、函数原型

        参数说明:

        返回值:

                b、使用示例


一、组播的初始化和关闭

        1、multicast_util.h

#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

#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);
}

        使用示例:

#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地址可以进行如下修改:

// 创建 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函数原型

#include <sys/socket.h>

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

                 b、setsockopt函数参数介绍

setsockopt函数参数介绍
sockfd套接字描述符,由 socket() 返回
level选项所属的协议层,决定 optname 的解释范围。常见值:
SOL_SOCKET:通用套接字层
IPPROTO_IP:IPv4 层
IPPROTO_IPV6:IPv6 层
IPPROTO_TCP:TCP 层

详细值见表:《常见level及optname》

optname具体的选项名称,依赖于 level详细值见表:《常见level及optname》
optval指向存放选项值的缓冲区。选项的数据类型取决于选项(可以是整数、结构体等)
optlenoptval 指向的数据长度(字节数)。通常用 sizeof(类型) 获得。
《常见level及optname》

 SOL_SOCKET 层(通用套接字选项)

选项名数据类型作用使用场景
SO_REUSEADDRint允许重用本地地址和端口,避免 bind 失败(如 TIME_WAIT 状态)。服务器快速重启、多实例绑定相同端口
SO_REUSEPORTint允许多个套接字绑定到同一端口,内核负载均衡分发数据。多进程/多线程服务器
SO_RCVBUFint设置接收缓冲区大小(字节)。提高接收能力,防止丢包
SO_SNDBUFint设置发送缓冲区大小。优化发送性能
SO_RCVTIMEOstruct timeval设置接收超时时间,recv/recvfrom 超时返回 -1,errno 为 EAGAIN/EWOULDBLOCK避免无限阻塞
SO_SNDTIMEOstruct timeval设置发送超时时间。避免发送无限阻塞
SO_BROADCASTint允许发送广播消息(需先设置)。UDP 广播通信
SO_KEEPALIVEint开启 TCP 保活探测,检测连接是否存活。TCP 长连接
SO_LINGERstruct linger控制 close 关闭时如何处理未发送数据。确保数据发送完成或立即关闭
SO_DEBUGint开启调试信息(需内核支持)。开发调试

 IPPROTO_IP 层(IPv4 选项)

选项名数据类型作用使用场景
IP_ADD_MEMBERSHIPstruct ip_mreq加入组播组,指定组播地址和本地接口。接收组播数据
IP_DROP_MEMBERSHIPstruct ip_mreq离开组播组。停止接收组播数据
IP_MULTICAST_IFstruct in_addr设置组播数据发送的默认网络接口。指定从哪个网卡发送组播
IP_MULTICAST_TTLint设置组播数据包的生存时间(TTL)。默认 1,限制在本地子网。控制组播传播范围
IP_MULTICAST_LOOPint是否允许组播回环(本机发送的组播是否被本机接收)。0 禁止,1 允许。避免收到自己发送的组播
IP_TTLint设置单播数据包的 TTL。控制 IP 包跳数

IPPROTO_TCP 层(TCP 选项)

选项名数据类型作用使用场景
TCP_NODELAYint禁用 Nagle 算法,立即发送小数据包,减少延迟。实时性要求高的应用(如游戏、远程控制)
TCP_MAXSEGint获取或设置 TCP 最大分段大小(MSS)。优化传输性能
TCP_KEEPIDLEint保活探测开始前的空闲时间(秒)。调整 TCP 保活参数
TCP_KEEPINTVLint保活探测间隔(秒)。调整 TCP 保活参数
TCP_KEEPCNTint保活探测失败前最大尝试次数。调整 TCP 保活参数

        2、inet_ntop 函数和 inet_pton函数详解 

                a、函数原型

#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_addr 或 struct 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转换

#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转换

#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> 即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值