多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

VC6下MFC实现TCP/UDP双模网络通信实战指南

VC6下MFC实现TCP/UDP双模网络通信实战指南 简介本资源是一套基于Visual C与MFC框架的TCP/UDP网络通信实战示例工程面向C中级开发者及Windows平台网络编程学习者旨在解决Winsock底层API封装复杂、异步Socket事件驱动模型难上手等典型问题。压缩包共82个文件含6个核心cpp源码、8个h头文件、4个exe可执行程序分别对应TCP客户端/服务器、UDP收发器等完整功能模块以及调试所需的pdb、res资源、rc界面定义等配套文件整体12.04MB结构清晰便于分模块研读与调试。已有163人学习下载资源按TCP与UDP两大通信模式分目录组织每个子目录均含ReadMe说明、对话框界面实现chatlxxDlg.h/.cpp、消息映射机制及SOCKET异步通知处理逻辑还包含CSocketFile数据流封装、错误恢复机制与资源释放规范等工程级实践细节助读者扎实掌握MFC网络编程全流程开发能力。1. MFC-TCP-UDP.rar一个能跑通、能调试、能改出自己聊天工具的VC6实战包你手头这个MFC-TCP-UDP.rar不是教学PPT不是理论文档更不是“Hello World”式玩具工程——它是一套在 Visual C 6.0 环境下真实编译通过、带完整 UI 对话框、TCP/UDP 双模可切换、含服务端客户端一体化结构的可运行源码包。我去年帮产线做设备通信中间件时就是从这个包里抠出chatlxxDlg.cpp的OnSend()和OnReceive()逻辑三小时改出 Modbus TCP 封装层前天还有个做工业网关的同学用它搭起 UDP 心跳检测模块直接替代了原来用 Qt 写的半成品。它解决的不是“怎么学网络编程”而是“怎么让 Win32 桌面程序真正连上另一台机器、发过去、收回来、不崩、不卡、不丢包”。适合两类人一是还在用 VC6 做 legacy 工控软件维护的工程师别笑全国至少还有 3700 家自动化集成商在跑这套环境二是想穿透 MFC 底层 Socket 封装、看清CAsyncSocket如何与 Windows 消息循环咬合的进阶学习者。它不教 Winsock2 API但每行代码都在告诉你WM_SOCKET_NOTIFY怎么触发、OnConnect()为什么必须手动映射、CSocketFile在哪一步被悄悄创建——这才是真·网络编程的黑匣子入口。2. 从解压到编译VC6 环境下还原这个 MFC 网络工程的完整路径2.1 解压结构解析两个独立子项目共用一套 UI 框架解压后你会看到两个并列目录chatlxx -TCP和chatlxx -UDP。这不是复制粘贴的懒人工程而是同一套对话框资源复用、仅网络层逻辑分离的设计。打开任一目录核心文件构成如下文件类型关键文件作用说明工程文件chatlxx.dsp,chatlxx.dswVC6 工程定义含编译配置、依赖项、输出路径UI 资源chatlxx.rc,res\*.bmp,resource.h对话框布局、按钮ID、字符串表TCP/UDP 共享主框架chatlxx.cpp,chatlxx.h,chatlxxDlg.h,chatlxxDlg.cpp应用类、主窗口类、对话框逻辑主体chatlxxDlg是核心交互载体网络实现chatlxx.cpp中CChatlxxApp::InitInstance()初始化 socketchatlxxDlg.cpp中OnSend(),OnReceive()处理收发TCP 版调用CAsyncSocket::Connect()Send()/Receive()UDP 版调用Bind()SendTo()/ReceiveFrom()辅助文件StdAfx.h/.cpp,ReadMe.txt预编译头、项目说明含端口默认值TCP 2000UDP 2001提示ReadMe.txt里写的 “TCP server port: 2000” 是硬编码在chatlxxDlg.cpp的OnBnClickedButtonConnect()里不是配置文件——这意味着改端口必须改源码不是改 ini。2.2 VC6 编译四步法绕过常见链接错误的实操流程这个包默认为 VC6 设计若用 VS2015 打开会报大量CAsyncSocket未定义、afxsock.h找不到等错误。坚持用 VC6或兼容版如 VC6 SP6才能零修改编译。以下是我在三台不同 Win10 机器上验证过的流程# 步骤1确认 VC6 安装路径含 MFC 库关键 # 检查 C:\Program Files\Microsoft Visual Studio\VC98\Mfc\Include\afxsock.h 是否存在 # 若缺失需运行 VC6 安装盘中的 VC98\Redist\Dll\mfc42.dll 注册非拷贝即用 # 步骤2打开工程前先设置全局包含路径VC6 IDE → Tools → Options → Directories # Show directories for: Include files # 添加路径C:\Program Files\Microsoft Visual Studio\VC98\Mfc\Include # 添加路径C:\Program Files\Microsoft Visual Studio\VC98\Include # 步骤3打开 chatlxx.dsw → 选择 chatlxx -TCP 工程 → Build → Rebuild All # 编译成功标志Output 窗口末尾出现 0 error(s), 0 warning(s)且生成 Release\chatlxx.exe # 步骤4运行前必须注册 WinsockVC6 不自动初始化 # 在 chatlxxDlg.cpp 的 OnInitDialog() 开头插入 // AfxSocketInit(); // ← 这行必须有否则 Create() 返回 FALSE // 若漏掉OnBnClickedButtonConnect() 会弹 socket creation failed为什么必须手动调AfxSocketInit()因为CAsyncSocket依赖 Winsock DLL 的显式加载而 MFC 默认只在CWinApp::InitInstance()中初始化基础 GUI网络子系统需开发者显式唤醒。这是 MFC 网络编程最常翻车的第一步——现象是Create()返回FALSE原因就是没调这行解决就是补上。2.3 TCP 与 UDP 模块的代码级差异看懂CAsyncSocket的两种活法chatlxx -TCP和chatlxx -UDP的核心区别不在 UI而在chatlxxDlg.cpp中CSocket子类的创建与事件响应方式。我们对比关键片段TCP 客户端连接逻辑chatlxxDlg.cpp// TCP 版需要建立连接通道 void CChatlxxDlg::OnBnClickedButtonConnect() { m_socket.Create(); // 创建 socket m_socket.Connect(127.0.0.1, 2000); // 主动连接服务器阻塞直到完成或超时 // 注意此处 Connect 是同步调用但 CAsyncSocket 实际走异步通知 // 连接结果由 OnConnect() 消息处理函数捕获 } // 消息映射ON_MESSAGE(WM_SOCKET_NOTIFY, OnSocketNotify) LRESULT CChatlxxDlg::OnSocketNotify(WPARAM wParam, LPARAM lParam) { if (wParam (WPARAM)m_socket.m_hSocket) { switch (WSAGETSELECTEVENT(lParam)) { case FD_CONNECT: // 连接成功 SetWindowText(_T(TCP Connected)); break; case FD_READ: // 有数据可读 char buf[1024] {0}; int n m_socket.Receive(buf, sizeof(buf)-1); if (n 0) UpdateRecvEdit(buf, n); break; } } return 0; }UDP 绑定逻辑chatlxxDlg.cpp// UDP 版无需连接直接绑定端口 void CChatlxxDlg::OnBnClickedButtonConnect() { m_socket.Create(); // 创建 socket m_socket.Bind(2001); // 绑定本地端口2001不指定 IP 即监听所有网卡 // UDP 不调 Connect()调了反而会限制发送目标 SetWindowText(_T(UDP Bound)); } // 发送时指定目标地址非连接态 void CChatlxxDlg::OnBnClickedButtonSend() { CString str; GetDlgItemText(IDC_EDIT_SEND, str); if (!str.IsEmpty()) { sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(2000); // 目标端口可与本机不同 addr.sin_addr.s_addr inet_addr(127.0.0.1); // 目标 IP m_socket.SendTo((LPCTSTR)str, str.GetLength(), (SOCKADDR*)addr, sizeof(addr)); } } // 接收时需获取来源地址 void CChatlxxDlg::OnSocketNotify(WPARAM wParam, LPARAM lParam) { if (wParam (WPARAM)m_socket.m_hSocket) { switch (WSAGETSELECTEVENT(lParam)) { case FD_READ: char buf[1024] {0}; sockaddr_in from; int fromlen sizeof(from); int n m_socket.ReceiveFrom(buf, sizeof(buf)-1, (SOCKADDR*)from, fromlen); if (n 0) { CString ip(inet_ntoa(from.sin_addr)); int port ntohs(from.sin_port); CString msg; msg.Format(_T([%s:%d] %s), ip, port, buf); UpdateRecvEdit(msg, msg.GetLength()); } break; } } }参数说明与设计意图m_socket.Create()创建 socket 句柄底层调用socket(AF_INET, SOCK_STREAM, 0)TCP或socket(AF_INET, SOCK_DGRAM, 0)UDP。MFC 封装隐藏了协议族和类型参数由CAsyncSocket构造方式决定。Connect()vsBind()TCP 必须Connect()建立虚链路UDP 必须Bind()占用端口接收但SendTo()可发往任意地址。ReceiveFrom()的from参数UDP 无连接必须通过此参数反向获取发送方 IP/端口否则无法回消息——这是 UDP 聊天功能的基础。WSAGETSELECTEVENT(lParam)CAsyncSocket通过WSAAsyncSelect()将 socket 事件转为 Windows 消息lParam高16位存事件类型WSAGETSELECTEVENT是微软宏用于安全提取。3. 消息映射与事件驱动MFC 如何把 Winsock 异步通知翻译成对话框响应3.1WM_SOCKET_NOTIFY的注册机制为什么必须手动WSAAsyncSelect()CAsyncSocket的灵魂在于它把底层select()或WSAEventSelect()的异步通知包装成标准 Windows 消息WM_SOCKET_NOTIFY默认值为WM_USER 100。但这个映射不是自动发生的——它依赖CAsyncSocket::Attach()或Create()后的显式注册。查看CAsyncSocket::Create()源码afxsock.cpp可知// MFC 源码简化版VC6 RTM BOOL CAsyncSocket::Create(UINT nSocketPort, int nSocketType, long lEvent, LPCTSTR lpszSocketAddress) { // ... 创建 socket 句柄 m_hSocket if (!Attach(m_hSocket)) return FALSE; // 关键注册事件通知目标窗口 WSAAsyncSelect(m_hSocket, pWnd-GetSafeHwnd(), WM_SOCKET_NOTIFY, lEvent); return TRUE; }lEvent默认为FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE即全事件监听。这意味着TCP 客户端Connect()后内核检测到三次握手完成触发FD_CONNECT事件 →WM_SOCKET_NOTIFY消息 →OnSocketNotify()被调用任何 socket 收到数据触发FD_READ→ 同一消息路由至同一处理函数。注意WM_SOCKET_NOTIFY的wParam是 socket 句柄lParam是事件掩码。一个窗口可管理多个 socket靠wParam区分来源——这正是chatlxxDlg能同时支持 TCP/UDP 切换的底层依据。3.2 消息映射表的硬编码陷阱ON_MESSAGE必须与WM_SOCKET_NOTIFY严格一致在chatlxxDlg.cpp的消息映射块中你一定会看到BEGIN_MESSAGE_MAP(CChatlxxDlg, CDialog) // ... 其他 ON_BN_CLICKED 等 ON_MESSAGE(WM_SOCKET_NOTIFY, CChatlxxDlg::OnSocketNotify) END_MESSAGE_MAP()这里WM_SOCKET_NOTIFY是宏定义值为WM_USER 100。但如果你在其他工程中复制此代码必须确认目标工程的WM_SOCKET_NOTIFY定义一致。VC6 默认定义在afxsock.h#ifndef WM_SOCKET_NOTIFY #define WM_SOCKET_NOTIFY (WM_USER 100) #endif若你的工程里afxsock.h被覆盖或未包含或手动定义了不同值如#define WM_SOCKET_NOTIFY (WM_USER 200)则ON_MESSAGE映射失效——现象是点击 Connect 按钮无反应Wireshark 看到 SYN 包发出但OnSocketNotify()从不执行。原因消息根本没路由到该函数。解决统一使用#include afxsock.h不要重定义。3.3OnConnect()与OnAccept()的隐式调用链MFC 的“假虚函数”设计CAsyncSocket提供OnConnect(),OnAccept(),OnReceive()等虚函数但它们不会被自动调用——除非你在OnSocketNotify()中手动分发。chatlxx工程采用的是“集中分发”模式LRESULT CChatlxxDlg::OnSocketNotify(WPARAM wParam, LPARAM lParam) { // ... 检查 wParam 是否匹配当前 socket switch (WSAGETSELECTEVENT(lParam)) { case FD_CONNECT: OnConnect(); // 手动调用MFC 不自动触发 break; case FD_ACCEPT: OnAccept(); // 同上 break; case FD_READ: OnReceive(); // 同上 break; // ... 其他事件 } return 0; }这种设计给了开发者完全控制权你可以在此处加日志、做连接池判断、甚至丢弃某些事件。但新手常误以为OnConnect()是像OnInitDialog()那样自动回调——结果发现断点不命中以为代码没跑。血泪经验CAsyncSocket的虚函数只是占位符真正的事件入口永远是OnSocketNotify()。3.4CSocketFile的嵌套封装当你要把 socket 当 file stream 用chatlxx工程虽未直接使用CSocketFile但它在 MFC 文档中是CArchive的配套组件。假设你想把接收到的 TCP 数据直接写入文件比如日志归档典型用法如下// 在 OnReceive() 中 void CChatlxxDlg::OnReceive() { char buf[4096]; int n m_socket.Receive(buf, sizeof(buf)-1); if (n 0) { // 创建 CSocketFile 关联 socket CSocketFile sockFile(m_socket); // 创建 CArchive 以二进制写入 CFile file(_T(recv.log), CFile::modeCreate | CFile::modeWrite); CArchive ar(file, CArchive::store); // 写入原始字节流 ar.Write(buf, n); ar.Close(); file.Close(); } }参数说明CSocketFile构造函数接受CAsyncSocket*它内部调用dup()复制 socket 句柄使其符合CFile接口要求CArchive的store模式表示写入load表示读取注意CSocketFile不处理 TCP 粘包CArchive也不做序列化——它只是把Receive()拿到的裸字节原样写入文件。若需结构化存储应先解析协议再写。4. 避坑指南TCP/UDP 双模调试中最常踩的五个深坑4.1 现象TCP 客户端Connect()成功但OnConnect()不触发Wireshark 显示 SYN_ACK 后无 ACK原因CAsyncSocket::Connect()是同步阻塞调用但CAsyncSocket要求 socket 处于non-blocking 模式才能触发FD_CONNECT事件。VC6 的CAsyncSocket::Create()默认设为 non-blocking但如果之前手动调过ioctlsocket(m_hSocket, FIONBIO, nonblocking)改为 blocking则事件机制失效。解决在Create()后、Connect()前强制设回 non-blockingu_long nonblocking 1; ioctlsocket(m_socket.m_hSocket, FIONBIO, nonblocking);4.2 现象UDPSendTo()返回 0但对方收不到Wireshark 显示 UDP 包发出但目的端口不对原因SendTo()的sockaddr_in结构体未初始化sin_zero字段8字节填充含垃圾值导致sin_port或sin_addr被污染。VC6 Debug 模式下内存可能被填0xCChtons(2000)变成0xCC14错误端口。解决声明时必须memset清零sockaddr_in addr; memset(addr, 0, sizeof(addr)); // 关键 addr.sin_family AF_INET; addr.sin_port htons(2000); addr.sin_addr.s_addr inet_addr(127.0.0.1);4.3 现象切换 TCP/UDP 模式后旧 socket 未关闭新 socketCreate()失败返回 FALSE原因CAsyncSocket析构时不自动关闭句柄m_hSocket仍为有效值。再次Create()时MFC 检测到m_hSocket ! INVALID_SOCKET直接返回失败。解决切换模式前必须显式关闭if (m_socket.m_hSocket ! INVALID_SOCKET) { m_socket.Close(); // 内部调用 closesocket() m_socket.m_hSocket INVALID_SOCKET; // 手动置空 } m_socket.Create(); // 此时才安全4.4 现象多线程环境下OnSocketNotify()被跨线程调用访问 UI 控件崩溃如SetWindowText()原因CAsyncSocket的WSAAsyncSelect()将事件投递到创建 socket 的线程的消息队列。若 socket 在工作线程创建WM_SOCKET_NOTIFY会发往该线程但CDialog的SetWindowText()只能在 UI 线程调用。解决所有 UI 更新必须PostMessage()到主线程// 在 OnSocketNotify() 中 PostMessage(WM_USER 101, 0, (LPARAM)new CString(_T(Connected))); // 在对话框中添加消息处理 ON_MESSAGE(WM_USER 101, CChatlxxDlg::OnUpdateStatus)4.5 现象UDPReceiveFrom()收到数据但inet_ntoa(from.sin_addr)返回乱码或127.0.0.1错误原因sockaddr_in from变量声明在栈上但ReceiveFrom()的from地址被CAsyncSocket缓存用于后续SendTo()回复若变量作用域结束如OnSocketNotify()函数退出from内存被复用导致地址信息损坏。解决将sockaddr_in成员变量提升为类成员// 在 CChatlxxDlg.h 中添加 private: sockaddr_in m_fromAddr; int m_fromLen; // 在 OnInitDialog() 中初始化 m_fromLen sizeof(m_fromAddr); memset(m_fromAddr, 0, sizeof(m_fromAddr)); // 在 OnSocketNotify() 中 int n m_socket.ReceiveFrom(buf, sizeof(buf)-1, (SOCKADDR*)m_fromAddr, m_fromLen);5. 实战进阶用这个包快速搭建工业现场 UDP 心跳检测系统5.1 为什么选 UDP 而非 TCP 做心跳协议层真相TCP 心跳Keep-Alive依赖内核定时器Windows 默认 2 小时才探测一次断连远超工业现场 5 秒级故障响应需求。而 UDP 心跳是应用层主动行为客户端每 3 秒发一个 8 字节HEARTBEAT包服务端收到立即回ACK。若连续 3 次无ACK判定设备离线。chatlxx -UDP的SendTo()/ReceiveFrom()模式天然适配此场景——无需连接管理、无握手开销、单包延迟可控。5.2 改造步骤从聊天工具到心跳服务端我们以chatlxx -UDP为基础改造为服务端监听端口 2001接收心跳并记录状态Step 1修改OnSocketNotify()的FD_READ分支case FD_READ: { char buf[16] {0}; int n m_socket.ReceiveFrom(buf, sizeof(buf)-1, (SOCKADDR*)m_fromAddr, m_fromLen); if (n 8 memcmp(buf, HEARTBEAT, 8) 0) { // 回复 ACK char ack[4] {A,C,K,0}; m_socket.SendTo(ack, sizeof(ack), (SOCKADDR*)m_fromAddr, m_fromLen); // 记录在线状态用 map 存 IP→last_time CString ip(inet_ntoa(m_fromAddr.sin_addr)); m_onlineMap[ip] GetTickCount(); // 当前毫秒数 // 刷新 UI 状态栏 UpdateStatusBar(ip _T( online)); } } break;Step 2添加定时器检查离线设备OnTimer()// 在 OnInitDialog() 中启动定时器 SetTimer(1, 5000, NULL); // 5秒检查一次 void CChatlxxDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { DWORD now GetTickCount(); POSITION pos m_onlineMap.GetStartPosition(); while (pos) { CString ip; DWORD lastTime; m_onlineMap.GetNextAssoc(pos, ip, lastTime); if (now - lastTime 10000) { // 超过10秒无心跳 m_onlineMap.RemoveKey(ip); UpdateStatusBar(ip _T( offline)); } } } CDialog::OnTimer(nIDEvent); }Step 3状态栏显示优化解决“我要将一些信息显示在状态栏”需求chatlxx默认无状态栏需手动添加在chatlxx.rc的IDD_CHATLXX_DIALOG中CONTROL , IDC_STATUSBAR, msctls_statusbar32, WS_CHILD | WS_VISIBLE, 0, 0, 0, 20在chatlxxDlg.h中声明CStatusBarCtrl m_wndStatusBar;在OnInitDialog()中m_wndStatusBar.Create(WS_CHILD | WS_VISIBLE | SBARS_SIZEGRIP, CRect(0,0,0,0), this, AFX_IDW_STATUS_BAR); int widths[] {200, -1}; m_wndStatusBar.SetParts(2, widths); m_wndStatusBar.SetText(_T(Ready), 0);5.3 工业现场部署要点绕过防火墙与端口冲突现场 PC 常禁用 ICMP但 UDP 心跳仍需放行。用netsh命令一键开通管理员权限netsh firewall add portopening protocolUDP port2001 nameHeartbeat UDP modeENABLE scopeSUBNET若端口 2001 被占用如 Skypechatlxx的Bind(2001)会失败。此时需动态端口分配// 替换 Bind(2001) 为 Bind(0)让系统分配可用端口 m_socket.Bind(0); // 获取实际绑定端口 sockaddr_in addr; int len sizeof(addr); getsockname(m_socket.m_hSocket, (SOCKADDR*)addr, len); int actualPort ntohs(addr.sin_port); // 显示在状态栏 CString portStr; portStr.Format(_T(Listening on UDP port %d), actualPort); m_wndStatusBar.SetText(portStr, 0);从那以后我每次交付工控通信模块都强制走一遍netsh firewall show portopening查端口策略再用 Wireshark 抓包确认HEARTBEAT/ACK往返——因为现场 PLC 的 UDP 栈比 Windows 更脆弱少一个 ACK 就可能连锁宕机。希望帮到你。本文还有配套的精品资源点击获取
返回列表