简介这是一份面向VC初学者与网络编程入门者的UDP通信演示工程围绕Windows平台Winsock套接字展开帮助读者理解无连接传输协议的基本用法。资源以客户端与服务器双端示例为主线覆盖WSAStartup初始化、socket创建、sockaddr_in地址结构、bind绑定、sendto发送与recvfrom接收等核心环节并涉及错误码捕获与缓冲区管理等常见问题。压缩包共15个文件约11KB包含6个cpp源文件、3个头文件、3个dsp工程文件、2个dsw工作区文件及1个positions文件工程配置与源码配套齐全可直接在VC环境中打开编译调试。目前已有131人学习下载。通过对照客户端与服务器两端代码读者能够快速掌握UDP数据收发流程理解地址与端口设置方式并在此基础上扩展多线程或异步处理适合作为网络编程课程实验与自学练手的参考范例。1. 从一份 VC UDP Demo 说起为什么它至今仍是网络编程的入门硬通货如果你手里正好有一个名为UDP.rar_DEMO_UDP_VC UDP_udp demo_vc UDP的压缩包或者你正在搜索引擎里翻找“vc udp demo”的可用代码那你大概率遇到了一个非常具体的场景需要在 Windows 上用 Visual C 快速搭起一个能跑通的 UDP 收发程序。这不是要你从零研究协议栈也不是让你去啃 RFC 768而是需要一个能编译、能运行、能改参数、能直接嵌进现有工程的最小可用骨架。UDP 协议本身不复杂但落到 VC 的 Winsock 环境里从WSAStartup到recvfrom的阻塞处理再到closesocket的清理顺序每一步都有固定的套路和容易翻车的地方。这份 Demo 的价值就在于它把“能跑”这件事压缩到了最短路径上。适合谁看适合刚接触 Windows 网络编程的新手也适合需要快速验证 UDP 打流、调试设备通信、做协议对接的老手。你不需要先成为 TCP/IP 专家但你需要知道SOCK_DGRAM和SOCK_STREAM在代码层面的分叉点在哪里。2. 拆开 VC UDP Demo 的最小骨架从工程配置到收发闭环2.1 为什么选 UDP 而不是 TCP三个必须想清楚的选型理由在动手改代码之前先确认你选 UDP 不是因为它“看起来简单”。UDP 和 TCP 的区别在面试里能背出十条但落到工程决策上真正影响你选型的只有三点。第一你的场景是否容忍丢包。UDP 不保证送达也不保证顺序如果业务逻辑里每一帧数据都必须完整到达且按序处理那 TCP 是更省心的选择。第二你是否需要广播或组播。TCP 只支持单播而 UDP 天然支持广播和组播像设备发现、局域网状态同步这类需求UDP 几乎是唯一选择。第三你对延迟的敏感程度。TCP 的拥塞控制和重传机制在弱网下会引入不可控的延迟抖动而 UDP 把控制权完全交给应用层适合实时性优先、允许少量丢包的场景比如视频流、游戏状态同步、传感器高频上报。常见做法是先用 UDP 把通信跑通如果发现丢包率超出业务容忍度再在应用层加序号和重传或者直接换 TCP。不要一上来就纠结“UDP 不可靠怎么办”先让数据包能发出去、能收回来再谈可靠性增强。2.2 用 VS 建一个能编译的 UDP 工程项目属性里必须改的三处拿到 Demo 源码后第一件事不是看main函数而是确认工程配置能过编译。VC 的 Winsock 编程需要链接ws2_32.lib这个库在默认的控制台工程里不会自动带上。打开项目属性页按下面三步走。第一步确认平台工具集和 SDK 版本匹配。如果你用的是 VS2017 或更高版本右键项目 → 属性 → 常规 → 平台工具集选你本机安装的版本。第二步在“链接器 → 输入 → 附加依赖项”里加上ws2_32.lib。第三步确认“C/C → 预处理器 → 预处理器定义”里没有误删_WINSOCK_DEPRECATED_NO_WARNINGS否则inet_addr这类函数会报弃用警告虽然不影响运行但会干扰你排查真正的错误。// 标准头文件顺序不要随意调换 #include winsock2.h #include ws2tcpip.h #include iostream #include string #pragma comment(lib, ws2_32.lib) // 显式链接避免手动改项目属性上面这段代码里#pragma comment(lib, ws2_32.lib)是一种更省事的做法直接把链接指令写在源码里这样即使换一台机器编译也不用重新配项目属性。注意winsock2.h必须排在windows.h前面否则会触发重定义错误这是 VC 网络编程里最经典的翻车点之一。2.3 初始化 Winsock 与创建套接字每个返回值的检查都不能省Winsock 的初始化是整套流程的入口WSAStartup失败意味着后续所有网络调用都没有意义。很多 Demo 为了简洁会忽略返回值检查但你在实际项目里必须逐项判断。WSADATA wsaData; int ret WSAStartup(MAKEWORD(2, 2), wsaData); if (ret ! 0) { std::cerr WSAStartup failed: ret std::endl; return -1; } SOCKET sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock INVALID_SOCKET) { std::cerr socket failed: WSAGetLastError() std::endl; WSACleanup(); return -1; }MAKEWORD(2, 2)请求的是 Winsock 2.2 版本这是目前 Windows 上最通用的版本。socket函数的三个参数分别指定地址族、套接字类型和协议UDP 对应SOCK_DGRAM和IPPROTO_UDP。如果socket返回INVALID_SOCKET用WSAGetLastError()拿到具体错误码常见的 10093 表示 Winsock 未初始化10047 表示地址族不支持。2.4 绑定端口与收发数据recvfrom的阻塞行为和超时设置UDP 接收端必须绑定一个本地端口否则操作系统不会把数据包投递给你。发送端可以不绑定系统会自动分配一个临时端口。下面是一个完整的接收循环骨架。sockaddr_in recvAddr; recvAddr.sin_family AF_INET; recvAddr.sin_port htons(8888); // 监听 8888 端口 recvAddr.sin_addr.s_addr INADDR_ANY; // 接受任意网卡的数据 if (bind(sock, (sockaddr*)recvAddr, sizeof(recvAddr)) SOCKET_ERROR) { std::cerr bind failed: WSAGetLastError() std::endl; closesocket(sock); WSACleanup(); return -1; } char buffer[1024]; sockaddr_in senderAddr; int senderLen sizeof(senderAddr); while (true) { int bytesReceived recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)senderAddr, senderLen); if (bytesReceived SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) { std::cout recv timeout, continue... std::endl; continue; } std::cerr recvfrom failed: err std::endl; break; } buffer[bytesReceived] \0; std::cout Received from inet_ntoa(senderAddr.sin_addr) : ntohs(senderAddr.sin_port) - buffer std::endl; }recvfrom默认是阻塞的没有数据时会一直挂起。如果你需要定时做其他事情有两种方案一是用setsockopt设置SO_RCVTIMEO超时二是把套接字设为非阻塞模式配合select或WSAEventSelect。上面代码里判断了WSAETIMEDOUT这是设置了接收超时后的正常返回不应该当成致命错误。发送端的代码更短核心是sendto。sockaddr_in destAddr; destAddr.sin_family AF_INET; destAddr.sin_port htons(8888); destAddr.sin_addr.s_addr inet_addr(127.0.0.1); const char* msg hello udp demo; int sent sendto(sock, msg, (int)strlen(msg), 0, (sockaddr*)destAddr, sizeof(destAddr)); if (sent SOCKET_ERROR) { std::cerr sendto failed: WSAGetLastError() std::endl; }sendto的返回值是实际发送的字节数对于 UDP 来说要么全部发出要么返回错误不存在 TCP 那种“部分发送”的情况。如果你发送的数据超过 MTUIP 层会自动分片但分片会显著增加丢包概率建议应用层控制单包在 1400 字节以内。2.5 清理顺序closesocket和WSACleanup的调用时机很多 Demo 在退出时直接return 0不清理套接字和 Winsock 环境。短时间跑一下没问题但如果你的程序会被反复启动或者嵌在服务里长期运行资源泄漏会逐渐累积。正确的顺序是先closesocket(sock)再WSACleanup()。反过来调用会导致closesocket失败因为 Winsock 环境已经卸载了。closesocket(sock); WSACleanup();如果你有多个套接字每个都要单独closesocket最后统一WSACleanup。不要依赖进程退出时的自动回收那是在给自己埋雷。3. 避坑与排查VC UDP Demo 跑不通时先看这五个地方3.1 现象bind返回 10048端口被占用原因很直接你选的端口已经被另一个进程监听了。UDP 虽然不像 TCP 那样有严格的连接状态但同一端口在同一时刻只能被一个套接字绑定除非设置了SO_REUSEADDR。解决方法是换一个端口或者在bind之前设置SO_REUSEADDR。int opt 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)opt, sizeof(opt));注意SO_REUSEADDR在 UDP 场景下的行为和 TCP 不同它允许同一端口被多个套接字绑定但数据包的投递规则取决于操作系统通常只有最后一个绑定的套接字能收到数据。所以更稳妥的做法还是换端口。3.2 现象recvfrom返回 10054连接被重置这个错误码WSAECONNRESET在 UDP 里出现通常是因为你之前用sendto给某个目标发过数据对方没有监听操作系统收到 ICMP 端口不可达消息后把这个错误反馈给了你的套接字。这是 UDP 的正常行为不是你的代码写错了。解决办法是忽略这个错误继续接收或者用WSAGetLastError判断后continue。3.3 现象发送成功但接收端收不到防火墙在拦截Windows 防火墙默认会拦截未经授权的入站 UDP 流量。如果你在本地127.0.0.1测试能通换成局域网 IP 就不行大概率是防火墙的问题。临时关闭防火墙验证一下如果确认是防火墙需要在“高级安全 Windows Defender 防火墙”里为你的程序添加入站规则放行对应的 UDP 端口。3.4 现象中文数据乱码字符集没统一VC 默认使用多字节字符集但如果你在项目属性里改成了 Unicodechar和wchar_t的混用会导致发送和接收的字节流不一致。UDP 传输的是原始字节不关心编码。发送端用strlen算长度接收端用recvfrom拿到的字节数直接当字符串用只要两端编码一致就不会乱码。建议统一用 UTF-8 编码发送前确认strlen和实际字节数一致。3.5 现象程序退出时报内存错误套接字没关干净如果你在recvfrom阻塞期间直接关闭控制台窗口操作系统会强制回收资源不会报错。但如果你是在代码里break出循环后忘记closesocket再次运行程序时可能遇到端口被占用或句柄泄漏。养成习惯每个socket调用后面都跟一个对应的closesocket每个WSAStartup后面都跟一个WSACleanup。4. 把 Demo 变成工具UDP 打流测试与协议对接的进阶用法4.1 用iperf3做 UDP 打流验证链路真实带宽Demo 跑通之后下一步通常是验证网络质量。iperf3是业界常用的打流工具UDP 模式下可以指定带宽、包长和测试时长。服务端执行iperf3 -s客户端执行iperf3 -c server_ip -u -b 100M -t 30 -l 1400。参数含义-u表示 UDP 模式-b 100M表示目标带宽 100 Mbps-t 30表示测试 30 秒-l 1400表示每个数据包 1400 字节。测试结束后关注三个指标丢包率、抖动和乱序包数。如果丢包率超过 1%说明链路质量不足以支撑实时业务需要考虑降低码率或增加应用层重传。4.2 在 Demo 里加一个简单的包序号和校验快速判断丢包位置UDP 本身不提供丢包检测但你可以在应用层加一个 4 字节的序号和 2 字节的校验和。发送端每发一包序号加一接收端检查序号是否连续不连续就记录丢包区间。校验和可以用简单的累加和用来发现比特翻转。struct UdpPacket { uint32_t seq; uint16_t checksum; char payload[1400]; }; uint16_t calcChecksum(const char* data, int len) { uint32_t sum 0; for (int i 0; i len; i) { sum (unsigned char)data[i]; } return (uint16_t)(sum 0xFFFF); }这个结构体在发送前填充seq和checksum接收端先校验再处理。注意结构体要按 1 字节对齐打包否则不同编译器可能插入填充字节导致解析错位。用#pragma pack(push, 1)和#pragma pack(pop)包住结构体定义。4.3 用select同时管理多个 UDP 套接字当你的程序需要同时监听多个端口或者既要收又要发还要处理定时任务时阻塞式的recvfrom就不够用了。select是 Windows 上最通用的多路复用方案虽然性能不如 IOCP但对于几十个套接字的场景完全够用。fd_set readSet; FD_ZERO(readSet); FD_SET(sock, readSet); timeval timeout; timeout.tv_sec 1; timeout.tv_usec 0; int activity select(0, readSet, NULL, NULL, timeout); if (activity 0 FD_ISSET(sock, readSet)) { // 有数据可读调用 recvfrom }select的第一个参数在 Windows 上被忽略填 0 即可。timeout控制阻塞时长设为 0 则立即返回设为 NULL 则一直阻塞。这种模式适合需要周期性做状态检查的场景比如心跳超时判断。4.4 对接 HTTP 服务端 API 时的 UDP 边界有些场景下你需要 VC 程序既发 UDP 又调 HTTP 接口。这两者不冲突但要注意HTTP 基于 TCPUDP 套接字不能直接发 HTTP 请求。如果你需要把 UDP 收到的数据转发到 HTTP 服务端得单独创建一个 TCP 套接字或者用 WinHTTP/WinINet 库。常见做法是 UDP 负责设备侧的低延迟上报HTTP 负责业务侧的数据落库两者通过内存队列解耦。4.5 一个我反复用的调试习惯先抓包再改代码每次遇到“发了但收不到”或者“收了但数据不对”的问题我的第一反应不是改代码而是打开 Wireshark 抓包。过滤规则用udp.port 8888看数据包到底有没有发出去、目标 IP 和端口对不对、payload 的十六进制内容是否符合预期。抓包能看到代码层面看不到的信息比如网卡是否真的发出了包、中间有没有被防火墙丢弃、对方有没有回 ICMP 错误。这个习惯帮我省下了大量在代码里盲目加日志的时间。希望帮到你。本文还有配套的精品资源点击获取