Windows环境下C++如何读取IPv6组播UDP通信的IP头部及第二层数据
Windows IPv6组播原始套接字获取IP头/二层数据修改方案
问题根因
- Windows下
IPPROTO_RAW类型的原始套接字默认仅支持发送,不具备接收能力,直接替换协议类型自然收不到数据 - 原有代码中
getaddrinfo的hints配置与socket创建逻辑不匹配:hints指定了SOCK_DGRAM/IPPROTO_UDP,后续却创建SOCK_RAW类型套接字,地址信息适配错误导致绑定无效 - 所有Windows原始套接字操作必须以管理员权限运行程序,否则所有接收操作都会静默失败
- 三层原始套接字(AF_INET6域下的SOCK_RAW)仅能获取IPv6头部及上层数据,无法拿到包含MAC地址的二层以太网帧,要获取二层数据需要在链路层创建套接字
修改方案
分两种场景实现:
场景1:仅需获取IPv6头部+UDP头部+负载(不需要二层数据)
直接修改原始套接字配置即可,修改点如下:
- 调整
getaddrinfo的hints参数,匹配原始套接字属性 - 新增
IPV6_HDRINCL套接字选项设置,开启接收完整IPv6头部能力 - 接收数据时按「IPv6头->UDP头->负载」的结构解析即可
修改后核心代码:
#include <winsock2.h> #include <windows.h> #include <Ws2tcpip.h> #include <mswsock.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #define MAX_PACKET 65535 int my_raw_socket() { WSADATA wsd; SOCKET sock; ADDRINFO* ressrc = NULL; ADDRINFO hints = { 0 }; ADDRINFO* multicastAddr; char* multicastIP; char* multicastPort; char c_IP[] = "ff02::1"; char c_Port[] = "56333"; multicastIP = c_IP; multicastPort = c_Port; WSAStartup(MAKEWORD(2, 2), &wsd); memset(&hints, 0, sizeof(hints)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_INET6; // 固定IPv6,避免AF_UNSPEC返回IPv4地址 hints.ai_socktype = SOCK_RAW; // 原始套接字 hints.ai_protocol = IPPROTO_IPV6; // 接收IPv6协议栈的完整报文 if (getaddrinfo(NULL, multicastPort, &hints, &ressrc) != 0) { printf("getaddrinfo失败: %d\n", WSAGetLastError()); return -1; } sock = socket(ressrc->ai_family, ressrc->ai_socktype, ressrc->ai_protocol); if (sock == INVALID_SOCKET) { printf("socket创建失败: %d\n", WSAGetLastError()); return -1; } // 开启接收IPv6头部选项 BOOL optval = TRUE; if (setsockopt(sock, IPPROTO_IPV6, IPV6_HDRINCL, (char*)&optval, sizeof(optval)) != 0) { printf("设置IPV6_HDRINCL失败: %d\n", WSAGetLastError()); return -1; } if (bind(sock, ressrc->ai_addr, ressrc->ai_addrlen) != 0) { printf("绑定失败: %d\n", WSAGetLastError()); return -1; } /* 解析组播组地址 */ memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET6; hints.ai_flags = AI_NUMERICHOST; if (getaddrinfo(multicastIP, NULL, &hints, &multicastAddr) != 0) { printf("组播地址解析失败: %d\n", WSAGetLastError()); return -1; } struct ipv6_mreq multicastRequest; memcpy(&multicastRequest.ipv6mr_multiaddr, &((struct sockaddr_in6*)(multicastAddr->ai_addr))->sin6_addr, sizeof(multicastRequest.ipv6mr_multiaddr)); // 注意:ipv6mr_interface不要写死0,要填实际接收组播的网卡索引,可通过GetAdaptersAddresses接口获取 multicastRequest.ipv6mr_interface = 0; if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (char*)&multicastRequest, sizeof(multicastRequest)) != 0) { printf("加入组播失败: %d\n", WSAGetLastError()); return -1; } freeaddrinfo(ressrc); freeaddrinfo(multicastAddr); // 测试阶段可以先关闭非阻塞模式,确认能收到包再开启 // u_long nonblocking_enabled = TRUE; // ioctlsocket(sock, FIONBIO, &nonblocking_enabled); SOCKADDR_STORAGE safrom; char buf[MAX_PACKET]; int fromlen; int recvlen; while (1) { fromlen = sizeof(safrom); recvlen = recvfrom(sock, buf, MAX_PACKET, 0, (SOCKADDR*)&safrom, &fromlen); if (recvlen < 0) { int err = WSAGetLastError(); if (err != WSAEWOULDBLOCK) { printf("接收失败: %d\n", err); } Sleep(10); continue; } else { // buf开头就是40字节的IPv6头部,后面接8字节UDP头部,再后面是负载 printf("成功接收%d字节数据\n", recvlen); } } closesocket(sock); WSACleanup(); return 0; }
场景2:需要获取包含MAC地址的二层以太网帧
Windows 10 1703及以上版本支持原生AF_PACKET链路层套接字,低版本可以用Npcap/WinPcap库实现,核心修改点:
- 创建套接字时指定
AF_PACKET域,SOCK_RAW类型,协议为htons(0x86DD)(对应IPv6帧) - 绑定到指定网卡的链路层地址
- 组播加入逻辑保持不变,确保网卡能收到目标组播流
- 接收的报文结构为「14字节以太网头(6字节目的MAC+6字节源MAC+2字节类型)-> IPv6头-> UDP头-> 负载」
注意事项
- 所有原始套接字程序必须右键选择「以管理员身份运行」,否则会静默收不到包
- 组播加入时的接口索引要和实际接收组播的网卡对应,不要写死0,避免多网卡场景下收不到数据
- 非阻塞模式下
recvfrom返回WSAEWOULDBLOCK属于正常无数据状态,不需要做错误处理
内容的提问来源于stack exchange,提问作者solid
相关产品推荐
相关产品推荐

