简介这份资源是面向Windows平台C开发者与网络编程学习者的MFC网络通信实例工程聚焦MFC框架下HTTP、FTP及套接字通信的实现方式。压缩包共66个文件约4.88MB以h头文件与cpp源文件为核心配合dsp、dsw工程文件、rc资源脚本、ico图标及exe可执行程序另含obj、pdb、ncb等编译调试中间文件完整保留了Visual C项目的原始结构。内容围绕CInternetSession、CHttpConnection、CFtpConnection等关键类展开涉及连接建立、请求发送、数据接收、CInternetException异常处理与异步操作等环节并可与Winsock结合实现底层TCP/IP通信。已有215人学习适合希望从示例代码入手理解MFC网络通信机制、对照调试并迁移到自建应用的读者参考。1. MFC 网络通信从一份 rar 包说起把 C 老框架接上 TCP 链路很多人第一次拿到MFC.rar_MFC_MFC网络通信这种命名的压缩包第一反应是「这年头谁还用 MFC 写网络程序」。但真到工控上位机、医疗设备配套软件、产线测试工具这些场景里MFC 依然是绕不开的选项——客户机器上装的是 VC 运行库界面要原生 Win32 风格交付物就是一个 exe没有 Python 环境、没有 Node、没有浏览器内核。这时候用 MFC 做网络通信反而是最稳的路子。这个标题背后要解决的核心问题很具体在 MFC 对话框程序里怎么把 TCP 客户端/服务端跑起来怎么收发数据不卡界面怎么处理粘包和断线重连。适合两类人一类是刚学完 MFC 编程入门、想给对话框加个联网功能的同学另一类是维护老项目、需要把串口通信改成网口通信的一线工程师。下面按「选型 → 搭骨架 → 收发数据 → 排错 → 进阶」的顺序讲透。2. 选型先立住CAsyncSocket、CSocket 还是裸 WinsockMFC 网络通信的坑一半来自选错 API。MFC 对 Winsock 做了两层封装加上可以直接调原生接口实际有三条路。选错了后面全是血泪经验所以这一章先把选型讲清楚再动手。2.1 三种方案的能力边界对比方案封装程度阻塞行为适用场景主要代价CAsyncSocket薄封装基于 WSAAsyncSelect非阻塞消息驱动单线程多连接、轻量客户端回调里不能做耗时操作CSocket厚封装内部带阻塞消息泵表面阻塞内部抽消息简单客户端、教学demo多线程下容易出玄学问题裸 Winsock无封装自己控制阻塞/非阻塞高吞吐、需要精细控制要自己管生命周期和线程我一般给新项目的建议是单连接客户端用 CAsyncSocket服务端或者需要多线程收发的用裸 Winsock。CSocket 看着最省事但它在工作线程里创建时会依赖消息泵一旦你在AfxBeginThread出来的线程里new CSocket很容易出现收不到OnReceive的情况这就是热词里提到的「mfc静态库中对话框创建失败」那类问题的近亲——本质都是 MFC 的线程状态module state没挂对。2.2 用 CAsyncSocket 搭一个最小 TCP 客户端先看能直接抄的骨架。假设你有一个基于 MFC 的对话框程序工程代码对话框类叫CNetDlg加一个继承自CAsyncSocket的类// MySocket.h #pragma once #include afxsock.h class CMySocket : public CAsyncSocket { public: CMySocket() {} virtual ~CMySocket() {} protected: // 连接成功后被调用 virtual void OnConnect(int nErrorCode); // 有数据到达时被调用 virtual void OnReceive(int nErrorCode); // 连接断开时被调用 virtual void OnClose(int nErrorCode); public: HWND m_hNotifyWnd nullptr; // 用来把消息转发给对话框 };// MySocket.cpp #include pch.h #include MySocket.h void CMySocket::OnConnect(int nErrorCode) { if (nErrorCode 0 m_hNotifyWnd) ::PostMessage(m_hNotifyWnd, WM_USER 100, 0, 0); // 通知UI连接成功 CAsyncSocket::OnConnect(nErrorCode); } void CMySocket::OnReceive(int nErrorCode) { if (nErrorCode 0 m_hNotifyWnd) ::PostMessage(m_hNotifyWnd, WM_USER 101, 0, 0); // 通知UI去收数据 CAsyncSocket::OnReceive(nErrorCode); } void CMySocket::OnClose(int nErrorCode) { if (m_hNotifyWnd) ::PostMessage(m_hNotifyWnd, WM_USER 102, 0, 0); // 通知UI断线 CAsyncSocket::OnClose(nErrorCode); }逻辑说明CAsyncSocket的所有事件都是通过窗口消息投递的所以回调里绝对不能直接操作控件否则就是跨线程访问 UI。正确做法是PostMessage把事件甩给对话框让 UI 线程自己去处理。参数上nErrorCode 0表示正常非 0 要去查 Winsock 错误码表。初始化时别忘了在InitInstance里调用AfxSocketInit()否则所有 socket 操作都会失败BOOL CNetApp::InitInstance() { if (!AfxSocketInit()) { AfxMessageBox(_T(Winsock 初始化失败)); return FALSE; } // ... 其余初始化 }2.3 连接与发送的关键参数连接用Create()Connect()两步注意Create不指定端口时系统自动分配本地端口m_sock.Create(); // 创建 socket本地端口自动分配 m_sock.m_hNotifyWnd GetSafeHwnd(); // 绑定通知窗口 if (!m_sock.Connect(_T(192.168.1.10), 9000)) { int err GetLastError(); if (err ! WSAEWOULDBLOCK) // 非阻塞连接返回这个错误是正常的 { // 真正的连接失败处理错误 } }这里有个必调参数认知Connect在非阻塞模式下几乎总是返回 FALSE错误码是WSAEWOULDBLOCK这不代表失败真正的结果在OnConnect里。很多新手在这里就翻车了以为连不上其实是异步还没完成。发送用Send()返回值是实际发出的字节数可能小于你传入的长度所以大块数据要循环发。3. 收发数据落地粘包、分包与线程安全骨架搭好只是能连上真正让程序可用的是数据收发这一层。MFC 网络通信里 80% 的 bug 都出在这里尤其是粘包和 UI 线程安全。3.1 粘包的本质与定长/分隔符/长度前缀三种解法TCP 是字节流没有消息边界。你发两次 100 字节对面可能一次收到 200 字节也可能分三次收到。这不是 bug是 TCP 的设计。常见解法有三种定长包每条消息固定 N 字节简单但浪费带宽适合协议固定的场景。分隔符用\r\n或特殊字节分隔适合文本协议但数据里不能出现分隔符。长度前缀包头 4 字节存长度后面跟数据最通用二进制协议首选。我一般用长度前缀收数据的缓冲区逻辑这样写// 在对话框类里维护一个接收缓冲区 CByteArray m_recvBuf; void CNetDlg::OnSocketReceive() { char temp[4096]; int n m_sock.Receive(temp, sizeof(temp)); if (n 0) return; m_recvBuf.Append((BYTE*)temp, n); // 先全部追加到缓冲区 while (true) { if (m_recvBuf.GetSize() 4) break; // 连包头都不够 // 解析长度前缀网络字节序转主机字节序 DWORD bodyLen 0; memcpy(bodyLen, m_recvBuf.GetData(), 4); bodyLen ntohl(bodyLen); if (m_recvBuf.GetSize() 4 bodyLen) break; // 包体还没收全 // 取出完整一包 CString payload((char*)m_recvBuf.GetData() 4, bodyLen); HandleOnePacket(payload); // 从缓冲区移除已处理的部分 m_recvBuf.RemoveAt(0, 4 bodyLen); } }逻辑说明核心是「先攒后拆」。每次Receive到的数据无条件追加到m_recvBuf然后循环尝试从缓冲区头部解析出一个完整包解析成功就处理并移除不够就退出等下次。参数上ntohl处理字节序如果你两端都是 Windows 小端其实可以省但跨平台对接时一定要加。3.2 发送端的分包与循环发送发送端对应地要加长度前缀并且处理Send返回值小于预期的情况bool CNetDlg::SendPacket(const CString payload) { DWORD len htonl(payload.GetLength()); CByteArray out; out.Append((BYTE*)len, 4); out.Append((BYTE*)payload.GetBuffer(), payload.GetLength()); payload.ReleaseBuffer(); int total 0; int need (int)out.GetSize(); while (total need) { int n m_sock.Send(out.GetData() total, need - total); if (n SOCKET_ERROR) { int err GetLastError(); if (err WSAEWOULDBLOCK) { Sleep(1); continue; } // 发送缓冲满稍等重试 return false; // 真错误 } total n; } return true; }参数说明WSAEWOULDBLOCK在发送时表示内核发送缓冲区满了不是错误Sleep(1)后重试即可。但要注意如果在 UI 线程里死循环重试界面会卡死所以大流量场景建议把发送放到工作线程或者用OnSend回调驱动。3.3 工作线程里用 MFC 对象的线程状态问题如果你把 socket 放到AfxBeginThread的工作线程里会踩到 MFC 的模块状态坑。工作线程里直接用CString、CSocket这些 MFC 类可能崩溃或者行为异常。解决办法是在线程函数开头挂上模块状态UINT MyThreadProc(LPVOID pParam) { // 关键把 MFC 模块状态挂到当前线程 AFX_MANAGE_STATE(AfxGetStaticModuleState()); // 现在可以安全使用 CString、CSocket 等 MFC 类 CString str _T(hello); // ... 线程逻辑 return 0; }这个宏在静态库和 DLL 场景下尤其重要热词里「mfc静态库中对话框创建失败」很多时候就是漏了这一步。它保证线程能正确访问 MFC 的全局状态和资源句柄。4. 避坑与排查那些让程序半夜崩掉的细节网络程序最怕的是「平时好好的一上产线就断」。下面这几条是我实际项目里踩过的按现象、原因、解决写清楚。4.1 现象程序运行几小时后内存持续上涨原因CString和CByteArray在频繁拼接时反复分配释放加上GetBuffer后忘记ReleaseBuffer或者OnReceive里new出来的对象没delete。热词里的「mfc字符串内存泄漏」多半是这个。解决所有GetBuffer()必须配对ReleaseBuffer()接收缓冲区用成员变量复用不要每次new用_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF)在 Debug 下打开泄漏检测退出时看输出窗口的泄漏报告。4.2 现象界面卡死点关闭没反应原因在 UI 线程里做了阻塞操作比如Connect后死等、Receive用了阻塞 socket、或者发送循环里Sleep太久。解决所有网络 IO 走非阻塞 消息通知确需阻塞的放到工作线程UI 线程只做界面刷新。判断标准很简单——任何可能超过 50ms 的操作都不该在 UI 线程里。4.3 现象断线后重连旧 socket 句柄泄漏原因OnClose触发后没有Close()并销毁 socket 对象直接new一个新的旧的句柄一直占着。解决断线回调里先Close()再delete然后才创建新连接。用一个状态机管理「未连接 / 连接中 / 已连接 / 断开中」四个状态避免重复连接。4.4 现象收到的中文变成乱码原因发送端用CString的GetBuffer()拿的是TCHAR在 Unicode 工程里是宽字符直接当char*发出去对面按 UTF-8 或 GBK 解析就乱了。解决统一编码。要么两端都用 UTF-8发送前WideCharToMultiByte转要么协议里明确用 GBK。别混用混用必乱。4.5 现象getaddrinfo返回失败或卡顿原因热词里的「mfc getaddrinfo」通常是用域名连接时DNS 解析在 UI 线程同步执行网络不好时卡几秒。另外getaddrinfo返回的链表要freeaddrinfo释放忘了就泄漏。解决域名解析放工作线程用完freeaddrinfo如果目标地址固定直接用 IP省掉 DNS 这一环。5. 进阶把通信层抽成可复用模块与自测方法写到这儿一个能跑的 MFC 网络通信程序已经成型。但要让它在多个项目里复用还得做一层抽象并且有一套自己能验证的方法不然每次改协议都要重新调一遍。5.1 把 socket 封装成独立类UI 只订阅事件我的习惯是定义一个INetObserver接口把「连接成功、收到包、断线」三个事件暴露出去对话框实现这个接口。这样 socket 类完全不依赖具体 UI换到控制台程序或者服务里也能用class INetObserver { public: virtual void OnNetConnected() 0; virtual void OnNetPacket(const CString payload) 0; virtual void OnNetClosed() 0; virtual ~INetObserver() {} }; class CNetClient : public CAsyncSocket { public: void SetObserver(INetObserver* obs) { m_obs obs; } bool Start(const CString ip, UINT port); bool SendPacket(const CString payload); protected: virtual void OnConnect(int nErrorCode) override; virtual void OnReceive(int nErrorCode) override; virtual void OnClose(int nErrorCode) override; private: INetObserver* m_obs nullptr; CByteArray m_recvBuf; void DispatchPackets(); };这样对话框只需要m_client.SetObserver(this)然后实现三个回调。协议解析、粘包处理全在CNetClient里UI 层干净。5.2 用回环地址和模拟服务端做自测没有真实设备时怎么验证两个办法。一是用本机回环127.0.0.1自己写一个极简服务端可以用 Python 的 socket 模块几行就够来对发数据# 模拟服务端收到长度前缀包后原样回发 import socket, struct srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.bind((127.0.0.1, 9000)) srv.listen(1) conn, _ srv.accept() while True: head conn.recv(4) if not head: break length struct.unpack(!I, head)[0] body b while len(body) length: body conn.recv(length - len(body)) conn.sendall(head body) # 原样回发验证收发闭环二是写一个压力测试连续发 10 万个小包看内存是否稳定、有没有丢包。这一步能提前暴露缓冲区增长和句柄泄漏问题。5.3 一个具体技巧用 Wireshark 抓包定位协议层问题当两端都觉得自己发对了、但就是收不到时别猜直接抓包。Wireshark 过滤tcp.port 9000看三个东西TCP 三次握手有没有完成、数据有没有真的发出去、长度前缀的值和实际包体是否一致。我遇到过好几次「发送成功但对面收不到」抓包一看是长度前缀算错了多算了 4 字节对面一直在等永远等不到的数据。这个技巧比在代码里加一百行日志都快。最后说个我自己的习惯每次改完通信层先跑一遍「连接 → 发 1000 个包 → 主动断开 → 重连 → 再发 1000 个包」的循环跑够 100 轮再交给测试。这个土办法帮我拦下了绝大多数断线重连和内存泄漏的问题。MFC 网络通信不难难的是把边界情况都想到希望帮到你。本文还有配套的精品资源点击获取