简介:这份资源是面向Windows平台C++开发者与网络编程学习者的MFC网络通信示例工程,聚焦MFC框架下HTTP、FTP及套接字通信的实现思路,适合具备一定C++基础、希望理解MFC如何封装网络API的读者参考。压缩包共66个文件,约4.88MB,以h头文件与cpp源文件为核心,配合dsp、dsw工程文件及opt、plg等编译配置,另有obj、pdb、ilk等调试中间产物和exe可执行程序,便于直接运行与调试。内容围绕CInternetSession、CHttpConnection、CFtpConnection等关键类展开,涉及连接建立、请求发送、数据接收、异常处理与异步操作等环节,并可与Winsock结合实现更底层的TCP/IP通信。已有215人学习,读者可通过分析示例工程理解MFC网络通信的完整流程,积累连接管理与错误处理的实践经验,为构建自己的网络通信应用提供参考。
1. 从一份老工程拆起:MFC 网络通信到底能跑出什么
很多人第一次拿到MFC.rar_MFC_MFC网络通信这种压缩包,第一反应是「这玩意儿还能编译吗」。我拆开看过,里面是两套典型的 MFC 对话框工程:CSocketcli和CSocket,配套.dsp、.dsw、.clw、.rc、ReadMe.txt一应俱全,属于 VC6 时代的经典目录结构。它解决的不是「教你写 MFC」这种泛问题,而是把 MFC 下两条网络通信路线——Winsock 封装类CSocket和 WinInet 封装类CInternetSession——摆成一个能直接打开、编译、抓包观察的实物。适合谁?正在做 Windows 桌面端上位机、需要和嵌入式设备或服务端做 TCP/HTTP 交互的工程师,以及被「mfc 网络通信」这个词搜到、想找一个能跑的最小样例的人。下面我按「工程结构 → 编译 → 通信实现 → 排错 → 进阶」的顺序,把这份资源拆到能复现的程度。
2. 工程结构与编译环境:先让 .dsp 在 VS 里活过来
2.1 两套工程分别是什么
压缩包里其实是两个独立工程,别当成一个。CSocketcli从命名看是客户端侧,CSocket是服务端或对端样例,两边都带Dlg后缀的对话框类和StdAfx预编译头。判断依据是文件清单里CSocketcliDlg.cpp、CSocketDlg.cpp各自成对出现,.clw是 ClassWizard 的类信息库,.aps是资源符号缓存,.ncb是 VC6 的浏览数据库——这些文件的存在说明工程原本是在 VC6 + ClassWizard 工作流下开发的。
先认清哪些文件必须留、哪些可以删。.ncb、.opt、.aps、.clw都是 IDE 生成的中间态,删掉不影响编译,VS 会重建;.dsp/.dsw是工程与工作区定义,必须留;.rc+resource.h是对话框资源,删了界面就没了。
| 文件 | 作用 | 能否删除 |
|---|---|---|
.dsp/.dsw | 工程 / 工作区定义 | 必须保留 |
.rc/resource.h | 对话框与控件资源 | 必须保留 |
.clw | ClassWizard 类库 | 可删,会重建 |
.ncb/.opt/.aps | IDE 缓存 | 可删 |
StdAfx.cpp/.h | 预编译头 | 保留 |
2.2 用 VS 打开 VC6 工程的正确姿势
VC6 的.dsp直接双击在现代 VS 里会触发升级向导,这一步是血泪经验的重灾区:升级会改写工程文件,一旦失败原工程就回不去了。我一般先复制一份整个目录再动手。
# 先备份,再升级,别在原目录上直接开 cp -r MFC_network MFC_network_bak # 用 VS 打开 .dsw,走升级向导,字符集选「使用多字节字符集」升级完成后重点检查三处:项目属性里的「字符集」要设成多字节(MFC 老工程大量用char*和CString的 ANSI 接口,改成 Unicode 会冒出一堆LPCTSTR转换错误);「MFC 的使用」要选「在共享 DLL 中使用 MFC」或「在静态库中使用 MFC」,取决于你目标机器有没有 MFC 运行库;平台工具集如果报f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\dumpcont.cpp这类路径错误,说明是旧版 MFC 源码路径残留,重新生成即可,不影响功能。
2.3 编译报错的常见来源
老工程编译不过,八成不是代码问题,是环境问题。StdAfx.h里如果#include <afxinet.h>缺失,用CInternetSession的代码会报未定义类型;CSocket相关代码需要#include <afxsock.h>,而且必须在AfxWinInit之后调用AfxSocketInit(),否则 socket 初始化失败。这两条是 MFC 网络编程的固定前置,漏一个就是一堆链接错误。
提示:升级向导跑完后,先只编译不运行,把警告级别调到
/W3,老代码里sprintf、strcpy这类不安全函数会刷屏,先忽略,能链接通过再谈运行。
3. CSocket 客户端实现:TCP 连接、收发与阻塞模型
3.1 CSocket 的封装逻辑与选型理由
MFC 的CSocket继承自CAsyncSocket,本质是对 Winsock 的一层 C++ 封装,把socket、connect、send、recv包成成员函数,并挂到 MFC 的消息泵上做阻塞式调用。选它的理由很直接:在对话框程序里写 TCP 客户端,用CSocket比裸 Winsock 少写一半样板代码,而且阻塞调用期间 MFC 会帮你泵消息,界面不会假死——这是它比CAsyncSocket更适合「点一下按钮发一条命令」这种交互场景的地方。
代价是它把网络细节藏进了黑匣子,出问题时不好定位。所以我的习惯是:先用CSocket把功能跑通,抓包确认协议无误,再决定要不要下沉到裸 Winsock。
3.2 建立连接与发送数据的完整代码
下面这段是CSocketcliDlg.cpp里客户端连接与发送的典型写法,我按可复现的方式整理出来:
// 在对话框类里持有一个 CSocket 成员,避免局部变量提前析构 // CSocketcliDlg.h CSocket m_clientSocket; // 连接按钮响应函数 void CCSocketcliDlg::OnBnClickedConnect() { if (m_clientSocket.GetSafeSocketHandle() != INVALID_SOCKET) return; // 已连接,避免重复 connect // 创建流式 socket,AF_INET + SOCK_STREAM 即 TCP if (!m_clientSocket.Create()) { AfxMessageBox(_T("socket 创建失败")); return; } // 连接目标:IP 与端口按实际服务端填写 if (!m_clientSocket.Connect(_T("192.168.1.100"), 6000)) { // 连接失败要主动 Close,否则句柄泄漏 m_clientSocket.Close(); AfxMessageBox(_T("连接服务端失败")); return; } AfxMessageBox(_T("连接成功")); } // 发送按钮响应函数 void CCSocketcliDlg::OnBnClickedSend() { CString strSend = _T("HELLO\r\n"); // Send 返回实际发送字节数,失败返回 SOCKET_ERROR int nSent = m_clientSocket.Send(strSend, strSend.GetLength()); if (nSent == SOCKET_ERROR) { AfxMessageBox(_T("发送失败")); } }逻辑说明:Create()内部调用socket(),Connect()内部是阻塞的connect(),在 MFC 消息泵配合下不会卡死界面。参数上,Connect的第二个参数是端口,服务端监听哪个端口这里就填哪个,两边必须一致。Send的第二个参数是字节数,注意CString在 ANSI 工程下GetLength()返回的就是字节数,Unicode 工程下要乘 2,这也是前面强调字符集的原因。
3.3 接收数据与 OnReceive 回调
CSocket收数据靠重写OnReceive,它由 MFC 在 socket 可读时回调:
// 需要在对话框类里重写 OnReceive,并让 socket 关联到对话框 void CCSocketcliDlg::OnReceive(int nErrorCode) { if (nErrorCode != 0) { CSocket::OnReceive(nErrorCode); return; } char buf[1024] = {0}; int nRecv = m_clientSocket.Receive(buf, sizeof(buf) - 1); if (nRecv > 0) { buf[nRecv] = '\0'; // 追加到显示控件,注意跨线程时要用 PostMessage m_editRecv.SetSel(-1, -1); m_editRecv.ReplaceSel(CString(buf)); } CSocket::OnReceive(nErrorCode); }参数说明:Receive的缓冲区要留一个字节给结束符,sizeof(buf) - 1就是这个意思。nRecv == 0表示对端正常关闭,nRecv == SOCKET_ERROR表示出错,这两种情况都要处理,否则界面会一直显示「已连接」但实际已经断了。OnReceive是在 MFC 的消息线程里回调的,如果要在里面更新控件,直接调用一般没问题,但一旦你把它挪到工作线程,就必须用PostMessage把数据传回主线程,这是 MFC 网络编程里最常见的翻车点之一。
4. WinInet 路线:CInternetSession 与 HTTP/FTP 客户端
4.1 什么时候该用 WinInet 而不是 CSocket
CSocket解决的是「我要自己定协议、自己管连接」的场景;而当你只是要发个 HTTP 请求、拉个网页、传个 FTP 文件,用CInternetSession系列类更省事。它把 HTTP、FTP 的协议细节封装好了,你不用自己拼请求头、解析状态码。压缩包摘要里提到的CHttpConnection、CFtpConnection、CInternetFile就是这条路线的主角。
选型判断很简单:协议是标准的 HTTP/FTP,用 WinInet;协议是自定义的二进制帧,用CSocket或裸 Winsock。别拿CSocket去手写 HTTP,那是给自己找麻烦。
4.2 一个 HTTP GET 的最小实现
void CCSocketcliDlg::OnBnClickedHttpGet() { CInternetSession session(_T("MFCClient"), 1); // 1 = 异步模式 session.SetOption(INTERNET_OPTION_CONNECT_TIMEOUT, 5000); // 5 秒连接超时 CHttpConnection* pHttp = nullptr; CInternetFile* pFile = nullptr; try { // 端口 80,无用户名密码 pHttp = session.GetHttpConnection(_T("www.example.com"), 80); // 第二个参数是请求方式,第三个是路径 pFile = pHttp->OpenRequest(_T("GET"), _T("/index.html")); pFile->SendRequest(); DWORD dwStatus = 0; pFile->QueryInfoStatusCode(dwStatus); // 拿 HTTP 状态码 if (dwStatus == 200) { char buf[4096]; UINT nRead = pFile->Read(buf, sizeof(buf) - 1); buf[nRead] = '\0'; m_editRecv.SetWindowText(CString(buf)); } } catch (CInternetException* e) { TCHAR szErr[256]; e->GetErrorMessage(szErr, 256); AfxMessageBox(szErr); e->Delete(); // 必须 Delete,否则异常对象泄漏 } if (pFile) pFile->Close(); if (pHttp) pHttp->Close(); session.Close(); }逻辑说明:CInternetSession是所有 WinInet 操作的入口,构造时第二个参数传 1 表示异步,传 0 表示同步。SetOption设超时很关键,默认超时可能长达几十秒,界面会像卡死一样。OpenRequest的第一个参数是 HTTP 方法,GET、POST都行。QueryInfoStatusCode拿到的状态码要判断,200 才是正常。异常处理里e->Delete()是必须的,CInternetException是堆上分配的,不删就泄漏——这是 WinInet 路线最容易被忽略的一处。
4.3 FTP 上传下载的要点
CFtpConnection的用法和 HTTP 类似,GetFtpConnection(服务器, 用户名, 密码, 端口)拿到连接后,GetFile下载、PutFile上传、Remove删除、SetCurrentDirectory切目录。参数上要注意 FTP 默认端口 21,被动模式在老防火墙环境下经常连不上,这时可以尝试主动模式。上传下载都是阻塞的,大文件要放到工作线程里做,否则界面会僵住。
注意:WinInet 的会话对象不要跨线程共享,每个线程用自己的
CInternetSession,否则会出现难以复现的句柄冲突。
5. 避坑与排查:老 MFC 网络工程最容易翻的五个地方
5.1 现象:编译通过但一运行就崩在 AfxSocketInit
原因:CSocket依赖 Winsock 初始化,而 MFC 不会自动帮你做。解决:在InitInstance()里、创建主对话框之前调用AfxSocketInit(),返回失败就直接退出。这一步漏了,Create()会返回失败或直接断言。
5.2 现象:连接成功但收不到数据,OnReceive 不触发
原因:CSocket对象是局部变量,函数返回后析构,socket 被关掉了,自然收不到。解决:把CSocket作为对话框类的成员变量,生命周期跟对话框一致。这是新手最常踩的坑,代码看着没错,就是收不到。
5.3 现象:Unicode 工程下中文乱码、发送长度不对
原因:老工程按 ANSI 写的,CString的GetLength()在 Unicode 下是字符数不是字节数,Send的长度参数就错了。解决:项目属性字符集改回多字节,或者显式用CT2A转换后再算字节数。别硬扛 Unicode,老代码改起来成本高。
5.4 现象:升级到新 VS 后报dumpcont.cpp路径错误
原因:这是旧版 MFC 源码路径的残留引用,不是你的代码问题。解决:清理解决方案、删除.ncb/.aps/.opt后重新生成,一般就消失。如果还在,检查项目属性里有没有硬编码的包含路径。
5.5 现象:程序退出时崩溃或句柄泄漏
原因:CSocket、CInternetSession、CInternetFile没按顺序关闭,或者异常对象没Delete。解决:养成「谁创建谁关闭」的习惯,异常分支里也要保证资源释放,用try/catch包住并在catch里清理。MFC 没有 RAII 帮你兜底,全靠手写。
6. 进阶:把这份样例改成能抓包验证的调试工具
跑通只是第一步,真正有价值的是把它变成你能验证协议的工具。我的做法是在Send和Receive两处各加一行日志,把原始字节按十六进制打出来,配合抓包软件对照,协议对不对一眼就能看出来。
// 十六进制打印,方便和抓包结果对照 static void DumpHex(const char* p, int n) { CString s, t; for (int i = 0; i < n; ++i) { t.Format(_T("%02X "), (unsigned char)p[i]); s += t; } TRACE(_T("HEX: %s\n"), s); }在Send前调DumpHex(strSend, strSend.GetLength()),在Receive拿到数据后调一次,输出到 VS 的输出窗口。这样你发出去的每一个字节、收到的每一个字节都有记录,和抓包软件的结果一比对,是粘包、是字节序、还是长度算错,立刻定位。参数上%02X保证每字节两位、不足补零,(unsigned char)转换是为了避免负数被格式化成FFFFFFxx。
再进一步,可以把目标 IP、端口、发送内容做成界面上的输入框,而不是硬编码。这样这份样例就从「一个死工程」变成「一个能改参数、能抓包、能复现问题的调试台」。我一般还会加一个「循环发送」按钮,用来压测对端的粘包处理逻辑——很多嵌入式设备的协议栈问题,就是这么压出来的。
从那以后我每次拿到这种老 MFC 网络工程,都强制先做三件事:备份原目录、确认字符集和AfxSocketInit、在收发两处加十六进制日志。这三步走完,剩下的基本就是填 IP 和端口的事了。希望帮到你。
本文还有配套的精品资源,点击获取