简介这份资源是面向VC6.0初学者与网络编程入门者的UDP通信实例包围绕MFC中的CAsyncSocket类展开帮助读者理解无连接传输协议下服务器与客户端的搭建方式。包内共56个文件以h头文件、cpp源文件、dsp与dsw工程文件、rc资源脚本及exe可执行文件为主另含obj、pdb等编译中间产物压缩包约4.43MB工程结构完整可直接在VC6.0中打开调试。实例分为UDPClient与UDPSever两个工程涵盖套接字创建、Bind绑定、Send与Receive收发以及OnReceive等回调处理便于对照学习异步事件驱动的通信流程。资源已有585人学习下载适合希望从零跑通UDP收发、理解CAsyncSocket封装机制并动手改造示例的读者参考。1. 老编译器里的 UDP 通讯一份能直接跑通的 VC6.0 例子手上还有一堆 VC6.0 时代的老工控项目要维护网络模块想加个轻量的状态上报TCP 那套连接管理太重串口又不够用最后往往落到 UDP 通讯上。这份例子就是干这个的在 VC6.0 里用 Winsock 起一个最小可跑的 UDP 收发程序发送端和接收端各一个工程编译完就能互相发数据。它解决的不是高并发网络框架的问题而是「老环境里怎么快速验证一条 UDP 链路通不通」——适合还在维护 MFC 老代码、需要给设备加个简单上报通道、或者刚接触 Winsock 想看清 bind/sendto/recvfrom 到底怎么串起来的从业者。下面按「先跑通、再拆参数、最后说坑」的顺序走一遍。2. 把 UDP 收发拆成两个工程Winsock 初始化与套接字创建VC6.0 的工程向导和现在的 IDE 差别很大先把工程骨架搭对后面才不至于卡在链接错误上。这一章讲清楚为什么 UDP 比 TCP 更适合这个场景、Winsock 在 6.0 里怎么初始化以及发送端和接收端两个工程各自的最小代码。2.1 为什么这个场景选 UDP 而不是 TCPUDP 通讯的核心特征是「无连接、不保证到达、保留报文边界」。放到老工控场景里这三点恰好是优点设备状态上报丢一帧无所谓下一帧马上又来不需要三次握手发送端启动即发接收端没起来也不阻塞每次 sendto 对应一次 recvfrom报文边界天然保留解析时不用自己拼包。反过来如果业务要求「每条指令必须到达且按序」那就该用 TCP硬套 UDP 只会把重传和排序逻辑搬到应用层得不偿失。选型上还有一个现实因素VC6.0 自带的 Winsock 1.1 头文件和库足够支撑 UDP 的全部常用接口不需要额外装 SDK工程配置简单。常见做法是在 stdafx.h 里包含 winsock2.h 并链接 ws2_32.lib但 6.0 默认的 windows.h 会先引入 winsock.h顺序搞反就是一堆重定义错误这个坑后面单独说。2.2 发送端工程初始化、创建套接字、sendto发送端不需要 bind 到固定端口系统会自动分配。核心流程是 WSAStartup → socket → sendto → closesocket → WSACleanup。下面是最小实现放在一个控制台工程里就能编译。#include winsock2.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main(int argc, char* argv[]) { WSADATA wsaData; // 请求 Winsock 2.2实际 6.0 环境回落 1.1 也能跑 if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return -1; } SOCKET s socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (s INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; } sockaddr_in to; memset(to, 0, sizeof(to)); to.sin_family AF_INET; to.sin_port htons(6000); // 接收端监听端口 to.sin_addr.s_addr inet_addr(127.0.0.1); // 本机回环先验证 const char* msg hello udp; int n sendto(s, msg, (int)strlen(msg), 0, (sockaddr*)to, sizeof(to)); if (n SOCKET_ERROR) printf(sendto failed: %d\n, WSAGetLastError()); else printf(sent %d bytes\n, n); closesocket(s); WSACleanup(); return 0; }逻辑说明WSAStartup 是 Winsock 所有调用的前置条件失败时 WSAGetLastError 能给出具体错误码socket 的第二个参数 SOCK_DGRAM 就是 UDP 的标志第三个参数写 IPPROTO_UDP 或 0 都可以sendto 的第五个参数是目标地址结构体指针长度必须传 sizeof(sockaddr_in)传错会导致发送失败但错误码不明显。参数上sin_port 必须用 htons 转网络字节序sin_addr 用 inet_addr 把点分十进制转成网络序这两处是新手最容易漏的。2.3 接收端工程bind 到端口再 recvfrom接收端必须 bind 到一个本地端口否则系统随机分配发送端不知道往哪发。bind 之后进入循环 recvfrom每收到一包就打印来源地址和内容。#include winsock2.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main(int argc, char* argv[]) { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return -1; } SOCKET s socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (s INVALID_SOCKET) { WSACleanup(); return -1; } sockaddr_in local; memset(local, 0, sizeof(local)); local.sin_family AF_INET; local.sin_port htons(6000); local.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网卡 if (bind(s, (sockaddr*)local, sizeof(local)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(s); WSACleanup(); return -1; } char buf[1024]; sockaddr_in from; int fromLen sizeof(from); while (1) { int n recvfrom(s, buf, sizeof(buf) - 1, 0, (sockaddr*)from, fromLen); if (n SOCKET_ERROR) { printf(recvfrom failed: %d\n, WSAGetLastError()); break; } buf[n] \0; printf(from %s:%d - %s\n, inet_ntoa(from.sin_addr), ntohs(from.sin_port), buf); fromLen sizeof(from); // 每轮重置否则部分实现会失败 } closesocket(s); WSACleanup(); return 0; }逻辑说明INADDR_ANY 表示绑定本机所有网卡地址工控机上有多块网卡时这个写法最省事recvfrom 的最后一个参数是值-结果参数调用前要填结构体大小调用后会被改写为实际地址长度所以循环里每轮都要重置 fromLen这一点在 VC6.0 配套的旧版 Winsock 上尤其明显不重置可能第二轮就报错。缓冲区留一个字节给结束符避免打印越界。3. VC6.0 工程配置头文件顺序、库链接与字符集代码能看懂不代表能编译过VC6.0 的工程配置有几个固定套路配错就是满屏错误。这一章把工程层面的配置讲透包括头文件包含顺序、ws2_32.lib 的链接方式、以及 MFC 工程和控制台工程的差异。3.1 头文件顺序winsock2.h 必须在 windows.h 之前VC6.0 的 windows.h 内部会包含旧版 winsock.h如果先包含 windows.h 再包含 winsock2.h两套定义冲突典型报错是「redefinition of fd_set」「conflicting types for select」之类。正确做法是在 stdafx.h 或源文件最顶部先写 winsock2.h。// stdafx.h 里推荐的顺序 #include winsock2.h #include windows.h // 如果确实需要旧版 winsock.h 的某些定义用这个宏抑制 // #define _WINSOCKAPI_逻辑说明winsock2.h 自带一个守卫包含后旧版 winsock.h 的内容会被跳过所以顺序对了就不会冲突。如果工程里第三方头文件强制先包含 windows.h可以在工程设置里预定义WINSOCKAPI宏让 windows.h 不再引入旧版头但这样会失去旧版接口只在确认不用时采用。参数上没有可调的纯粹是顺序问题改一次管一辈子。3.2 链接 ws2_32.lib 的两种方式第一种是代码里写 #pragma comment(lib, ws2_32.lib)简单直接适合单个源文件的小例子。第二种是在 Project → Settings → Link 选项卡的 Object/library modules 里追加 ws2_32.lib适合多工程统一管理。两种方式选一种即可重复链接不会报错但没必要。配置项控制台工程MFC 工程头文件包含位置源文件顶部或 stdafx.hstdafx.h 最前库链接方式pragma 或 Link 选项卡Link 选项卡更稳字符集默认多字节注意 CString 与 char 转换运行库默认单线程多线程调试版需一致表格里最后一行值得展开VC6.0 默认运行库是单线程版如果工程里混用了多线程库编译的静态库会出现 LNK2005 重复定义。UDP 例子本身不涉及多线程但一旦把接收循环放进工作线程就要把 Project → Settings → C/C → Code Generation 里的运行库统一改成 Multithreaded 或 Multithreaded DLL调试版和发布版分别对应 Debug 和 Release 后缀混用是链接错误的常见来源。3.3 从控制台验证到 MFC 集成先在控制台工程里把收发跑通确认链路没问题再往 MFC 对话框工程里搬。搬的时候注意两点一是接收循环不能放在 UI 线程里死等否则界面卡死常见做法是开一个工作线程跑 recvfrom收到数据用 PostMessage 通知主线程刷新控件二是 MFC 的 CString 和 Winsock 的 char 缓冲区之间要转换发送时用 CStringA 或 WideCharToMultiByte接收时反过来别直接强转。// 工作线程里收包收到后投递消息给主线程 UINT RecvThread(LPVOID pParam) { SOCKET s (SOCKET)pParam; char buf[1024]; sockaddr_in from; int fromLen sizeof(from); while (1) { int n recvfrom(s, buf, sizeof(buf) - 1, 0, (sockaddr*)from, fromLen); if (n 0) break; buf[n] \0; // 堆上分配主线程负责释放避免跨线程栈数据失效 char* p new char[n 1]; memcpy(p, buf, n 1); ::PostMessage(g_hWnd, WM_APP 1, (WPARAM)p, 0); fromLen sizeof(from); } return 0; }逻辑说明PostMessage 是异步的投递完立即返回不会阻塞接收线程参数用堆指针传递是因为栈上的 buf 在线程继续循环后会被覆盖主线程处理时数据已经变了这是跨线程传数据的经典翻车点。主线程的消息处理函数里记得 delete[] 释放否则每收一包漏一块内存。4. 避坑与排查UDP 例子跑不通时先看这几处UDP 没有连接状态出错时往往没有明显提示发送端显示成功但接收端收不到是常态。这一章按「现象 → 原因 → 解决」列几条实际踩过的坑覆盖端口、防火墙、字节序和缓冲区四类问题。4.1 发送成功但接收端一包都收不到现象sendto 返回正数字节数接收端 recvfrom 一直阻塞。原因通常是目标地址或端口不对或者接收端根本没 bind 成功。排查顺序是先确认接收端 bind 的返回值再确认发送端 inet_addr 的字符串没写错最后用 netstat -an 看端口有没有处于监听状态。UDP 的 netstat 显示是 *:* 形式看到 6000 端口在列说明 bind 生效。另一个隐蔽原因是发送端和接收端不在同一台机器时目标地址写成了 127.0.0.1回环地址只能本机通跨机必须写对端实际 IP。4.2 本机通、跨机不通现象两台机器各自跑收发都正常互相发就收不到。原因多半是系统防火墙拦了入站 UDP。解决是在接收端机器的防火墙里给对应端口放行入站规则或者临时关闭防火墙验证。工控环境里还有一层是交换机或路由的 ACL这个超出代码范围但排查时要有这个意识先用 ping 确认网络层通再用端口探测工具确认 UDP 端口可达最后才怀疑代码。4.3 收到的数据是乱码或端口号显示异常现象接收端打印的来源端口是个很大的数或者内容对不上。原因是字节序没转换。sin_port 在网络包里是大端直接打印就是错的必须 ntohssin_addr 打印用 inet_ntoa 而不是直接读 s_addr。发送端设置端口时同理htons 不能省。这类问题不影响收发本身但会让日志误导排查方向血泪经验是日志里所有网络字段统一走转换函数别图省事直接 printf。4.4 recvfrom 第二轮开始报错现象第一包正常第二包 recvfrom 返回 SOCKET_ERROR。原因是 fromLen 没有在每轮循环前重置为 sizeof(from)。recvfrom 会把实际地址长度写回这个变量第二轮传进去的就是上一轮的小值旧版 Winsock 实现会因此报 WSAEFAULT。解决就是循环体内每次调用前重新赋值代码里那行fromLen sizeof(from);不是多余的。4.5 大包发送失败或截断现象发几百字节正常发几 KB 就失败或接收端只收到一部分。原因是 UDP 单包有大小上限理论上限 64KB 左右但实际受 MTU 和系统缓冲区限制超过 1472 字节以太网 MTU 1500 减去 IP 头和 UDP 头就会分片分片丢失整包作废。解决是业务层控制单包大小超过就自己拆包加序号别指望 UDP 帮你重组。接收缓冲区也要设得比预期包大留余量。5. 进阶给这个例子加上超时与收发分离基础例子跑通后实际项目里马上会遇到两个需求接收不能无限阻塞以及同一进程要能同时收发。这一章给两个可直接套用的改法都是在这个例子的骨架上加不引入额外依赖。5.1 用 setsockopt 设置接收超时recvfrom 默认阻塞界面或主循环会被卡住。用 SO_RCVTIMEO 设一个超时超时后返回 SOCKET_ERROR错误码 WSAETIMEDOUT据此做其他事再回来收。int timeout 1000; // 毫秒 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)timeout, sizeof(timeout)); // 循环里这样处理 int n recvfrom(s, buf, sizeof(buf) - 1, 0, (sockaddr*)from, fromLen); if (n SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) continue; // 超时不算错误继续下一轮 break; // 其他错误才退出 }逻辑说明SO_RCVTIMEO 的参数在 Windows 上是毫秒整数Linux 上是 timeval 结构跨平台代码要分开写超时值别设太小工控环境网络抖动时 100ms 容易误判1 秒是个稳妥起点。注意超时后套接字本身没坏可以继续用这点和连接类错误不同。5.2 单套接字同时收发与多播入门UDP 套接字本身是全双工的同一个 socket 既能 sendto 也能 recvfrom不需要开两个。如果要做一对多上报把目标地址换成多播地址224.0.0.0 到 239.255.255.255 范围接收端用 IP_ADD_MEMBERSHIP 加入组即可发送端照常 sendto 到多播地址。多播在局域网内效率高但跨网段需要路由支持工控现场用之前先确认交换机配置。场景套接字数量关键设置点对点收发1 个bind 本地端口一对多上报1 个发送 N 个接收接收端 IP_ADD_MEMBERSHIP需要超时控制1 个SO_RCVTIMEO大包传输不建议用 UDP改 TCP 或应用层拆包最后说个习惯。这个例子我前后在不同机器上配过好几次每次翻车几乎都出在头文件顺序和 fromLen 重置这两处代码本身反而没出过问题。从那以后我每次新建 VC6.0 的网络工程都先把 stdafx.h 的头文件顺序固定下来接收循环里 fromLen 的重置写成模板编译前扫一眼这两处再点 Build。老环境没有现代 IDE 的智能提示兜底靠的就是把踩过的坑固化成固定动作。希望帮到你。本文还有配套的精品资源点击获取