简介:这是一份基于微软基础类库开发的文件传输协议客户端项目压缩包,面向初学者,帮助理解在视窗环境下利用类库实现网络文件交换的基本流程。压缩包共二十八份文件,整体约一点八三兆字节,除了源程序头文件与实现文件、工程配置文件,还包含编译生成的中间文件、资源定义文件以及可直接运行的程序,目录结构完整,适合对照源码与运行效果逐步学习。项目覆盖了对话框界面设计、网络会话连接、文件传输协议登录指令、文件上传下载、多线程后台处理与异常捕获等关键知识点,并涉及主动与被动两种传输模式的区别,描述中已按模块列出可深入钻研的要点。目前已有两百一十七人浏览学习,适合作为入门实践,也可在后续功能扩展时参考其代码组织方式。
1. 从 FTP.rar 到能用的 MFC FTP 客户端,这个标题到底在说什么
我经常看到有人从技术群里存下「FTP.rar_MFC FTP_ftp客户端mfc」这种资源包,解压后是一个老式 MFC 对话框工程,能连 FTP、能列目录、能传文件,但一换到真实服务器就开始翻车:中文文件名乱码、下载到一半卡死、换台机器编译不过。这篇笔记想解决的,就是把这个标题里的东西变成你自己的工具:先搞清楚 FTP 客户端在 MFC 里是怎么运转的,再照着代码在真机上联调一遍,最后把参数和坑都填上。适合手里已经有一个 FTP 源码包却改不动的人,也适合想用 MFC 快速做个公司内部 FTP 小工具、又不想从零写 Socket 的桌面开发从业者。
2. FTP 客户端从建立连接到第一次拿到文件:控制链路、CInternetSession 选型与最小连接
2.1 FTP 不是「一个连接」:控制连接与数据连接是怎么回事
FTP 和 HTTP 最大的区别是它有两条链路。一条是控制连接,默认走 21 端口,用来传指令,比如 USER、PASS、LIST、RETR、STOR;另一条是数据连接,用来传目录列表和文件内容。这两条链路的建立顺序和方向,决定了连接能不能通。
常见的说法是主动模式(PORT)和被动模式(PASV)。主动模式下,客户端把自己的 IP 和端口告诉服务器,服务器主动来连客户端的这个端口;被动模式下,服务器开放一个临时端口,客户端主动去连。对做客户端的人来说,被动模式通常省心一些,因为主动模式要求客户端机器开放高端口给服务器回连,而很多内网机器和防火墙根本不会给你开这样的端口。这也是为什么很多下载好的源码包在自己电脑上连不上,拿到服务器局域网里却能正常工作的原因之一。
在 MFC 里,这一堆协议细节不需要你自己拼命令去处理。CInternetSession 和 CFtpConnection 会把 PORT/PASV 的协商、响应码解析、数据连接复用都封装掉,你只需要在调用 GetFtpConnection 的时候传一个布尔值,告诉它是主动还是被动。很多新手不理解这两个模式的区别,只在代码里看到个 TRUE/FALSE,结果连不上就瞎改,越改越乱。先分清这条链路,后面排错才能有方向。
2.2 为什么选 CInternetSession 而不是自己写 SOCKET
直接从 Socket 写 FTP 客户端不是不行,但你要处理的东西比想象中多:连接 21 端口、按行读取服务器响应、解析 220/230/331 这样的状态码、组织 USER/PASS/SYST/PWD/TYPE/PASV/LIST/RETR 命令序列、处理 CRLF 分隔符、再维护数据连接的超时和复用。这一套写下来,至少几百行基础代码,还没算异常处理。而 MFC 的 WinINet 封装把这条链路简化成了几个函数调用。GetFtpConnection 负责建连,GetFile/PutFile 负责传输,CFtpFileFind 负责列目录,出错时抛 CInternetException,能拿到人类可读的错误信息。
这个选型也有边界。WinINet 封装的是标准 FTP,对 FTPS(FTP over TLS)和基于 SSH 的 SFTP 支持很弱,企业里要求加密传输的场景,MFC 这套东西就用不了。如果你只是在内网传文件、做数据同步、连公司 FTP 服务器下载气象数据再转格式这类活儿,CInternetSession 完全够用,而且代码量少,几天就能交付。要知道 WININET 方案与直接 SOCKET 方案的核心区别不在于性能,而在于你省下了协议栈的维护成本,换来的是快速交付和容易排查。
2.3 先跑通最小连接:CInternetSession 与 GetFtpConnection 的最小代码
拿到一个源码包,不要先改界面,先写一段最小代码验证 FTP 服务器能不能连上。下面是连接并切换目录的最小示例,通常在对话框的“连接”按钮里跑:
#include <afxinet.h> // 注意:这段代码演示连接,真实项目建议放到工作线程里执行 CInternetSession session; CFtpConnection* pConn = NULL; try { pConn = session.GetFtpConnection( _T("192.168.1.10"), // FTP 服务器地址 _T("ftpuser"), // 用户名 _T("ftp123456"), // 密码 21, // 端口,默认 21 TRUE // bPassive = TRUE,被动模式 ); // 切换目录,验证这个账号对 /data 是否有访问权限 BOOL bRet = pConn->SetCurrentDirectory(_T("/data")); if (!bRet) { // 目录不存在或者没权限,这里往往是权限问题的第一个信号 AfxMessageBox(_T("目录切换失败,请检查账号权限")); } } catch (CInternetException* e) { TCHAR szErr[256] = { 0 }; e->GetErrorMessage(szErr, 256); AfxMessageBox(szErr); e->Delete(); }这段代码的逻辑顺序是:先创建 CInternetSession,它负责整个会话期的连接管理;再调用 GetFtpConnection 建立控制连接,这里会完成用户认证;随后用 SetCurrentDirectory 验证一次真实权限,因为很多客户端只验证了登录,却没验证目录权限。GetFtpConnection 的端口参数传 21 是最常见的,如果你接的是非标准端口,比如 2121,这里传对应数值即可。第五个参数 bPassive 是最值得注意的,默认是 TRUE,意味着被动模式。如果网络环境特殊,改成 FALSE 走主动模式,但要承担防火墙不放行高端口的风险。
参数说明里最容易忽略的是 session 的创建位置。常见做法是在对话框初始化时创建作为成员变量,或在线程内局部创建。切忌在按钮事件里反复创建,那样每一次文件操作都会重新握手,速度慢而且容易被服务器判定为异常连接。这段代码如果在你的环境里跑通了,说明 FTP 链路没有问题,接下来再往工程里加目录枚举和文件传输就顺理成章。
提示:这里没有使用返回值检查 GetFtpConnection 是否成功,因为连接失败时 WinINet 会抛出 CInternetException,所以必须用 try-catch 包住。你只需要在 catch 里把错误信息原样弹出来,连接问题就能看到具体原因。
3. 把 FTP 操作搬进 MFC 工程:线程、目录枚举、上传下载与状态栏显示
3.1 不是所有源代码包都能直接用:先看 FTP 操作跑在哪个线程
网上流传的 MFC FTP 源码包,很大一部分是直接把 GetFile、PutFile 放在按钮的 OnBnClicked 事件里。这种写法在下载小文件时看不出毛病,一旦下载几十 MB 的文件,界面直接卡死,窗口拖动起来都费劲。原因在于 WinINet 的 FTP 操作是阻塞式的,GetFile 返回之前,UI 线程被占住了,消息循环跑不起来。想知道手头的代码有没有这个问题,只需要在按钮事件里找到 GetFile 或 PutFile 调用,看它们的外层有没有 AfxBeginThread 或 CreateThread 包裹。
正确做法是让 FTP 操作全部走工作线程,UI 线程只负责发消息和接收消息。下面是一个标准的线程函数骨架:
UINT FtpWorkThreadProc(LPVOID pParam) { // pParam 里传入对话框指针,线程序号等 CMyFtpDlg* pDlg = (CMyFtpDlg*)pParam; // 在线程内部创建会话,避免与其他线程共享 CInternetSession session; CFtpConnection* pConn = NULL; try { pConn = session.GetFtpConnection( pDlg->m_strHost, pDlg->m_strUser, pDlg->m_strPwd, pDlg->m_nPort, TRUE); BOOL bOk = pConn->GetFile( pDlg->m_strRemoteFile, pDlg->m_strLocalFile, FALSE, // 覆盖已有文件 FILE_ATTRIBUTE_NORMAL, // 普通文件属性 FTP_TRANSFER_TYPE_BINARY, // 二进制传输,避免换行转换 0); // 无论成败都告诉界面线程 pDlg->PostMessage(WM_FTP_TASK_DONE, bOk ? 1 : 0, (LPARAM)pConn); return 0; } catch (CInternetException* e) { e->GetErrorMessage(pDlg->m_szErr, 256); pDlg->PostMessage(WM_FTP_TASK_ERROR, 0, 0); e->Delete(); } return 0; }这段代码的关键点有三处。第一,CInternetSession 在线程内部定义,这样每个任务都有独立的会话上下文,多个任务同时跑不会互相干扰。第二,用 PostMessage 而不是 SendMessage 通知界面线程,PostMessage 不会等 UI 处理完才返回,避免工作线程在 UI 忙时被阻塞,进而引发假死。第三,GetFile 的第六个参数 dwContext 传 0,表示不关联上下文回调;如果后续要做进度条,需要给它一个非零值并配合 EnableStatusCallback 使用。
3.2 用 CFtpFileFind 列目录:文件、文件夹与通配符
目录枚举是 FTP 客户端最核心的功能之一。MFC 里对应类是 CFtpFileFind,它的用法和 CFileFind 很像,但内部封装了 FTP 的 LIST 命令解析。下面这段代码把某个目录下的文件和文件夹全部列出来:
CFtpFileFind finder(pConn); BOOL bContinue = finder.FindFile(_T("/data/*.*")); while (bContinue) { bContinue = finder.FindNextFile(); // 先判断是不是目录,再取文件名、大小和修改时间 if (finder.IsDirectory()) { // 目录项也要显示,很多源码包只处理文件,漏掉子目录 TRACE(_T("[DIR] %s\n"), finder.GetFileName()); } else { ULONGLONG nSize = finder.GetLength(); CString strTime = finder.GetLastWriteTime().Format(_T("%Y-%m-%d %H:%M")); TRACE(_T("[FILE] %s | %llu bytes | %s\n"), finder.GetFileName(), nSize, strTime); } } finder.Close();CFtpFileFind 构造函数接收 CFtpConnection 指针,所以必须在连接成功之后使用。FindFile 支持通配符,_T("/data/.") 会匹配该目录下的所有条目,包括子目录。调用完 FindFile 之后,必须继续调用 FindNextFile 才能获取下一个条目,这个循环模式和操作系统的文件查找一致。GetLength 返回 ULONGLONG 类型,TRACE 格式化时用 %llu,这个细节容易踩坑,很多人在这里用了 %d,打印出来永远是 0。GetLastWriteTime 返回 CTime,能直接拿到格式化的时间字符串,比你自己解析 FTP 的原始日期格式要省事得多。
实际项目里,你大概率需要把这些条目存进一个列表控件。常见做法是定义一个结构体存文件信息,再用循环向 ClistCtrl 插入行。这里有个容易忽略的点:FindFile 返回的条目里包括“.”和“..”,但它们通常被服务器以普通条目的形式返回。如果你发现列表里出现这两个系统目录,不要慌,这是 FTP 服务器的正常行为,过滤掉即可。
3.3 GetFile 与 PutFile 的调用方式:参数含义和进度回传
上传下载是 FTP 客户端的最终目的。MFC 的 CFtpConnection 提供了 GetFile 和 PutFile,光是这两个函数就能覆盖大部分需求。先说 GetFile 的完整签名和关键参数:
BOOL bDownload = pConn->GetFile( _T("/data/2024/result.csv"), // 远程文件路径 _T("D:\\local\\result.csv"), // 本地文件路径 FALSE, // 本地文件存在时是否失败 FILE_ATTRIBUTE_NORMAL, // 创建本地文件时使用的属性 FTP_TRANSFER_TYPE_BINARY, // 传输类型:ASCII 或二进制 1); // dwContext:回调上下文标识第三个参数 bFailIfExists 比较关键,传 TRUE 时,如果本地已有同文件,函数直接返回 FALSE,不会覆盖。很多源码包把这个参数写死成 TRUE,导致用户手动重复下载时永远失败,还以为是文件被占用。第五个参数是传输类型,FTP_TRANSFER_TYPE_ASCII 会在传输过程中做换行转换,适合 .txt、.csv 等纯文本;FTP_TRANSFER_TYPE_BINARY 原样传输,适合压缩包、程序、图片。我的建议是统一用二进制,因为文本文件的换行转换在某些情况下会把 UTF-8 的 BOM 头搞乱。
上传用 PutFile,参数和 GetFile 类似:
BOOL bUpload = pConn->PutFile( _T("D:\\local\\report.txt"), // 本地文件路径 _T("/upload/report.txt"), // 远程文件路径 FTP_TRANSFER_TYPE_BINARY);如果需要进度条,单纯调用 GetFile 是拿不到进度的。WinINet 的进度机制依赖 CInternetSession::EnableStatusCallback 和 dwContext。你需要在创建 session 后调用一次 EnableStatusCallback(TRUE),然后通过重写 CInternetSession 的 OnStatusCallback 或者给 session 设置回调函数来接收进度。由于回调发生在后台线程,不能直接在这里操作 UI 控件,常见做法是把进度信息写进一个共享变量,或者用 PostMessage 通知 UI 线程去刷新。很多人在这里图省事直接更新进度条控件,结果界面闪跳甚至崩溃,原因就是跨线程操作 UI 没有走消息机制。
3.4 把进度和监控信息塞进 MFC 状态栏:不弹窗也能看见进度
FTP 客户端跑起来后,最好的反馈方式不是弹出一堆 MessageBox,而是把当前状态写到窗口底部的状态栏。MFC 的 CStatusBar 本身就支持多窗格,在框架窗口里已经默认建好了。如果你用的是单文档框架,可以用 SetPaneText 直接更新文本:
// 假设 pFrame 是 CMainFrame*,m_wndStatusBar 是框架自带的状态栏 CString strMsg; strMsg.Format(_T("正在下载 %s,已完成 %d%%"), strRemoteFile, nPercent); pFrame->m_wndStatusBar.SetPaneText(0, strMsg);如果你用的是 CDialogEx 对话框程序,窗口没有现成的状态栏,但想在界面上显示“正在传输”这类信息,可以用一个 Static Text 控件代替,效果差不多。需要注意的是 SetPaneText 本身是线程不安全的,工作线程里不要直接调用,要通过 PostMessage 把状态文本发到 UI 线程再更新。这也是我前面强调线程模型的原因:先保证消息通路,再考虑控件更新。
一个完整的 FTP 监控场景其实就是这样:定时调用 CFtpFileFind 枚举远程目录,对比本地文件的时间戳和大小,把变化记录到状态栏或者日志控件里。这套逻辑完全可以用 MFC 的定时器实现,不需要额外引入第三方库,对气象数据同步、日志拉取这一类运维工具来说足够了。
4. 参数与边界:超时、被动模式、传输类型这 3 个必调项
4.1 三个必调参数:超时、被动模式、传输类型的推荐值
从实际联调经验看,FTP 客户端能不能稳定用,往往不是功能代码的问题,而是几个基础参数没调对。下表里这三项几乎在每个项目里都会遇到,直接给出推荐值和理由。
| 参数 | 默认表现 | 常见坑 | 推荐设置 |
|---|---|---|---|
| 连接/读写超时 | WinINet 的默认超时较长 | 服务器宕机时界面长时间无响应 | 连接超时 30 秒,读写超时 60 秒 |
| bPassive(被动模式) | GetFtpConnection 默认 TRUE | 主动模式被防火墙拦截导致下载挂死 | 默认 TRUE,特殊网络再改 FALSE |
| 传输类型 | GetFile 默认 BINARY?实际要看是否显式指定 | ASCIII 传输导致二进制文件损坏 | 统一显式传 FTP_TRANSFER_TYPE_BINARY |
超时参数要通过 CInternetSession::SetOption 设置,注意单位是毫秒。以下几个数值是业内常用的:
// 设置连接超时:30 秒 DWORD dwTimeout = 30000; session.SetOption(INTERNET_OPTION_CONNECT_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); // 设置接收、发送超时:60 秒 DWORD dwRwTimeout = 60000; session.SetOption(INTERNET_OPTION_RECEIVE_TIMEOUT, &dwRwTimeout, sizeof(dwRwTimeout)); session.SetOption(INTERNET_OPTION_SEND_TIMEOUT, &dwRwTimeout, sizeof(dwRwTimeout));如果你的 FTP 服务器在内网,延迟低,可以把连接超时缩短到 10 秒,这样故障反馈更快。但如果你要连的是公网服务器,建议不要小于 15 秒,否则弱网环境下连接很容易被误判失败。这里的逻辑很简单:超时设置的目标不是让程序跑得最快,而是在服务器不可达时让用户早一点看到错误提示,而不是对着一个没有响应的窗口等待。
4.2 功能取舍:单文件下载如何扩展成批量任务
源码包里往往只有单文件下载和上传按钮,真实需求却是批量——比如每天固定时间拉取某个目录下所有新文件。在 MFC 里做批量任务,不要在一个循环里串行下载,那样一旦一个文件卡住,后面全部排队。常见做法是维护一个任务队列,后台线程从队列里取任务,每完成一个,通过 PostMessage 通知 UI 更新列表。批量任务还要注意文件名冲突:下载前先检查本地目录,发现同名的处理策略要在需求阶段定好,覆盖还是改名。
断点续传是另一个高频需求。WinINet 的 GetFile 没有直接提供断点续传参数,这意味着你不能传一个偏移量让服务器从指定位置开始。有两条路可以走:一是文件不大时放弃断点续传,失败后整包重传;二是用 CInternetConnection::FtpCommand 发送原始的 REST 命令,再手动读取数据流写入本地文件的追加模式。第二条路要自己维护命令序列,跳过了 MFC 的封装,代码量会上升,但对于几百 MB 的气象数据这类大文件,这是唯一现实的做法。我通常会在需求评估阶段先问清楚文件大小和网络稳定性:如果单个文件普遍在 50MB 以下,整包重传带来的带宽浪费可以接受,优先用最简单的方案。
4.3 ASCII 与二进制的选择:别让小文件坏得无声无息
传输类型的坑在真实项目中比想象中多。FTP_TRANSFER_TYPE_ASCII 模式下,WinINet 会把 CRLF 转换成客户端的换行符,这个转换对 CSV、TXT 是友好的,但对 .zip、.exe、图片、数据库备份文件就是灾难。最典型的表现是:下载完的压缩包可以打开目录却解压失败,或者要求输入密码,实际是文件字节被改掉。排查这类问题要对比原始文件大小和下载后文件大小,如果大小对不上,八成是传输类型写成了 ASCII。
在富文本文件或含 BOM 的 UTF-8 文件上,ASCII 模式甚至会破坏文件头。所以我的做法是把所有文件都按二进制传输,文本内容到本地后再用专门的编码转换逻辑处理换行。代价是 UTF-8 的 LF 行尾和 Windows 的 CRLF 会保留原样,但这比字节损坏可控得多。源码包里的传输类型如果写死成了 FTP_TRANSFER_TYPE_ASCII,建议改成二进制,并测试一次中文文件名和内容都完整的 CSV 文件。
注意:不要在一个连接上混用不同传输类型后不重连。部分 FTP 服务器的 TYPE 命令切换会让数据连接重置,最稳妥的原则是“一个传输任务用固定传输类型”,不要在批量任务里对每个文件动态切换。
5. 避坑:中文乱码、下载挂死与权限不足的 5 次现场
5.1 中文文件名乱码:列表显示正常,下载却提示文件不存在
现象:用 CFtpFileFind 枚举目录,中文名文件显示正常,但点击下载时服务器返回 550,文件不存在。
原因:很多 FTP 服务器(尤其是 Linux 下的 vsftpd、FileZilla Server)用 UTF-8 存储文件名,而老 MFC 工程如果没有启用 Unicode,CString 内部是 ANSI,调用 GetFile 时传出去的远程路径已经被转换成了本地代码页的字节。服务器拿到后对比文件名,发现字节不一致,自然找不到文件。这与磁盘上的文件名字形看起来一样,实际编码已经不是 UTF-8 的那套字节序列。
解决:第一优先把工程切换成 Unicode 字符集,项目属性 -> 字符集 -> 使用 Unicode 字符集。如果是老代码大量用了 char,可以先用 _T 宏包住字符串处理,再逐步替换 CString 的配套调用。第二如果服务器本身是 GBK 编码(如某些国产 NAS 系统),那要在枚举结果后做一次 MultiByteToWideChar 转换,把 GBK 文件名转成 Unicode 再存进界面列表。第三种情况是服务器端设置了 UTF-8,但客户端没有告诉服务器支持 UTF-8。WinINet 会自动发送 OPTS UTF8 ON,但也有些服务器版本对这条命令支持不好,需要在服务器端确认编码选项。
5.2 能连接、能列目录,下载到一半挂死
现象:连接正常,目录列表正常,开始下载后进度走了一部分就卡住,程序不报错也不退出,像是死锁。
原因:这是典型的主动模式数据连接被防火墙阻断。客户端用 PORT 模式告诉服务器自己的某个端口,服务器主动去连接时,连接包被 Windows 防火墙或者路由器拦截,数据连接建立不起来。控制连接还活着,所以程序看起来一切正常,实际上数据通道是断的。
解决:把 GetFtpConnection 的第五个参数改为 TRUE 走被动模式。被动模式下,数据连接由客户端发起,防火墙通常只拦截入站不拦截出站,问题迎刃而解。如果是服务器限制了被动端口范围,则要在服务器端配置允许的 PASV 端口段,并在防火墙里放行这些端口。我在飞牛 OS 上搭测试服务器时遇到过类似情况,最后就是在服务器配置里指定了 40000-41000 的被动端口并放行,客户端才稳定下来。
5.3 弱口令账号能登录,但目录枚举和下载全部失败
现象:服务器能登录,但目录列表为空,GetLastError 返回的也是通用网络错误,没有明确提示。
原因:这是个很容易被忽略的业务问题,不是技术问题。很多内网 FTP 服务器建了多个账号,存在弱口令或默认口令,比如 admin/123456。这类账号的根目录权限可能被设置成“只能登录、不能查看文件”。WinINet 的 FindFile 在这种权限下返回 FALSE,但不抛 CInternetException,所以外层 try-catch 捕捉不到任何异常,新手会误以为是代码问题。
解决:先用系统自带的命令提示符验证权限,执行 ftp 命令手动登录,然后执行 dir 看看目录列表。如果命令行下也看不到文件,那就是服务器端权限配置的问题,去服务器管理界面把账号的目录读取权限打开,或者指定正确的根目录。这个排查顺序省去了在代码里加日志的时间,因为问题根本不在客户端。
5.4 把 FTP 操作放在 UI 线程:界面卡死与假死
现象:点击“下载”按钮后,窗口只能移动不能点击,任务管理器里看到程序占用 CPU 为 0。
原因:FTP 操作阻塞了 UI 线程的消息循环。GetFile 传输期间,窗口的所有按钮、输入框都没法响应鼠标消息。光标一直在转圈,看起来像崩溃,其实只是消息队列堵住了。
解决:把 FTP 操作放进 AfxBeginThread 启动的工作线程,如本文 3.1 的示例。线程内完成后通过 PostMessage 回传结果。这里尤其注意不要在工作线程里直接调用 UpdateData、SetDlgItemText 这些 UI 函数,哪怕你用了 pDlg 指针。正确的做法是全部走消息机制,只在新消息里更新控件。
5.5 局域网内连接超时,但同一台机器用 FTP 软件能连上
现象:程序报连接超时,但用 FileZilla 客户端或者 XFTP 却能正常连接。
原因:第一是这个工程创建了多个 CInternetSession 而没有释放,导致句柄泄漏,达到上限后新连接无法建立;第二是有第三方安全软件拦截了程序进程的联网行为,但白名单里的传统 FTP 客户端被放行。这两种情况都很玄学,第一反应应该看连接前的 session 创建次数。
解决:全局搜代码,确认 CInternetSession 是否在循环里创建后没有 Delete。每操作完一次 FTP,调用 pConn->Close() 或 delete pConn,并让 session 离开作用域释放。安全软件拦截就更好判断,把编译出的 exe 加入白名单,或者临时关闭安全软件再跑一次连接,基本能定位。
6. 把 FTP.rar 改造成自己的 FTP 工具:迁移顺序与联调验证
6.1 接手源码包的第一件事:先编译,再读代码
不要一上来就改逻辑。先把工程打开,确认编译环境能过,再在本地跑一次连接测试。老 MFC 工程最常见的坑是字符集和依赖库版本不匹配,比如在 VS2015 之后编译 VS2008 的工程,会有大量关于 _MBCS 的编译错误。这类问题先统一处理掉。编译通过后的下一步,才是通读代码找关键位置:session 创建在哪个类、连接参数硬编码在哪个文件、文件列表控件的数据结构是什么。我习惯先把这些位置标出来,再开始动功能。
6.2 联调验证的准备:用 Windows IIS 或飞牛 OS 搭一个测试 FTP 服务器
做 FTP 客户端开发,手边最好有一个能随时重置的测试服务器。最常见的做法是 Windows 自带的 IIS FTP 服务,配置路径是控制面板 -> 程序和功能 -> 启用或关闭 Windows 功能 -> Internet Information Services -> FTP 服务器;或者用飞牛 OS 这类国产 NAS 系统,图形界面配置目录权限和用户密码都很直观。联调顺序建议是:先用命令行 ftp 客户端手动操作一遍,确认服务器本身没问题;再用测试代码跑最小连接;最后才测试批量任务。如果不能登录,先查服务器日志,大多数 FTP 服务器都会记录认证失败的原因码,这比反复猜客户端代码要快得多。
6.3 我现在的做法:新项目不再从零写 MFC FTP,老项目维护时只改必要部分
接手维护老 MFC FTP 项目时,我会坚持一个习惯:凡是源码包里下载的代码,先加日志再跑通一次,再改逻辑。先加日志不是浪费时间,而是在为你后续的排查做一个黑匣子。FTP 类代码最怕出了问题不知道在哪个环节,日志里记录连接参数、被动模式、传输类型,以及每个操作的耗时,基本能把问题缩小到具体位置。现在 AI 辅助工具也能帮上忙,对 MFC 老代码的语法理解已经比想象中准确,我经常用它生成代码片段再手工核对。至于桌面 FTP 工具选 MFC 还是 Qt,我的判断是:如果这个工具要长期维护、界面还要现代化,新项目直接用 Qt 或者 C# 更省心;如果它只是公司内部临时用的辅助工具,并且团队熟悉 MFC,那继续用 MFC 是成本最低的路径,不值得为了换框架而换框架。这也是我常说的,不要因为一个技术老就否定它,关键看你手里的资源是哪些。希望帮到你。
本文还有配套的精品资源,点击获取