简介面向Windows平台学习UDP组播多播发送与接收的开发者这份VS工程示例围绕Winsock套接字编程展开可用于搭建局域网多播通信实验环境。资源包共36个文件、约315KB以cpp源码与h头文件为核心配合vcproj工程配置、manifest、res等Visual Studio项目文件并含obj等编译中间产物便于直接打开解决方案对照调试。内容涉及多播地址与端口设置、接收套接字绑定、通过IP_ADD_MEMBERSHIP加入多播组以及sendto发送、recvfrom接收等关键流程并区分发送端与接收端两个示例工程。已有1685人学习下载适合需要理解组播通信机制、验证本机多网卡接口选择、排查收发异常或在此基础上改造成服务发现、消息广播原型的读者参考。1. 一个组播包发出去没人收这份 VS 工程的定位与适用人群两个进程放在同一台机器上跑发送端sendto每轮都返回正数接收端recvfrom一个字节都没等到Wireshark 里也不见包影——这是很多人第一次在 Windows 上调 UDP 组播的状态。组播多播和普通 UDP 单播最核心的差别在“谁能收到”这一层单播点对点组播是把一份数据发到某个组地址凡是加入了这个组、并且绑定了对应端口的网卡都会拿到一份副本。这份 VS 工程把发送端和接收端拆成两个能独立编译运行的控制台程序把组播链路上最容易被忽略的几个开关TTL、回环、出口网卡、地址复用、组成员管理全部显式打开。适合已经会写基础 socket、但被“收不到包”卡住的人也适合要把组播塞进设备发现、行情推送、局域网视频分片这类场景的开发者。2. 工程骨架与技术选型Win32 Socket 下的两条独立链路组播收发本质上是两个方向相反、职责完全不同的程序发送端只关心“包怎么构造、从哪块网卡、以多大跳数发出去”接收端只关心“绑哪个地址、加哪个组、收上来怎么处理”。把这两件事塞进同一个 main 里调试时会互相干扰所以这份工程按“两个可执行文件 一个公共头”来组织。技术栈也刻意压到最低只用 Winsock2 的原始 API不引第三方网络库C17 标准VS 2019 及以上都能打开目的就是让每一行代码的行为都可以对着 MSDN 查。2.1 文件划分发送端、接收端与公共头文件目录结构大致是这样McastDemo/ ├─ McastDemo.sln ├─ common/ │ └─ mcast_common.h 收发共用的组地址、端口、缓冲区常量 ├─ sender/ │ ├─ sender.vcxproj │ └─ main.cpp 发送端组包 sendto 循环 └─ receiver/ ├─ receiver.vcxproj └─ main.cpp 接收端bind 加入组播组 recvfrom 循环拆成两个 exe 的直接好处是调试姿势在 VS 里右键解决方案 → 属性 → 启动项目可以选“多个启动项目”让 sender 和 receiver 同时起来两边各自下断点互不影响。如果做成一个 exe 带--send/--recv参数断点会抢同一个进程切换起来非常别扭。公共头只放常量和一个轻量的日志封装不放任何实现// common/mcast_common.h —— 收发两端唯一的事实来源 #pragma once #include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) // 免去手动配链接器 namespace mcast { constexpr const char* kGroupIp 239.255.0.1; // 组播组地址 constexpr unsigned short kPort 45678; // 收发双方必须严格一致 constexpr int kMaxDatagram 1400; // 单包负载上限避开分片 constexpr int kRecvBufSize 256 * 1024; // 接收缓冲 }kMaxDatagram定在 1400 是个经验值以太网 MTU 1500减去 IPv4 头 20 字节和 UDP 头 8 字节留 1472 的安全上限再扣掉隧道、VLAN 之类可能叠加的头部1400 基本不会触发 IP 分片。组播场景下分片是灾难——只要一个分片丢了整个 UDP 报文都会被丢弃而且接收端重组还要占额外内存。2.2 组播地址与端口的选定规则IPv4 组播地址范围是224.0.0.0到239.255.255.255。这份工程用的是239.255.0.1落在239.0.0.0/8这个管理范围内可以理解为“私有组播段”不会和公网上的标准组播服务撞车。有几个地址不要用224.0.0.1是“本网段所有主机”224.0.0.2是“本网段所有路由器”224.0.0.9是 RIPv2这些属于协议保留地址绑上去容易收到一堆莫名其妙的流量。224.0.0.0/24这一段还有一个特殊性质TTL 恒为 1不管你把IP_MULTICAST_TTL设成多少路由器都不会转发只能在本地链路内玩。想跨网段传播起始地址至少要从239.x或者239.255.x挑。端口选 45678属于动态端口区49152 以上是私有端口但 45678 也不冲突。要避开的坑是不要用 1024 以下的端口Windows 上普通用户没有绑定权限不要用 1900SSDP、5353mDNS这类被系统服务占着的端口否则bind直接返回WSAEADDRINUSE。2.3 VS 工程配置链接 ws2_32 与编译选项工程里已经用#pragma comment(lib, ws2_32.lib)把链接写死省得每个项目手动去配链接器。如果因为某些原因要手动配路径是项目属性 → 链接器 → 输入 → 附加依赖项加上ws2_32.lib。漏掉这一步的典型症状是编译通过、链接报LNK2019: 无法解析的外部符号 __imp__WSAStartup8看到这个错误先查链接器别去查头文件。其余几项建议一起设掉配置项建议值原因语言标准ISO C17constexpr、std::snprintf都能用字符集Unicode 或 多字节均可代码里统一用窄字符 API不受影响平台x6432 位也能跑但 64 位下指针和socklen_t更省心SDL 检查否Demo 不需要头文件包含顺序有一个经典陷阱winsock2.h必须在windows.h之前否则windows.h会把老版本的winsock.h拉进来导致fd_set、timeval重定义。这份工程的做法是公共头里只包含winsock2.h和ws2tcpip.h需要Sleep、GetTickCount64的地方在 .cpp 里单独加windows.h并且放在winsock2.h之后。3. 发送端逐段拆解从 WSAStartup 到 sendto 的完整链路发送端的代码其实只有三步起 Winsock、把 socket 的组播属性设对、进循环发数据。绝大多数“发送端看起来成功了但没人收到”的问题都出在第二步的几个setsockopt上。3.1 启动 Winsock 与创建 UDP 套接字#include ../common/mcast_common.h #include windows.h #include cstdio int main() { WSADATA wsa{}; int rc WSAStartup(MAKEWORD(2, 2), wsa); if (rc ! 0) { std::printf(WSAStartup failed: %d\n, rc); return -1; } // 确认运行时给的是 2.2 版本某些精简系统会降级 if (LOBYTE(wsa.wVersion) ! 2 || HIBYTE(wsa.wVersion) ! 2) { std::printf(WinSock 2.2 not available\n); WSACleanup(); return -1; } SOCKET s ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (s INVALID_SOCKET) { std::printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; } // ... 后续配置 }WSAStartup的MAKEWORD(2, 2)请求的是 Winsock 2.2这是 Windows 上组播 API 的基线版本IP_ADD_MEMBERSHIP、IP_MULTICAST_IF都在这个版本里才有完整支持。wVersion回读这一步很多人省掉但在被裁剪过的系统上确实会遇到只给 1.1 的情况回读一次成本很低。socket()的第三个参数写IPPROTO_UDP而不是 0是为了让代码自身表达意图——虽然传 0 时系统会根据SOCK_DGRAM自动推导成 UDP但写明确之后读代码的人一眼就能看出这是 UDP 而非某种原始套接字。返回值必须和INVALID_SOCKET比较不能用 0因为SOCKET在 Windows 上是UINT_PTR无符号类型。错误码统一用WSAGetLastError()不要用errno。Winsock 的错误码是独立的一套errno在这里基本拿不到有用信息这是个新手高频误区。3.2 四个 setsockopt 决定包能发多远、从哪块网卡出去// 1) TTL1 只在本子网传播跨路由要逐级调大 DWORD ttl 1; if (setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, reinterpret_castconst char*(ttl), sizeof(ttl)) SOCKET_ERROR) { std::printf(set TTL failed: %d\n, WSAGetLastError()); } // 2) 回环本机也收到自己发的包单机自测必须开 BOOL loop TRUE; setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_castconst char*(loop), sizeof(loop)); // 3) 出口网卡多网卡机器不指定就会走默认路由那块 in_addr localIf{}; localIf.s_addr ::inet_addr(192.168.1.100); // 换成实际网卡 IP if (localIf.s_addr ! INADDR_NONE) { setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, reinterpret_castconst char*(localIf), sizeof(localIf)); } // 4) 发送缓冲突发场景下减少阻塞 int sndBuf 64 * 1024; setsockopt(s, SOL_SOCKET, SO_SNDBUF, reinterpret_castconst char*(sndBuf), sizeof(sndBuf));IP_MULTICAST_TTL在 Windows 上的值类型是DWORD这点和 Linux 上常见的unsigned char不一样网上抄来的跨平台代码经常在这里翻车——传一个 1 字节的char进去setsockopt的optlen只有 1系统读到的 TTL 可能是 0包发出去直接被丢弃sendto却照样返回成功。IP_MULTICAST_LOOP控制“本机是否收到自己发的组播包”。单机自测时必须是TRUE否则发送端和接收端在同一台机器上永远对不上但如果是两个进程分别部署在两地又不想让本机重复处理可以关掉它减少一次无用功。IP_MULTICAST_IF是最容易漏掉的一个。机器上有多块网卡有线、无线、虚拟网卡、Docker 的 vEthernet时系统默认走路由表里优先级最高的那块。如果接收端在另一块网卡的网段里包就发到了错误的地方。这里的in_addr填的是本地网卡的 IPv4 地址不是组播地址这点很多人第一遍会写错。第四个SO_SNDBUF属于优化项不是必需。发送频率不高的时候可以删掉但如果是几百赫兹的推送场景默认的 8KB 缓冲会频繁触发阻塞。3.3 组包与发送循环控制包长与发送节奏sockaddr_in dst{}; dst.sin_family AF_INET; dst.sin_port htons(mcast::kPort); dst.sin_addr.s_addr ::inet_addr(mcast::kGroupIp); char payload[mcast::kMaxDatagram]; for (int seq 0; seq 100; seq) { int len std::snprintf(payload, sizeof(payload), SEQ%d;TS%llu;DATAhello-multicast, seq, static_castunsigned long long(::GetTickCount64())); int sent ::sendto(s, payload, len, 0, reinterpret_castsockaddr*(dst), sizeof(dst)); if (sent SOCKET_ERROR) { std::printf(sendto #%d failed: %d\n, seq, WSAGetLastError()); } else if (sent ! len) { // UDP 理论上不会部分发送出现即说明有问题记一笔 std::printf(sendto #%d partial: %d/%d\n, seq, sent, len); } ::Sleep(100); } ::closesocket(s); ::WSACleanup(); return 0;报文格式SEQ...;TS...;DATA...是刻意设计的SEQ给了接收端做丢包统计的依据TS用GetTickCount64毫秒级、64 位不会回绕可以算端到端延迟DATA留作业务负载。真实项目里一般会换成二进制头 负载的形式但调试阶段纯文本更容易在 Wireshark 里直接看懂。sendto的返回值检查分两层SOCKET_ERROR说明这次调用失败WSAGetLastError()会给具体原因WSAENETUNREACH、WSAEHOSTUNREACH最常见返回值小于len说明只发了一部分UDP 理论上不会出现一旦出现基本可以判定是底层驱动或者协议栈出了问题。Sleep(100)是人为限速10 包/秒足够观察。去掉它做压测时接收端的SO_RCVBUF要同步调大否则内核缓冲区一满后面的包会被静默丢弃——UDP 不重传丢了就丢了sendto依然返回成功。如果发送端是单向持续推送可以把sendto换成先connect()到组播地址再send()。好处是省掉每次的地址解析坏处是拿不到“目标不可达”以外的细粒度错误。初次接触组播建议保留sendto排查时信息更全。4. 接收端逐段拆解bind、IP_ADD_MEMBERSHIP 与收包循环接收端的顺序不能乱先设SO_REUSEADDR再bind最后IP_ADD_MEMBERSHIP。这三步的顺序错了症状各不一样后面避坑那一节会展开。4.1 bind 的地址选择为什么不能绑组播地址#include ../common/mcast_common.h #include windows.h #include cstdio int main() { WSADATA wsa{}; if (WSAStartup(MAKEWORD(2, 2), wsa) ! 0) { std::printf(WSAStartup failed\n); return -1; } SOCKET s ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (s INVALID_SOCKET) { std::printf(socket failed: %d\n, WSAGetLastError()); return -1; } // 1) 端口复用必须在 bind 之前设置 BOOL reuse TRUE; ::setsockopt(s, SOL_SOCKET, SO_REUSEADDR, reinterpret_castconst char*(reuse), sizeof(reuse)); // 2) 加大接收缓冲抗突发 int rcvBuf mcast::kRecvBufSize; ::setsockopt(s, SOL_SOCKET, SO_RCVBUF, reinterpret_castconst char*(rcvBuf), sizeof(rcvBuf)); // 3) 绑定到 INADDR_ANY 端口不要绑组播地址 sockaddr_in local{}; local.sin_family AF_INET; local.sin_port htons(mcast::kPort); local.sin_addr.s_addr htonl(INADDR_ANY); if (::bind(s, reinterpret_castsockaddr*(local), sizeof(local)) SOCKET_ERROR) { std::printf(bind failed: %d\n, WSAGetLastError()); ::closesocket(s); ::WSACleanup(); return -1; } // ... 加入组播组 }bind的地址这里写的是INADDR_ANY这是接收组播最稳妥的做法。很多人会把bind的地址直接写成组播地址239.255.0.1结果就是收不到包——Windows 协议栈对“绑定到组播地址”的行为支持并不一致某些版本上能绑成功但收不到任何数据某些版本直接返回WSAEADDRNOTAVAIL。正确的模型是端口 任意地址负责“占坑”加入组播组负责“告诉协议栈这个坑要收哪个组的数据”两件事分开做。SO_REUSEADDR也必须放在bind之前。它的作用是允许多个 socket 绑定同一个端口Windows 上组播场景的语义是“多个进程可以同时监听同一个组播端口”这正是同一台机器上跑多个接收实例的前提。设置晚了第二个进程会直接拿到WSAEADDRINUSE (10048)。SO_RCVBUF设到 256KB 是为了抗突发。默认的 8KB 在高频组播下能扛的量非常有限而 UDP 没有流控缓冲满了就丢且丢得无声无息。4.2 ip_mreq 结构体与加入组播组的正确姿势// 4) 加入组播组 ip_mreq mreq{}; mreq.imr_multiaddr.s_addr ::inet_addr(mcast::kGroupIp); mreq.imr_interface.s_addr htonl(INADDR_ANY); // 或指定某块网卡 IP if (::setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_castconst char*(mreq), sizeof(mreq)) SOCKET_ERROR) { std::printf(IP_ADD_MEMBERSHIP failed: %d\n, WSAGetLastError()); ::closesocket(s); ::WSACleanup(); return -1; }ip_mreq只有两个字段但每个字段都有讲究。imr_multiaddr是要加入的组播组地址就是发送端sendto的目标imr_interface是本地哪块网卡去加入这个组——填INADDR_ANY表示让系统自己挑多网卡机器上系统挑的不一定是你想要的那块所以生产代码一般会显式指定成网卡 IP。显式指定网卡的常见写法是枚举本机所有 IPv4 地址逐个尝试加入// 多网卡场景对每块网卡都加入一次哪块能收到算哪块 ULONG bufLen 15 * 1024; std::vectorchar buf(bufLen); auto* addrs reinterpret_castIP_ADAPTER_ADDRESSES*(buf.data()); if (::GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST, nullptr, addrs, bufLen) NO_ERROR) { for (auto* p addrs; p; p p-Next) { for (auto* ua p-FirstUnicastAddress; ua; ua ua-Next) { auto* sa reinterpret_castsockaddr_in*(ua-Address.lpSockaddr); if (sa-sin_family ! AF_INET) continue; ip_mreq m{}; m.imr_multiaddr.s_addr ::inet_addr(mcast::kGroupIp); m.imr_interface sa-sin_addr; ::setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_castconst char*(m), sizeof(m)); } } }对同一 socket 多次调用IP_ADD_MEMBERSHIP每次传不同的imr_interface协议栈会把这块 socket 挂到多块网卡的组播监听上。缺点是可能收到重复包如果同一个包从多块网卡进来业务层需要用SEQ去重。退出前要对应地调IP_DROP_MEMBERSHIP否则组播组成员关系会残留到进程结束——进程正常退出时系统会清理但如果是常驻服务反复重建 socket不主动 drop 会慢慢累积。GetAdaptersAddresses的bufLen传小了会返回ERROR_BUFFER_OVERFLOW并把实际需要的长度写回bufLen所以稳健的写法是先调一次拿长度再分配。上面为了简洁写成了固定 15KB实际项目里建议按两段式写。4.3 recvfrom 循环与超时退出机制// 5) 设置接收超时避免 recvfrom 永久阻塞导致无法优雅退出 DWORD timeoutMs 3000; ::setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, reinterpret_castconst char*(timeoutMs), sizeof(timeoutMs)); char buf[65535]; while (true) { sockaddr_in from{}; int fromLen sizeof(from); // 每轮都要重置 int n ::recvfrom(s, buf, sizeof(buf) - 1, 0, reinterpret_castsockaddr*(from), fromLen); if (n SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) continue; // 超时只是没数据继续等 if (err WSAEINTR) break; // 被信号打断退出 std::printf(recvfrom error: %d\n, err); break; } buf[n] \0; char ip[INET_ADDRSTRLEN]{}; ::inet_ntop(AF_INET, from.sin_addr, ip, sizeof(ip)); std::printf([%s:%u] %s\n, ip, ::ntohs(from.sin_port), buf); } ::setsockopt(s, IPPROTO_IP, IP_DROP_MEMBERSHIP, reinterpret_castconst char*(mreq), sizeof(mreq)); ::closesocket(s); ::WSACleanup(); return 0;fromLen每一轮循环都要重新赋值成sizeof(from)。虽然 MSDN 上写着recvfrom内部会重置这个值但实际测试中不同 Windows 版本行为不一致一旦被改成 0后面的调用就会因为长度不够而失败症状是“第一包能收到之后全是错误”。这种血泪经验在跨机器部署时特别容易暴露。SO_RCVTIMEO的作用是让recvfrom最多阻塞 3 秒就返回WSAETIMEDOUT这样主循环里有地方能检查退出标志。生产代码里一般会把接收放到独立线程用一个std::atomicbool做退出信号超时返回时检查这个标志比强杀线程干净得多。buf开到 65535 是 UDP 报文的理论上限实际业务里按kMaxDatagram 头部开就够了。buf[n] \0是为了把收到的数据当字符串打印如果负载是二进制这一步要删掉n才是有效长度。inet_ntop比老的inet_ntoa安全后者返回静态缓冲区多线程下会串数据。它需要ws2tcpip.h也就是公共头里已经包含的那个。5. 避坑与排查组播收不到包的 5 个高频翻车现场组播的问题几乎不报错sendto永远成功recvfrom永远沉默这是排查最难受的地方。下面五条是按“出现频率 × 排查耗时”排序的。5.1 五个现象级问题从现象到原因到解决现象一本机能收同网段另一台机器收不到。原因Windows Defender 防火墙默认拦截入站 UDP。sendto和recvfrom都是本机行为时回环路径不经过防火墙所以自测通过跨机器时包在目标机上被防火墙丢掉接收端毫无感知。 解决在目标机上为接收程序对应的 UDP 端口加一条入站规则高级安全 Windows Defender 防火墙 → 入站规则 → 新建规则 → 端口 → UDP → 45678 → 允许。判断方法是临时关掉防火墙再测一次如果立刻能收就锁定到这一条。现象二多网卡机器上只有某一块网卡所在的网段能收到。原因发送端没设IP_MULTICAST_IF或者接收端imr_interface填了INADDR_ANY系统按路由表优先级挑了默认网卡和实际业务网段对不上。虚拟网卡VMware、Hyper-V、Docker 的 vEthernet经常抢到默认路由。 解决发送端显式setsockopt(IP_MULTICAST_IF)到业务网卡 IP接收端按 4.2 那段的写法遍历网卡逐个加入。先用ipconfig /all或route print把网卡列表打出来确认哪块是业务口。现象三只能跑一个接收端第二个进程启动就报 10048。原因SO_REUSEADDR没设或者设了但位置在bind之后。顺序错了等于没设。 解决把setsockopt(SO_REUSEADDR)挪到bind之前。另外注意 Windows 上还有个SO_EXCLUSIVEADDRUSE如果代码里别处设过它会覆盖SO_REUSEADDR的效果全局搜一下。现象四进程跑了几小时后开始偶发丢包重启就好。原因SO_RCVBUF太小或者处理线程太慢。组播是持续推送的接收端一旦处理速度跟不上内核缓冲一满就静默丢弃业务层只能看到SEQ跳号。 解决先把SO_RCVBUF调到 256KB 以上如果还丢说明处理逻辑本身成了瓶颈把收包和处理拆成两个线程中间用环形队列收包线程只做recvfrom 入队尽量压缩单包处理耗时。现象五换了一台机器编译报LNK2019: __imp__WSAStartup8。原因新机器的 VS 工程没链ws2_32.lib或者#pragma comment(lib, ...)被注释掉了。 解决项目属性 → 链接器 → 输入 → 附加依赖项加ws2_32.lib或者恢复#pragma。别去查头文件这个错误 100% 是链接阶段的问题。5.2 排查工具与定位顺序工具上其实不需要太多三个够用工具用法能定位什么Wireshark过滤ip.dst 239.255.0.1包有没有发出来、TTL 是多少、从哪块网卡出去netstatnetstat -ano -p UDP | findstr 45678端口有没有绑上、被哪个进程占着ipconfig / route printipconfig /all、route print多网卡时系统会从哪块走定位顺序我一般固定按这个链路走先看 Wireshark 里包发出来没有——没有就查发送端的setsockopt和IP_MULTICAST_IF有包但接收端没收到就查接收端有没有bind成功、有没有加入组播组两者都正常了还收不到最后查防火墙和交换机。交换机那一层也值得一提很多企业级交换机会默认开启 IGMP Snooping没有 IGMP 查询器的时候组播流量可能被当成未知组播泛洪丢弃。表现是“同一台交换机下的两台机器收不到直连却可以”。这种情况下要么在交换机上配 IGMP Querier要么临时关掉 Snooping 验证一下。这一步已经超出代码范围但排查到这一层的时候问题往往就在这里。6. 二次开发把 Demo 改成能进生产环境的组播模块Demo 到生产之间隔着的不是功能是稳定性。我一般会在这个骨架上做四件事。第一把收发逻辑封成类。发送端一个MulticastSender构造时完成WSAStartup和全部setsockopt析构时closesocketWSACleanup对外只暴露bool send(const void* data, int len)。接收端一个MulticastReceiverstart()起线程stop()置退出标志并 join。这样业务代码里看不到任何 socket 细节也避免了“忘了WSACleanup导致端口占用残留”这类问题。第二加丢包统计。发送端在报文头里塞 32 位序列号接收端维护一个expectedSeq每收到一包算一次差值累加进丢失计数。这比凭感觉判断“好像有点丢”靠谱得多压测的时候直接看这个数字调SO_RCVBUF。第三明确退出路径。接收线程不要用TerminateThread那会让 socket 处于半关闭状态。正确做法是stop()里把std::atomicbool running_置 false主循环靠SO_RCVTIMEO的超时返回去检查最多延迟 3 秒线程自然结束。第四多网卡适配做成可配置。把IP_MULTICAST_IF和imr_interface的 IP 从配置文件读而不是硬编码。部署到不同网段的机器上时只改配置不用重新编译——这是我在一个跨机房部署里被坑过之后才加上的那次因为网卡 IP 变了现场重启了三次程序都没起来。// 简化的类骨架突出生命周期管理 class MulticastReceiver { public: bool start(const std::string groupIp, unsigned short port, const std::string ifaceIp) { if (!initWinsock()) return false; sock_ ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock_ INVALID_SOCKET) return false; BOOL reuse TRUE; ::setsockopt(sock_, SOL_SOCKET, SO_REUSEADDR, reinterpret_castconst char*(reuse), sizeof(reuse)); sockaddr_in local{}; local.sin_family AF_INET; local.sin_port htons(port); local.sin_addr.s_addr htonl(INADDR_ANY); if (::bind(sock_, reinterpret_castsockaddr*(local), sizeof(local)) SOCKET_ERROR) return false; mreq_.imr_multiaddr.s_addr ::inet_addr(groupIp.c_str()); mreq_.imr_interface.s_addr ifaceIp.empty() ? htonl(INADDR_ANY) : ::inet_addr(ifaceIp.c_str()); if (::setsockopt(sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_castconst char*(mreq_), sizeof(mreq_)) SOCKET_ERROR) return false; running_ true; worker_ std::thread([this] { loop(); }); return true; } void stop() { running_ false; // 靠 SO_RCVTIMEO 超时让 loop 自己退出 if (worker_.joinable()) worker_.join(); ::setsockopt(sock_, IPPROTO_IP, IP_DROP_MEMBERSHIP, reinterpret_castconst char*(mreq_), sizeof(mreq_)); ::closesocket(sock_); ::WSACleanup(); } private: void loop(); // recvfrom 序列号统计 SOCKET sock_ INVALID_SOCKET; ip_mreq mreq_{}; std::atomicbool running_{false}; std::thread worker_; };这个骨架把“资源获取即初始化”和“优雅退出”都收进了类里业务侧只需要start/stop一对调用。改造时最容易忽略的是stop()里那几行的顺序必须先 join 线程再 drop 组播组和关 socket反过来的话工作线程可能还在recvfrom里用着一个已经无效的句柄。从那以后我每次接手组播相关的代码都会先把“发送端设了哪几个setsockopt、接收端bind的地址是什么、有没有IP_ADD_MEMBERSHIP”这三件事对着代码逐条核对一遍再上机测而不是一上来就抓包。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?