简介:这份资源是面向Windows平台C++开发者与网络编程学习者的MFC网络通信示例工程,聚焦MFC框架下HTTP、FTP及套接字通信的实现思路,适合已具备C++基础、希望深入理解MFC网络类库的读者参考。压缩包共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. 从一份 2000 年代的 MFC 网络通信源码包说起
如果你手头正好有一份MFC.rar_MFC_MFC网络通信,解压后看到CSocketcli和CSocket两个工程目录,别急着双击.dsw就编译。这份包本质上是一套基于 MFC 的 CAsyncSocket/CSocket 封装示例,包含客户端和服务端两个独立工程,配套.clw、.ncb、.opt、.aps这些 VC6 时代的工程辅助文件。它解决的不是“怎么调 HTTP API”这种上层问题,而是把 Winsock 的阻塞/非阻塞模型、消息驱动接收、连接状态管理塞进 MFC 的文档-视图框架里。适合两类人:一是维护老工控上位机、需要看懂既有 CSocket 派生类逻辑的;二是想从 MFC 侧理解 TCP 粘包、FD_READ 触发时机、OnReceive 重入这些底层细节的。如果你只想要一个能跑通的 demo,这份包够用;但如果你指望它直接支持蓝牙或现代 TLS,那得自己补。
2. 拆包与工程结构:CSocketcli 和 CSocket 到底谁连谁
2.1 两个工程的角色划分与文件对应关系
解压后根目录下有两套并列的工程文件:CSocketcli.dsw/CSocketcli.dsp和CSocket.dsw/CSocket.dsp。从命名习惯看,CSocketcli是客户端(client 缩写),CSocket是服务端。每个工程都带Dlg后缀的对话框类文件,说明两者都是基于对话框的 MFC 应用,不是单文档/多文档。CSocketcliDlg.cpp和CSocketDlg.cpp是 UI 逻辑与网络逻辑的粘合层,CSocket.cpp和CSocketcli.cpp则是CWinApp派生类,负责应用初始化。
.clw是 ClassWizard 的类信息数据库,.ncb是 VC6 的浏览数据库,.opt保存工作区选项,.aps是资源符号的二进制缓存。这些文件在 VS2010 之后已经不再使用,但保留它们能帮你还原当年开发者的类视图和资源 ID 映射。ReadMe.txt通常只有几行,别指望有详细文档。res目录下是图标和对话框资源脚本。
提示:如果你用 VS2019/2022 打开,会提示升级工程。升级前先复制一份原始包,因为
.dsp转.vcxproj后字符集和 MFC 版本会变,老代码里的CString到char*隐式转换可能直接报错。
2.2 用 VS2022 打开旧工程:迁移步骤与字符集修正
直接双击.dsw在 VS2022 里会走“重定向项目”向导。我一般会先手动改两处再升级:一是把CSocketcli.dsp里的#define _MBCS改成_UNICODE和UNICODE,二是把CString相关转换处补上_T()或CT2A。但更稳妥的做法是保留多字节字符集,因为老代码里大量sprintf、strcpy直接操作char数组,强行转 Unicode 会引入一堆C4996和类型不匹配。
# 在 VS2022 开发者命令行里,先备份再升级 xcopy /E /I /Y MFC_original MFC_backup devenv CSocketcli.dsw /upgrade升级完成后,检查项目属性 → 高级 → 字符集,设为“使用多字节字符集”。然后在stdafx.h里确认#include <afxsock.h>存在,这是 MFC 套接字类的头文件。如果编译报CAsyncSocket未定义,就是漏了这个头。
// stdafx.h 中必须包含的顺序 #include <afxwin.h> #include <afxext.h> #include <afxsock.h> // 少了这行,CSocket 全部报未定义逻辑说明:afxsock.h必须在afxwin.h之后包含,否则CAsyncSocket的基类CObject还没声明。参数上,如果你用的是 VS2022 自带的 MFC 库,_WIN32_WINNT至少设为0x0601,否则getaddrinfo相关封装会缺失。
2.3 编译前必须检查的预处理器与链接库
打开项目属性 → C/C++ → 预处理器,确认_AFXDLL已定义(动态链接 MFC)或未定义(静态链接)。如果原工程是静态链接,你机器上又没装对应版本的静态库,会报nafxcw.lib找不到。链接器 → 输入 → 附加依赖项里,ws2_32.lib是必须的,因为 MFC 的 CSocket 底层还是调 Winsock 2。
// 在 CSocketcli.cpp 的 InitInstance 里,必须有这一句 if (!AfxSocketInit()) { AfxMessageBox(_T("Winsock 初始化失败")); return FALSE; }AfxSocketInit()内部会加载ws2_32.dll并填充WSADATA。如果返回 FALSE,常见原因是系统LSP损坏或安全软件拦截。参数上,它默认使用 Winsock 2.2,不需要你手动调WSAStartup。但如果你在InitInstance之前就创建了CSocket对象,会直接崩,因为 Winsock 还没初始化。
3. CSocket 派生类的连接、发送与接收:从 OnConnect 到 OnReceive 的完整链路
3.1 客户端连接建立:Connect 调用与 OnConnect 回调
在CSocketcliDlg.cpp里,通常会有一个“连接”按钮的处理函数。典型写法是派生一个CClientSocket类继承CSocket,然后重写OnConnect、OnReceive、OnClose。连接调用是异步的,Connect返回 TRUE 只代表请求已提交,不代表连接成功。
// ClientSocket.h class CClientSocket : public CSocket { public: virtual void OnConnect(int nErrorCode); virtual void OnReceive(int nErrorCode); virtual void OnClose(int nErrorCode); }; // ClientSocket.cpp void CClientSocket::OnConnect(int nErrorCode) { if (nErrorCode == 0) { // 连接真正成功,可以在这里发第一条数据 CString strMsg = _T("HELLO_SERVER\r\n"); Send(strMsg, strMsg.GetLength()); } else { // 连接失败,nErrorCode 是 WSA 错误码 TRACE(_T("Connect failed: %d\n"), nErrorCode); } CSocket::OnConnect(nErrorCode); }逻辑说明:OnConnect是 MFC 的消息映射回调,由CSocketWnd内部窗口接收FD_CONNECT后触发。参数nErrorCode为 0 表示成功,非 0 时常见值有WSAECONNREFUSED(10061,服务端没开)、WSAETIMEDOUT(10060,网络不通)。注意不要在OnConnect里做耗时操作,否则会阻塞 MFC 的套接字消息泵。
3.2 服务端监听与接受:Listen、OnAccept 和 CSocket 对象生命周期
服务端CSocketDlg.cpp里一般会有一个CListenSocket继承CSocket,重写OnAccept。Listen调用后,OnAccept在有新连接时触发。关键坑在于:Accept必须传入一个已构造但未连接的CSocket对象,而且这个对象的生命周期要覆盖整个通信过程。
// ListenSocket.h class CListenSocket : public CSocket { public: virtual void OnAccept(int nErrorCode); CClientSocket* m_pClient; }; // ListenSocket.cpp void CListenSocket::OnAccept(int nErrorCode) { if (nErrorCode == 0) { m_pClient = new CClientSocket(); if (Accept(*m_pClient)) { // 接受成功,m_pClient 进入通信状态 } else { delete m_pClient; m_pClient = nullptr; } } CSocket::OnAccept(nErrorCode); }逻辑说明:Accept内部会调用WSAAccept,把新连接的套接字句柄绑定到m_pClient上。参数上,Accept是阻塞的,但在 MFC 的消息驱动模型里,它只在OnAccept回调中被调用,所以不会卡 UI。注意m_pClient不要用栈对象,因为OnAccept返回后栈就销毁了,后续OnReceive会访问野指针。
3.3 数据收发与粘包处理:OnReceive 里的缓冲区管理
OnReceive触发时,数据已经在协议栈缓冲区里。Receive调用一次不一定能读完所有数据,而且 TCP 是字节流,没有消息边界。老代码里常见错误是假设一次Receive就是一条完整消息。
void CClientSocket::OnReceive(int nErrorCode) { if (nErrorCode == 0) { char szBuf[4096]; int nRead = Receive(szBuf, sizeof(szBuf) - 1); if (nRead > 0) { szBuf[nRead] = '\0'; m_strRecvBuf += szBuf; // 追加到成员缓冲区 // 按分隔符拆包 int nPos; while ((nPos = m_strRecvBuf.Find("\r\n")) != -1) { CString strLine = m_strRecvBuf.Left(nPos); m_strRecvBuf = m_strRecvBuf.Mid(nPos + 2); ProcessMessage(strLine); // 处理完整消息 } } else if (nRead == 0) { // 对端关闭 Close(); } else { // nRead == SOCKET_ERROR int nErr = GetLastError(); TRACE(_T("Receive error: %d\n"), nErr); } } CSocket::OnReceive(nErrorCode); }逻辑说明:Receive返回 0 表示对端正常关闭,返回SOCKET_ERROR表示出错。参数上,缓冲区大小建议 4096 或 8192,太小会增加回调次数,太大浪费栈空间。m_strRecvBuf必须是成员变量,不能是局部变量,否则跨回调数据就丢了。拆包逻辑用\r\n作为分隔符,这是文本协议常见做法;如果是二进制协议,得用长度字段。
3.4 错误码与异常处理:CInternetException 和 WSAGetLastError
MFC 的CSocket在出错时不会抛CInternetException,那是CInternetSession系列的。CSocket的错误通过OnConnect、OnReceive等回调的nErrorCode参数传递,或者GetLastError()获取。常见错误码:WSAEWOULDBLOCK(10035)表示非阻塞模式下暂时无数据,不是真错误;WSAECONNRESET(10054)表示对端强制关闭,通常是对端进程崩了。
void CClientSocket::OnClose(int nErrorCode) { if (nErrorCode != 0) { TRACE(_T("Socket closed with error: %d\n"), nErrorCode); } CSocket::OnClose(nErrorCode); // 通知 UI 更新状态 ::PostMessage(m_hWndNotify, WM_SOCKET_CLOSED, 0, 0); }逻辑说明:OnClose在连接关闭时触发,nErrorCode为 0 表示正常关闭。参数m_hWndNotify是主对话框的窗口句柄,用PostMessage而不是SendMessage避免死锁。注意不要在OnClose里delete this,因为 MFC 内部可能还在访问该对象。
4. 避坑与排查:老 MFC 网络代码里最容易翻车的五个点
4.1 现象:编译通过但运行时报“无法解析的外部符号 _WSAStartup@8”
原因:链接器没加ws2_32.lib,或者AfxSocketInit()没调用。解决:项目属性 → 链接器 → 输入 → 附加依赖项加ws2_32.lib;在InitInstance开头调AfxSocketInit()。
4.2 现象:客户端连上后发数据,服务端 OnReceive 不触发
原因:服务端Accept传入的CSocket对象是局部变量,函数返回后对象析构,套接字句柄被关闭。解决:用new在堆上创建,并在OnClose里delete。
4.3 现象:UI 卡死,点按钮没反应
原因:在OnReceive或OnConnect里调用了阻塞的Send或Receive,或者用了CSocket的同步模式。解决:确保所有网络操作都在消息回调里做,不要在主线程里循环Receive。如果必须同步,用CAsyncSocket而不是CSocket。
4.4 现象:中文乱码,收到一堆问号
原因:多字节字符集下,CString到char*转换用了默认的LPCTSTR,但发送时没指定编码。解决:统一用 UTF-8 发送,接收端按 UTF-8 解析。或者双方都约定 GBK,但跨平台会出问题。
4.5 现象:程序退出时崩溃在CSocket::~CSocket
原因:套接字对象在AfxSocketInit之前创建,或者WSACleanup之后才析构。解决:确保所有CSocket对象在InitInstance之后创建,在ExitInstance之前销毁。全局或静态的CSocket对象尤其危险。
5. 进阶:把 CSocket 封装成可复用的二进制协议通道
5.1 从文本协议升级到长度前缀协议
老包里的\r\n分隔适合调试,但传二进制文件或结构体就不行了。我一般会改成 4 字节网络序长度 + 负载。发送时先htonl长度,再发数据;接收时先收 4 字节,解析出长度,再循环收满。
// 发送带长度前缀的消息 bool SendPacket(CSocket& sock, const void* pData, int nLen) { uint32_t nNetLen = htonl(nLen); if (sock.Send(&nNetLen, 4) != 4) return false; if (sock.Send(pData, nLen) != nLen) return false; return true; } // 接收状态机(简化版) enum RecvState { RECV_LEN, RECV_BODY }; RecvState m_state = RECV_LEN; uint32_t m_nBodyLen = 0; char m_szBody[65536]; int m_nRecvPos = 0;逻辑说明:htonl把主机字节序转网络字节序,保证跨平台一致。参数上,长度字段用uint32_t支持最大 4GB 消息,但实际缓冲区别开太大,建议 64KB 以内。接收状态机要处理“一次收到半个长度字段”的情况,所以m_nRecvPos要累加。
5.2 心跳与超时:用 SetTimer 检测死连接
TCP 连接对端拔网线,本地不会立刻收到OnClose。我习惯在对话框里加一个SetTimer(1, 5000, nullptr),每 5 秒发一个心跳包,连续 3 次没收到回应就主动Close。
void CClientSocket::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1) { if (m_nMissedHeartbeat >= 3) { Close(); return; } SendPacket(*this, "PING", 4); m_nMissedHeartbeat++; } }逻辑说明:m_nMissedHeartbeat在收到PONG时清零。参数上,心跳间隔别小于 1 秒,否则老设备可能处理不过来。注意SetTimer的nIDEvent不要和对话框其他定时器冲突。
5.3 验证方法:用 Wireshark 抓包对照 OnReceive 触发次数
编译运行后,用 Wireshark 过滤tcp.port == 你监听的端口,看每次Send是否对应一个 TCP 段。如果发现多个Send被合并成一个段,说明 Nagle 算法生效了。可以在Connect之后设置SetSockOpt(TCP_NODELAY, ...)禁用。对照OnReceive的TRACE输出,确认拆包逻辑没有丢数据。
int nNoDelay = 1; SetSockOpt(TCP_NODELAY, &nNoDelay, sizeof(nNoDelay), IPPROTO_TCP);逻辑说明:TCP_NODELAY禁用 Nagle 合并小包,适合实时控制场景。参数上,IPPROTO_TCP层级,选项值 1 表示禁用。注意禁用后小包增多,网络利用率下降,别在文件传输里用。
从那以后我每次拿到这种老 MFC 网络包,都先复制一份原始目录,再在虚拟机里用 VS2022 升级编译,跑通连接和收发之后才动业务代码。希望帮到你。
本文还有配套的精品资源,点击获取