ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC网络编程实战:基于CAsyncSocket的TCP/UDP聊天工具详解

MFC网络编程实战:基于CAsyncSocket的TCP/UDP聊天工具详解 简介本资源是面向C初学者与Windows网络编程进阶者的MFC实战教学包聚焦TCP/UDP通信开发解决在Visual C环境下基于MFC封装Winsock进行异步网络编程的核心难点。压缩包共82个文件含6个核心cpp源码、8个h头文件、4个exe可执行程序、4个res资源文件及配套调试产物pdb/idb/ilk等完整呈现TCP客户端/服务器与UDP点对点通信双案例工程结构包体大小12.04MB。已有163人学习下载。资源按功能清晰分隔为“chatlxx -TCP”与“chatlxx -UDP”两大模块每个模块均含独立ReadMe说明、对话框界面实现chatlxxDlg.h/.cpp、消息映射机制、CAsyncSocket异步事件处理逻辑及StdAfx预编译支持便于理解MFC网络编程的典型工程组织方式与错误恢复实践。1. MFC-TCP-UDP.rar一个能跑通、能调试、能改出自己聊天工具的VC6实战包你手头刚拿到这个MFC-TCP-UDP.rar解压后看到两套几乎镜像的目录结构——chatlxx -TCP和chatlxx -UDP每个里面都塞满了.cpp、.h、.rc、.dsp、.dsw……第一反应可能是“这玩意儿还能在Win10/Win11上编译VC6的工程现在连VS2019都认不全吧”但恰恰相反——它不是古董标本而是一份“可执行的网络编程教科书”。它用最原始的 MFC CAsyncSocket 封装把 TCP 的三次握手、连接维持、粘包处理以及 UDP 的无连接广播、地址绑定、端口复用这些黑匣子级概念全部摊开在对话框界面上输入IP、点连接、发消息、收回显、断开重连——每一步都有对应的消息响应、错误弹窗、状态栏提示。它不依赖 ATL、不碰 WTL、不调用 .NET Framework纯 Win32 API MFC 消息泵驱动 Socket意味着你能在任何一台装了 Visual C 6.0 或兼容运行库的 Windows 机器上双击chatlxx.dsw就启动调试打断点看OnConnect()是怎么被WM_SOCKET_NOTIFY触发的观察Receive()返回值如何区分 EOF 和阻塞。适合两类人一是想从零理解 Windows 网络编程底层脉络的 C 初学者别急着上 Qt 或 Boost.Asio二是需要快速验证某段 Socket 逻辑、或给老旧工控设备写配套上位机的老工程师——它不炫技但每一行代码都在告诉你“Windows 上 socket 究竟怎么活”。2. 为什么选 CAsyncSocket 而不是 CSocket 或 Winsock 原生 API2.1 CAsyncSocket 是 MFC 对 Winsock 的“最小封装”不是偷懒是可控很多新手一上来就查CSocket觉得“类名带 Socket 肯定更高级”。错。CSocket是CAsyncSocket的派生类但它内部做了隐式线程封装和缓冲管理一旦Send()失败或Receive()返回 0你根本不知道是连接断了、对方关闭了还是数据还没来——它把错误吞掉了。而CAsyncSocket把所有 Winsock 错误码WSAENOTCONN,WSAEWOULDBLOCK,WSAETIMEDOUT原样透传给你配合WSAGetLastError()可以精准定位到第 3 次握手 SYN-ACK 丢包还是 FIN 包没收到。本资源里chatlxxDlg.cpp中OnConnect()的写法就是典型void CChatlxxDlg::OnConnect() { if (!m_socket.Create(0, SOCK_STREAM, FD_CONNECT | FD_READ | FD_CLOSE)) { AfxMessageBox(_T(Socket 创建失败)); return; } if (!m_socket.Connect(m_strIP, m_nPort)) { int nError WSAGetLastError(); if (nError WSAEWOULDBLOCK) { // 异步连接中等待 FD_CONNECT 消息 return; } AfxMessageBox(_T(连接失败) GetWinsockError(nError)); return; } // 同步连接成功 m_bConnected TRUE; UpdateStatus(_T(已连接)); }提示FD_CONNECT | FD_READ | FD_CLOSE这三个事件标志是关键。FD_CONNECT告诉系统“我要等连接结果”FD_READ表示“有数据来了喊我”FD_CLOSE是“对方断开了通知我”。漏掉任何一个你的对话框就会卡死或收不到消息。2.2 TCP 与 UDP 的工程结构差异不是代码多寡而是状态机设计逻辑你对比chatlxx -TCP和chatlxx -UDP目录会发现.cpp/.h文件名几乎一样但chatlxxDlg.cpp里的核心函数签名完全不同协议关键 Socket 方法是否需Accept()是否需Bind()数据接收方式典型状态变量TCPConnect(),Send(),Receive()服务端必须客户端可省略系统自动分配Receive(buf, len)m_bConnected,m_nTotalReceivedUDPBind(),SendTo(),ReceiveFrom()❌ 不需要✅ 必须否则SendTo失败ReceiveFrom(buf, len, addr, addrLen)m_remoteAddr,m_remotePort这就是为什么 UDP 版本里OnInitDialog()必须先m_socket.Bind(m_nLocalPort)而 TCP 版本只在点击“连接”时才m_socket.Connect()。UDP 没有连接状态所以m_socket实例既是发送端也是接收端TCP 则严格区分客户端主动 Connect和服务端Listen Accept。资源里两个工程共用chatlxx.h但chatlxxDlg.h中CAsyncSocket m_socket;成员变量的行为完全由你调用哪个方法决定——协议差异最终落在函数选择上而不是类继承上。2.3 为什么不用CSocketFileCArchive因为真实项目要自己管粘包和分包摘要里提到CSocketFile和CArchive可用于序列化传输但本资源刻意回避了它。原因很现实CArchive默认用\n或0x00当分隔符而真实聊天消息里可能含换行符、二进制图片头、甚至加密后的乱码。一旦Receive()读到半个 UTF-8 字符如0xE4 0xB8CArchive就直接抛异常。本资源采用固定包头 变长内容的朴素方案// 发送端先发4字节长度再发内容 DWORD dwLen (DWORD)m_strMsg.GetLength(); m_socket.Send((char*)dwLen, sizeof(dwLen)); m_socket.Send(m_strMsg, dwLen); // 接收端先收4字节长度再按长度收内容 if (m_nHeaderReceived sizeof(DWORD)) { // 收包头 int nRet m_socket.Receive(m_szHeader m_nHeaderReceived, sizeof(DWORD) - m_nHeaderReceived); if (nRet 0) m_nHeaderReceived nRet; if (m_nHeaderReceived sizeof(DWORD)) { memcpy(m_dwMsgLen, m_szHeader, sizeof(DWORD)); m_nBodyReceived 0; } } else { // 收正文 int nRet m_socket.Receive(m_szBody m_nBodyReceived, m_dwMsgLen - m_nBodyReceived); if (nRet 0) m_nBodyReceived nRet; if (m_nBodyReceived m_dwMsgLen) { // 组包完成 CString strMsg(m_szBody, m_dwMsgLen); AddMsgToLog(_T([接收]) strMsg); m_nHeaderReceived m_nBodyReceived 0; } }注意m_nHeaderReceived和m_nBodyReceived是对话框类的成员变量不是局部变量。这是异步 Socket 的铁律——Receive()可能一次只读到 1 字节你必须用成员变量记住当前收了多少下次OnReceive()再续上。忘了清零消息就永远拼不全。3. 编译前必做的三件事VC6 兼容性、运行库、项目配置3.1 VC6 工程在 VS2019/VS2022 上打开别试直接用 VC6 或 VC2008 Expresschatlxx.dsw是 VC6 的工作区文件.dsp是 VC6 的工程文件。VS2019 打开会报错“无法加载项目缺少平台工具集 v60”。强行转换会丢失#include afxwin.h的路径、CAsyncSocket的消息映射宏ON_MESSAGE(WM_SOCKET_NOTIFY, OnSocketNotify)也会失效。正确做法是下载 Microsoft Visual C 6.0官方已停止支持但可从可信镜像站获取 ISO或退而求其次用 Visual Studio 2008 Express支持v90工具集且仍保留CAsyncSocket完整实现绝对不要用 VS2015——CAsyncSocket在 v140 工具集中已被标记为 deprecatedOnMessage()映射机制彻底重构。3.2 运行库不是装个 “Microsoft Visual C Redistributable” 就完事VC6 编译的程序依赖msvcrt.dllVisual C 6.0 运行时而 Win10/Win11 默认只带msvcr120.dllVS2013、msvcr140.dllVS2015。直接双击Release/chatlxx.exe会弹窗“找不到 msvcrtd.dll” 或 “应用程序无法正常启动0xc000007b”。解决方法只有两个开发机安装 VC6 时勾选 “Install Microsoft Foundation Classes” 和 “Install Visual C Runtime Libraries”目标机将C:\Program Files\Microsoft Visual Studio\VC98\Redist\Dlls\下的msvcrt.dll注意不是msvcrtd.dll那是 Debug 版复制到chatlxx.exe同目录并在cmd中执行regsvr32 /s msvcrt.dll提示msvcrt.dll是系统级 DLLWin10 1809 默认禁止用户覆盖。若注册失败需用Dependency Walker检查chatlxx.exe实际依赖的是msvcrt.dll还是msvcp60.dll后者也需一并复制。3.3 工程配置关键参数字符集、预编译头、MFC 使用方式VC6 默认用 ANSI 字符集但现代 Windows API如GetWindowTextW要求 Unicode。本资源未启用 Unicode所以CString默认是char*_T(xxx)展开为xxx。若你手动改成 Unicode在Project → Settings → General中勾选 “Use Unicode libraries”在C/C → Preprocessor中定义UNICODE和_UNICODE但必须同步修改所有CString操作m_strIP.GetLength()→m_strIP.GetLength()仍可用但sprintf(buf, %s, m_strIP)会崩得用_stprintf_s更稳妥的做法是保持 ANSI用MultiByteToWideChar()在调用Connect()前转 IP 地址。4. 避坑TCP 连接卡死、UDP 收不到、状态栏不更新的五个血泪现场4.1 现象TCP 客户端点击“连接”后界面冻结状态栏一直显示“正在连接…”原因CAsyncSocket::Connect()在非阻塞模式下返回FALSE时你没检查WSAGetLastError() WSAEWOULDBLOCK而是直接认为失败并弹窗导致后续OnConnect()消息 never 被触发。解决严格按 2.1 节代码处理WSAEWOULDBLOCK并在OnSocketNotify()中监听FD_CONNECTLRESULT CChatlxxDlg::OnSocketNotify(WPARAM wParam, LPARAM lParam) { switch (WSAGETSELECTEVENT(lParam)) { case FD_CONNECT: if (WSAGETSELECTERROR(lParam) 0) { m_bConnected TRUE; UpdateStatus(_T(已连接)); } else { AfxMessageBox(_T(连接失败) GetWinsockError(WSAGETSELECTERROR(lParam))); } break; } return 0; }4.2 现象UDP 版本发送消息后对方收不到Wireshark 显示源端口是 0原因SendTo()前未调用Bind()或Bind()时端口为 0让系统随机分配但SendTo()传入的sockaddr_in中sin_port未用htons()转换字节序。解决UDP 必须显式Bind()且SendTo()的地址结构体要完整初始化sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(m_nRemotePort); // 关键必须 htons() addr.sin_addr.s_addr inet_addr(m_strIP); m_socket.SendTo(m_strMsg, m_strMsg.GetLength(), (SOCKADDR*)addr, sizeof(addr));4.3 现象TCP 服务端Accept()后客户端发消息服务端OnReceive()里Receive()返回 0原因CAsyncSocket的Receive()在对方closesocket()后返回 0表示连接关闭但你把它当“收到空消息”处理继续AddMsgToLog()导致 UI 卡住。解决Receive()返回 0 时必须调用Close()并重置状态int nRet m_socket.Receive(m_szBuf, sizeof(m_szBuf)-1); if (nRet 0) { m_socket.Close(); // 主动关闭 m_bConnected FALSE; UpdateStatus(_T(对方已断开)); return; }4.4 现象状态栏CStatusBarCtrl文字不更新SetPaneText()无效原因CStatusBarCtrl的 pane 数量未在OnCreate()中设置或SetPaneText()传入的 pane ID 超出范围。本资源chatlxxDlg.cpp中OnInitDialog()调用了m_wndStatusBar.Create(...)但没调m_wndStatusBar.SetIndicators()。解决在OnInitDialog()末尾添加UINT indicators[] { ID_SEPARATOR, ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL }; m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)); // 然后才能用 SetPaneText(0, _T(就绪));4.5 现象Win11 上双击chatlxx.exe闪退事件查看器报 “Application Error: faulting module msvcrt.dll”原因Win11 默认启用 “Control Flow Guard (CFG)” 保护而 VC6 编译的代码未适配 CFG 指针校验。解决两种办法任选其一用EditBin.exeVC6 自带关闭 CFGeditbin /cfg: Release\chatlxx.exe或在 Win11 组策略中禁用仅限测试环境计算机配置 → 管理模板 → 系统 → Mitigation Options → Control Flow Guard → 设置为“已禁用”5. 把 UDP 聊天升级成局域网广播探测器三步改造实战5.1 改造目标让 UDP 客户端不仅能发给指定 IP还能向255.255.255.255广播自动发现同一网段的在线服务端原始 UDP 版本只支持单播SendTo()指定 IP但工业现场常需设备自发现。广播只需两处改动步骤 1启用 SO_BROADCAST 选项在OnInitDialog()的m_socket.Create()后立即设置BOOL bBroadcast TRUE; m_socket.SetSockOpt(SO_BROADCAST, bBroadcast, sizeof(BOOL), SOL_SOCKET);步骤 2构造广播地址将m_strIP输入框默认值改为255.255.255.255并在发送前判断sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(m_nRemotePort); if (m_strIP _T(255.255.255.255)) { addr.sin_addr.s_addr INADDR_BROADCAST; // 关键不能用 inet_addr(255.255.255.255) } else { addr.sin_addr.s_addr inet_addr(m_strIP); } m_socket.SendTo(m_strMsg, m_strMsg.GetLength(), (SOCKADDR*)addr, sizeof(addr));注意INADDR_BROADCAST是宏定义0xFFFFFFFF而inet_addr(255.255.255.255)在某些 Winsock 版本下返回0xFFFFFFFF但语义不明确必须用标准宏。步骤 3服务端监听广播关键原始 UDP 服务端Bind()绑定的是INADDR_ANY0.0.0.0它能收单播也能收广播。但若你希望服务端只响应广播可在OnReceiveFrom()中加过滤CString strSenderIP; strSenderIP.Format(_T(%d.%d.%d.%d), (BYTE)addr.sin_addr.S_un.S_un_b.s_b1, (BYTE)addr.sin_addr.S_un.S_un_b.s_b2, (BYTE)addr.sin_addr.S_un.S_un_b.s_b3, (BYTE)addr.sin_addr.S_un.S_un_b.s_b4); if (strSenderIP _T(255.255.255.255)) { // 是广播包回复 ACK SendTo(_T(ACK), 3, addr); }5.2 验证广播是否生效用netsh interface ip show addresses查本机 IP再用 Wireshark 过滤udp.dstport 你的端口打开 Wireshark设置捕获过滤器udp.port 5000 (ip.dst 255.255.255.255 || ip.src 192.168.1.100)把192.168.1.100换成你本机 IP启动客户端点击“发送”应看到UDP包目的 IP 为255.255.255.255TTL128Length20消息长度。若没看到检查防火墙是否阻止了 UDP 广播Win10/11 默认阻止。5.3 进阶技巧用gethostbyname()替代硬编码 IP实现主机名自动解析很多现场设备只配主机名如PLC-001不配 IP。在OnConnect()中替换inet_addr()hostent* pHost gethostbyname(m_strIP); if (pHost NULL) { AfxMessageBox(_T(DNS 解析失败)); return; } in_addr addr; addr.s_addr *(u_long*)pHost-h_addr_list[0]; m_socket.Connect(addr, m_nPort);注意gethostbyname()是阻塞调用会卡 UI。真实项目中应起独立线程解析或改用getaddrinfo()但 VC6 不支持需升级到 VC2008。从那以后我每次调试网络程序都强制走一遍netsh interface ip show addresses Wireshark 抓包 telnet ip port三连验证——不是信代码是信链路。哪怕只是改了个端口号也要确认netstat -an | findstr :5000里有没有LISTENING。这份MFC-TCP-UDP.rar的价值不在它多新潮而在它把每个环节的“信不信”都交到你手上。希望帮到你。本文还有配套的精品资源点击获取
返回列表