简介:这是一份面向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 那种“部分发送”的情况。如果你发送的数据超过 MTU,IP 层会自动分片,但分片会显著增加丢包概率,建议应用层控制单包在 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++ 默认使用多字节字符集,但如果你在项目属性里改成了 Unicode,char和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 基于 TCP,UDP 套接字不能直接发 HTTP 请求。如果你需要把 UDP 收到的数据转发到 HTTP 服务端,得单独创建一个 TCP 套接字或者用 WinHTTP/WinINet 库。常见做法是 UDP 负责设备侧的低延迟上报,HTTP 负责业务侧的数据落库,两者通过内存队列解耦。
4.5 一个我反复用的调试习惯:先抓包再改代码
每次遇到“发了但收不到”或者“收了但数据不对”的问题,我的第一反应不是改代码,而是打开 Wireshark 抓包。过滤规则用udp.port == 8888,看数据包到底有没有发出去、目标 IP 和端口对不对、payload 的十六进制内容是否符合预期。抓包能看到代码层面看不到的信息,比如网卡是否真的发出了包、中间有没有被防火墙丢弃、对方有没有回 ICMP 错误。这个习惯帮我省下了大量在代码里盲目加日志的时间。希望帮到你。
本文还有配套的精品资源,点击获取