目录
[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函数详解)
一、组播的初始化和关闭
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,errno 为 EAGAIN/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_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。常见错误:
EAFNOSUPPORT:af 不是有效的地址族。
ENOSPC:size 指定的缓冲区太小。
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> 即可