
简介这份资源是面向VC6.0初学者与网络编程入门者的UDP通信实例包围绕MFC中的CAsyncSocket类展开帮助读者理解无连接通信的建立、监听、收发与回调处理流程。压缩包共56个文件约4.43MB包含8个h头文件、6个cpp源文件以及dsp、dsw工程文件、rc资源脚本、ico图标和exe可执行程序等完整保留了UDPClient与UDPSever两套工程目录便于直接编译运行与对照阅读。资源已有585人学习下载适合作为课程实验或自学练手素材。读者可从中获得服务器端Bind、Listen与客户端Connect的完整代码骨架OnReceive、OnSend等异步回调的写法示例以及发送与接收缓冲区的处理思路同时能直观感受UDP不可靠性带来的数据丢失与乱序问题为后续在应用层实现重传与排序机制打下基础。1. 从一份“老古董”工程说起UDP 通讯到底能拿来干什么前阵子帮一个做工业控制上位机的朋友看代码他接手了一套跑了十几年的老系统通讯层全是 VC6.0 时代的 UDP 实现。他问我这年头谁还看 MFC 和 Winsock 1.1我让他先别急着推翻重写因为这套东西解决的是一个非常具体的问题——在局域网里做低延迟、无连接、丢一两个包也不影响业务的数据上报。UDP 通讯的核心价值就在这里不建连接、不重传、开销小适合状态广播、心跳、采集数据上报这类场景。这份基于 VC6.0 的 UDP 通讯例子就是一个把 Winsock 收发流程跑通的最小工程适合两类人一是要维护或迁移老工控、老教学项目的开发者二是想搞明白“UDP 在 Windows 上到底怎么落地”的新手。它不花哨但把 socket 创建、绑定、收发、关闭这条链路完整摆在你面前跑一遍比看十页协议文档都管用。2. 工程结构与技术选型VC6.0 Winsock 的最小闭环2.1 为什么是 VC6.0 和 Winsock这个例子的技术栈非常朴素IDE 是 VC6.0语言是 C通讯依赖 Windows 自带的 Winsock 库ws2_32.lib。选这套组合不是因为它先进而是因为大量存量工控项目、教学实验环境还锁死在这个工具链上。VC6.0 的编译器对 C 标准支持很老所以工程里基本看不到 STL 容器和现代 C 特性更多是char数组、struct、全局函数这一套写法。Winsock 方面例子通常走 Winsock 1.1 的WSAStartup初始化流程也有部分版本用 2.2区别在于WSAStartup请求的版本号和是否需要getaddrinfo。对 UDP 来说1.1 的socket、bind、sendto、recvfrom已经够用没必要上 2.2 的异步接口。需要提前说清楚一个现实VC6.0 在现代 Windows 上安装和编译都可能翻车尤其是 64 位系统和较新的 SDK。常见做法是把工程放到 32 位兼容模式下编译或者干脆用 VS2010 以上版本打开.dsp工程做一次转换。转换后代码逻辑不变只是工程文件格式变了。这一点在动手前要有心理准备别一上来就怀疑代码写错了。2.2 目录与文件构成这类例子工程的文件结构通常很扁平核心就几个文件文件作用UdpDemo.cpp主程序入口包含main或WinMain负责初始化、调用收发逻辑UdpDemo.h函数声明、常量定义端口号、缓冲区大小UdpDemo.dsp/.dswVC6.0 的工程文件与工作区文件ws2_32.lib链接配置在工程设置里手动加入否则链接报错如果是 MFC 版本还会多出StdAfx.cpp、StdAfx.h以及对话框资源文件.rc。纯控制台版本则简单得多一个.cpp就能跑。判断标准很简单看入口函数是main还是InitInstance。控制台版更适合先跑通逻辑MFC 版更适合做带界面的上位机。我一般建议先跑控制台版把收发链路验证通了再往界面里搬。2.3 工程配置里必须动的两处VC6.0 新建或打开工程后有两处配置不改就跑不起来。第一处是链接库菜单Project → Settings → Link在Object/library modules里补上ws2_32.lib。第二处是字符集和运行库老工程默认可能是Debug单线程库如果换到新环境编译报LNK2001找不到符号多半是运行库不匹配改成Multithreaded DLL通常能解决。# 工程设置里等价于在链接阶段加入 ws2_32.lib # 若用命令行方式编译cl 为 VS 工具链大致是 cl UdpDemo.cpp ws2_32.lib上面这段不是让你真去敲命令行而是说明链接依赖的本质Winsock 的函数实现不在你的代码里而在系统动态库里编译器必须知道去哪里找。参数上ws2_32.lib是导入库运行时对应ws2_32.dllWindows 系统自带不需要额外分发。很多人第一次编译报一堆unresolved external symbol八成就是漏了这一行。3. 核心代码拆解一个 UDP 包从创建到收发的完整链路3.1 初始化与 socket 创建UDP 通讯的第一步永远是WSAStartup没有它后面所有调用都会返回WSANOTINITIALISED。这段代码几乎是模板化的但参数含义值得说清楚。#include winsock2.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsaData; // 请求 Winsock 2.2 版本MAKEWORD 把主次版本号拼成一个 WORD if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return -1; } // AF_INET 表示 IPv4SOCK_DGRAM 表示 UDPIPPROTO_UDP 可写 0 SOCKET sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; } // ... 后续绑定与收发 }WSAStartup的第二个参数是输出参数系统把实际支持的版本信息填进wsaData一般不需要读。MAKEWORD(2,2)表示请求 2.2 版如果系统只支持 1.1调用仍可能成功但版本被降级所以严格的做法是检查wsaData.wVersion。socket三个参数里AF_INET决定地址族SOCK_DGRAM决定是数据报UDP第三个参数传IPPROTO_UDP或 0 都行。返回INVALID_SOCKET时一定要用WSAGetLastError()拿错误码光看返回值判断不出原因。3.2 绑定本地地址与端口服务端接收方必须bind客户端发送方可以省略系统会自动分配临时端口。绑定这一步是把 socket 和本机的 IP、端口关联起来。sockaddr_in localAddr; memset(localAddr, 0, sizeof(localAddr)); localAddr.sin_family AF_INET; localAddr.sin_port htons(6000); // 主机字节序转网络字节序 localAddr.sin_addr.s_addr INADDR_ANY; // 监听所有网卡 if (bind(sock, (sockaddr*)localAddr, sizeof(localAddr)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(sock); WSACleanup(); return -1; }htons是必须的网络传输统一用大端字节序而 x86 是小端不转换端口号就会变成一个奇怪的数字。INADDR_ANY表示绑定到所有本地网卡如果只想监听某个特定 IP可以用inet_addr(192.168.1.10)替换。bind失败最常见的原因是端口被占用错误码WSAEADDRINUSE换个端口或先查netstat -ano就能定位。注意sockaddr_in要强制转换成sockaddr*这是 Winsock 接口的历史设计不是笔误。3.3 发送与接收sendto 和 recvfromUDP 的收发是成对的发送用sendto指定目标地址接收用recvfrom同时拿到数据和对端地址。这是它和 TCP 最大的写法差异——UDP 没有连接每次发送都要带地址。// 发送端构造目标地址 sockaddr_in remoteAddr; memset(remoteAddr, 0, sizeof(remoteAddr)); remoteAddr.sin_family AF_INET; remoteAddr.sin_port htons(6000); remoteAddr.sin_addr.s_addr inet_addr(127.0.0.1); const char* msg hello udp; int sent sendto(sock, msg, (int)strlen(msg), 0, (sockaddr*)remoteAddr, sizeof(remoteAddr)); if (sent SOCKET_ERROR) printf(sendto failed: %d\n, WSAGetLastError()); // 接收端阻塞等待数据 char buf[1024]; sockaddr_in fromAddr; int fromLen sizeof(fromAddr); int received recvfrom(sock, buf, sizeof(buf) - 1, 0, (sockaddr*)fromAddr, fromLen); if (received 0) { buf[received] \0; // 手动补字符串结束符 printf(recv from %s:%d - %s\n, inet_ntoa(fromAddr.sin_addr), ntohs(fromAddr.sin_port), buf); }sendto的第四个参数是标志位通常传 0返回值是实际发送的字节数UDP 下要么全发要么报错不存在“发了一半”的情况除非数据报超过 MTU 被分片那是 IP 层的事。recvfrom默认是阻塞的没有数据就一直卡在那里这也是为什么很多例子会配合select或设置超时。fromLen是值-结果参数调用前要初始化为缓冲区大小返回后变成实际地址长度。inet_ntoa把网络字节序的 IP 转成点分字符串注意它返回的是静态缓冲区多次调用会互相覆盖多线程下要小心。3.4 收尾closesocket 与 WSACleanup资源释放这块经常被忽略但在循环创建 socket 的场景里漏掉会耗尽句柄。closesocket(sock); // 关闭单个 socket WSACleanup(); // 释放 Winsock 库资源与 WSAStartup 配对 return 0;closesocket对应socketWSACleanup对应WSAStartup成对出现是基本纪律。在 MFC 程序里WSACleanup一般放在ExitInstance或析构里别放在按钮响应函数里反复调用否则后续 socket 操作会失败。这套收尾逻辑看着简单但我在老项目里见过在OnClose里只closesocket不WSACleanup的写法短时间没问题长时间反复开关窗口就会出玄学故障。4. 跑起来与调试从编译到两个进程互通4.1 编译与运行步骤先把工程用 VC6.0 打开确认链接了ws2_32.lib然后Build → Build UdpDemo.exe。如果报错集中在winsock2.h找不到检查是否装了 Platform SDK 或者头文件路径是否被改乱。编译通过后运行方式有两种单进程自收自发目标地址填127.0.0.1先 bind 再 sendto 给自己或者开两个进程一个只收一个只发。自收自发是最快的验证方式能排除网络和防火墙因素。# 验证端口是否真的在监听Windows 命令行 netstat -ano | findstr 6000 # 若看到 UDP 0.0.0.0:6000说明 bind 成功netstat里 UDP 行显示的是UDP而不是TCP状态列通常是空的因为 UDP 无连接。这一步能快速区分“代码没跑起来”和“跑了但没收到包”。如果netstat看不到端口问题在bind之前如果看得到但收不到数据问题在发送端或防火墙。4.2 用日志定位收发断点UDP 调试最怕“没反应”因为无连接你根本不知道包发出去没有。我的习惯是在每个关键节点打日志WSAStartup后打一行、bind后打一行、sendto返回值打一行、recvfrom返回后打一行。这样一眼能看出链路断在哪。WSAGetLastError()的返回值要打印出来常见错误码10054连接被重置UDP 下通常是对方端口不可达的 ICMP 回包触发、10048地址已占用、10049地址不可用都有明确含义查一下就能定位。4.3 防火墙与回环地址的坑本机测试用127.0.0.1一般不受防火墙影响但一旦换成局域网真实 IPWindows 防火墙可能静默丢弃入站 UDP 包。现象是发送端sendto返回成功接收端就是收不到。解决方式是给测试程序放行或者临时关闭防火墙做对照实验。另外如果机器有多张网卡比如虚拟网卡INADDR_ANY绑定后收到的包可能来自预期之外的网卡排查时用recvfrom打印对端 IP 就能确认。5. 避坑与常见问题老工程里那些血泪经验5.1 编译报 winsock2.h 与 windows.h 冲突现象是编译时报大量重定义错误集中在winsock2.h和windows.h的包含顺序上。原因是windows.h默认会拉入老的winsock.h和winsock2.h冲突。解决办法是把#include winsock2.h放在#include windows.h之前或者在包含windows.h前定义WIN32_LEAN_AND_MEAN宏砍掉它自带的 Winsock 1.1 部分。这个坑在 VC6.0 里尤其常见因为老工程的StdAfx.h往往先包含了windows.h。5.2 recvfrom 一直阻塞界面卡死现象是点了“接收”按钮后程序无响应。原因是recvfrom默认阻塞主线程被挂起消息循环跑不动。解决方式有三种把接收放到工作线程用select设置超时后再recvfrom或者用ioctlsocket把 socket 设为非阻塞。MFC 项目里我一般选工作线程因为改起来最干净界面线程只负责刷新。注意线程里操作界面控件要走PostMessage别直接跨线程调SetWindowText。5.3 发送成功但对方收不到现象是sendto返回的字节数正常接收端毫无反应。原因可能是目标端口写错、目标 IP 不可达、防火墙拦截或者接收端根本没bind。排查顺序是先在接收端netstat确认端口在监听再在发送端确认inet_addr的字符串没写错比如把192.168.1.10写成192.168.1.100最后用抓包工具看包有没有真正离开本机。UDP 没有重传和确认发送成功只代表数据交给了协议栈不代表对方收到这一点必须建立预期。5.4 大数据包被截断或分片现象是发送 2000 字节接收端只收到一部分或干脆失败。原因是 UDP 单包受 MTU 限制以太网 MTU 通常 1500减去 IP 和 UDP 头有效载荷约 1472 字节。超过这个值 IP 层会分片任何一片丢失整个包就废了。解决办法是把业务数据控制在 1400 字节以内超出的自己拆包编号。老项目里常见把结构体直接sendto的写法结构体一旦加字段就超限这是典型的埋雷。5.5 端口复用与重复 bind 失败现象是程序重启后bind报10048。原因是上一个进程还没完全释放端口或者用了SO_REUSEADDR的写法不当。UDP 下端口释放通常很快但如果程序异常退出端口可能短暂处于占用状态。解决方式是等几秒重试或者在bind前用setsockopt设置SO_REUSEADDR。不过要注意UDP 的SO_REUSEADDR语义和 TCP 不同多进程绑同一端口的行为依赖系统实现别指望它做负载均衡。6. 二次开发改造把这套例子变成能用的采集端拿到这个例子后真正的工作量不在“跑通”而在“改成能承载业务”。我一般按三步走先抽通讯层再定协议最后加容错。第一步把socket创建、bind、收发封装成一个CUdpSocket类暴露Open、Send、Recv、Close四个方法界面代码不再直接碰 Winsock API。这样以后换 TCP 或者换平台改动面可控。第二步定一个简单的应用层协议比如包头 4 字节长度 2 字节命令字 数据体解决粘包和半包问题——虽然 UDP 本身有消息边界但业务上仍需要区分消息类型。第三步加超时和重试策略recvfrom用select包一层超时返回而不是死等发送端对关键指令做应用层确认。// 用 select 给 recvfrom 加超时的典型写法 fd_set readSet; FD_ZERO(readSet); FD_SET(sock, readSet); timeval tv; tv.tv_sec 1; // 超时 1 秒 tv.tv_usec 0; int ret select(0, readSet, NULL, NULL, tv); if (ret 0 FD_ISSET(sock, readSet)) { // 有数据可读再调用 recvfrom 就不会阻塞 } else if (ret 0) { // 超时做心跳或重试逻辑 }select的第一个参数在 Windows 下被忽略传 0 即可这点和 Linux 不同移植时要注意。timeval控制等待时长返回 0 表示超时返回SOCKET_ERROR表示出错。把这段包进接收循环就能让采集端在没数据时定期做别的事而不是卡死。验证改造是否成功我习惯用两个指标一是连续跑 24 小时不崩、句柄数不涨二是人为拔网线再插回程序能自动恢复收发。前者查资源泄漏后者查异常处理。老工程最容易在异常路径上漏closesocket跑久了句柄耗尽表现就是“昨天还好好的今天突然收不到了”。从那以后我每次接手这类 VC6.0 的 UDP 老工程都强制先把WSAStartup到WSACleanup的配对、bind的端口占用、recvfrom的阻塞这三件事各写一个最小验证用例跑一遍再动业务代码。这套习惯帮我省下了大量“以为是协议问题、其实是资源没释放”的排查时间。希望帮到你。本文还有配套的精品资源点击获取