简介:这份资源是面向VC++6.0初学者与网络编程入门者的UDP通信实例包,围绕MFC中的CAsyncSocket类展开,帮助读者理解无连接传输协议下服务器与客户端的搭建方式。包内共56个文件,以h头文件、cpp源文件、dsp与dsw工程文件、rc资源脚本及exe可执行文件为主,另含obj、pdb等编译中间产物,压缩包约4.43MB,工程结构完整,可直接在VC++6.0中打开调试。实例分为UDPClient与UDPSever两个工程,涵盖套接字创建、Bind绑定、Send与Receive收发以及OnReceive等回调处理,便于对照学习异步事件驱动的通信流程。资源已有585人学习下载,适合希望从零跑通UDP收发、理解CAsyncSocket封装机制并动手改造示例的读者参考。
1. 老编译器里的 UDP 通讯:一份能直接跑通的 VC++6.0 例子
手上还有一堆 VC++6.0 时代的老工控项目要维护,网络模块想加个轻量的状态上报,TCP 那套连接管理太重,串口又不够用,最后往往落到 UDP 通讯上。这份例子就是干这个的:在 VC++6.0 里用 Winsock 起一个最小可跑的 UDP 收发程序,发送端和接收端各一个工程,编译完就能互相发数据。它解决的不是高并发网络框架的问题,而是「老环境里怎么快速验证一条 UDP 链路通不通」——适合还在维护 MFC 老代码、需要给设备加个简单上报通道、或者刚接触 Winsock 想看清 bind/sendto/recvfrom 到底怎么串起来的从业者。下面按「先跑通、再拆参数、最后说坑」的顺序走一遍。
2. 把 UDP 收发拆成两个工程:Winsock 初始化与套接字创建
VC++6.0 的工程向导和现在的 IDE 差别很大,先把工程骨架搭对,后面才不至于卡在链接错误上。这一章讲清楚为什么 UDP 比 TCP 更适合这个场景、Winsock 在 6.0 里怎么初始化,以及发送端和接收端两个工程各自的最小代码。
2.1 为什么这个场景选 UDP 而不是 TCP
UDP 通讯的核心特征是「无连接、不保证到达、保留报文边界」。放到老工控场景里,这三点恰好是优点:设备状态上报丢一帧无所谓,下一帧马上又来;不需要三次握手,发送端启动即发,接收端没起来也不阻塞;每次 sendto 对应一次 recvfrom,报文边界天然保留,解析时不用自己拼包。反过来,如果业务要求「每条指令必须到达且按序」,那就该用 TCP,硬套 UDP 只会把重传和排序逻辑搬到应用层,得不偿失。
选型上还有一个现实因素:VC++6.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,这一点在 VC++6.0 配套的旧版 Winsock 上尤其明显,不重置可能第二轮就报错。缓冲区留一个字节给结束符,避免打印越界。
3. VC++6.0 工程配置:头文件顺序、库链接与字符集
代码能看懂不代表能编译过,VC++6.0 的工程配置有几个固定套路,配错就是满屏错误。这一章把工程层面的配置讲透,包括头文件包含顺序、ws2_32.lib 的链接方式、以及 MFC 工程和控制台工程的差异。
3.1 头文件顺序:winsock2.h 必须在 windows.h 之前
VC++6.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.h | stdafx.h 最前 |
| 库链接方式 | pragma 或 Link 选项卡 | Link 选项卡更稳 |
| 字符集 | 默认多字节 | 注意 CString 与 char 转换 |
| 运行库 | 默认单线程 | 多线程调试版需一致 |
表格里最后一行值得展开:VC++6.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 在网络包里是大端,直接打印就是错的,必须 ntohs;sin_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 重置这两处,代码本身反而没出过问题。从那以后我每次新建 VC++6.0 的网络工程,都先把 stdafx.h 的头文件顺序固定下来,接收循环里 fromLen 的重置写成模板,编译前扫一眼这两处再点 Build。老环境没有现代 IDE 的智能提示兜底,靠的就是把踩过的坑固化成固定动作。希望帮到你。
本文还有配套的精品资源,点击获取