
简介《FTP客户端程序设计》是一份面向网络工程专业的课程设计报告围绕基本FTP客户端程序的完整开发流程展开适合计算机网络专业学生、MFC初学者以及需要完成类似课程设计项目的读者参考。资源包含1个doc文档压缩包整体约151KB内容涵盖题目及要求、系统概要设计、详细实现步骤以及控件、变量、函数清单可直接作为课程设计报告范本。报告重点讲解了FTP协议的基本原理、基于对话框的MFC程序框架、CInternetSession会话管理、CFtpConnection连接建立、CFtpFileFind目录检索等核心知识并具体说明了查询、上传、下载功能对应的事件驱动流程与函数调用关系包括每次操作完成后如何清理会话和连接对象以保证资源有效释放。目前已有140人学习浏览既能帮助读者理解FTP客户端的工作原理又能为MFC网络编程实践提供可参考的实现思路。1. 一份FTP客户端程序设计.doc值不值得读完整代码背后的可用价值拿到这份《FTP客户端程序设计.doc》时我第一反应是“学生大作业”。但翻完附录里的完整代码和设计说明我得重新评价它把一个基于对话框的 MFC 程序从界面布局、控件变量映射、事件响应到 WinInet 连接释放讲完整了而且代码能直接编译成可用工具。如果你是网络工程或计算机专业学生要交 MFC 课程设计如果你被派去维护十几年前的老项目需要快速搞懂 WinInet 封装或者你只是想在内网搭一个图形化上传下载小工具——这份资源都值得拆开看一遍。它的核心价值不在 FTP 协议本身而在“事件驱动 网络会话 对象生命周期”这三件事是怎么串起来的。2. 核心骨架CInternetSession与CFtpConnection、CFtpFileFind的分工和释放顺序2.1 为什么选WinInet而不直接用Winsock这个程序的技术选型有个很现实的理由直接用 Winsock 写 FTP 客户端你得自己拼USER、PASS、CWD、LIST、RETR、STOR这些命令还要处理数据连接和控制连接两条通道以及被动模式下的端口协商。这批代码的工作量在一周课程设计里根本完不成。MFC 的 WinInet 系列类把这些底层细节全部包掉了。CInternetSession负责创建一次 Internet 会话相当于传统 socket 编程里初始化 Winsock 并准备一套连接配置CFtpConnection由会话对象产生封装了与 FTP 服务器的连接提供GetFile、PutFile、Remove这些命令级方法CFtpFileFind则专门负责在连接上做目录检索。三层各管一段错误处理也集中在CInternetException上。用这套方案换来的代价是程序写起来简单但每个操作几乎都是“建会话→建连接→执行→清对象”四步走。原设计里查询、下载、上传三个功能各自独立走一遍这个流程代码重复度很高但非常容易看懂。对课程设计来说这个选择是合理的。2.2 查询动作落成代码OnQuery与ListContent的逐段解释先看查询按钮的事件响应函数。它的任务很单纯把编辑框里的域名、用户名、密码同步到成员变量清空列表框然后调用ListContent()做真正的网络操作。void CFTP_ClientDlg::OnQuery() { UpdateData(TRUE); // 把编辑框内容写到 m_dname / m_user / m_psw while (m_List.GetCount() ! 0) // 清空上次查询留下的列表内容 m_List.DeleteString(0); ListContent(); // 连接服务器并填充列表 }UpdateData(TRUE)是 MFC 对话框里最常见的变量同步手段。这里的TRUE表示从控件读数据到变量如果写FALSE则是反方向。m_List.GetCount()返回列表框当前条目数用循环从 0 开始逐条删除是因为DeleteString每次删除都会让条目重新编号固定删第 0 项是最稳妥的写法。ListContent()是程序的关键函数。原稿的代码在目录检索循环处有一处明显缺陷我先给出修正后的完整版本下一节再展开讲原问题。void CFTP_ClientDlg::ListContent() { CInternetSession *pSession NULL; CFtpConnection *pConn NULL; CFtpFileFind *pFind NULL; // 第一步创建 Internet 会话 pSession new CInternetSession(AfxGetAppName(), 1, PRE_CONFIG_INTERNET_ACCESS); try { // 第二步尝试建立 FTP 连接 pConn pSession-GetFtpConnection(m_dname, m_user, m_psw); } catch (CInternetException *e) { e-Delete(); // 注意MFC 异常对象要用 Delete() 释放不能直接 delete pConn NULL; } if (pConn ! NULL) { m_btn2.EnableWindow(TRUE); // 连接成功上传按钮可用 // 第三步创建文件检索对象查找当前目录任意文件 pFind new CFtpFileFind(pConn); BOOL bContinue pFind-FindFile(_T(*)); while (bContinue) { CString filename pFind-GetFileName(); if (pFind-IsDirectory()) { filename _T([) filename _T(]); } m_List.AddString(filename); bContinue pFind-FindNextFile(); // 用返回值判断是否还有下一个 } pFind-Close(); delete pFind; // 第四步关闭连接并释放对象 pConn-Close(); delete pConn; } delete pSession; }这个函数里有几个参数值得单独说。CInternetSession构造函数的第一个参数AfxGetAppName()返回当前应用程序名WinInet 会把它作为 FTP 客户端的标识字符串发给服务器第二个参数1是上下文 ID在异步操作里用来标识回调来源这里用不上但必须给一个非零值第三个参数PRE_CONFIG_INTERNET_ACCESS表示使用系统预配置的 Internet 访问方式后面避坑章节我会专门讲它带来的问题。GetFtpConnection不是返回NULL表示失败而是直接抛出CInternetException。所以必须用try/catch包住。CFtpFileFind通过FindFile(*)开始检索FindNextFile()返回TRUE表示还有下一个文件。这里最容易犯的错误是把异常判断和普通流程混在一起写下一节展开。2.3 文件检索循环的边界原稿里一处会影响后续功能的错误原稿的循环部分是这样写的while(bcontinue) { filename pfilefind-GetFileName(); ... bcontinue pfilefind-FindNextFile(); if(pfilefind ! NULL) { pfilefind-Close(); pfilefind NULL; } }问题出在循环体末尾FindNextFile()执行完毕后无论是否还有文件pfilefind本身都不是NULL。这个if条件永远成立于是第一次迭代就直接把检索对象Close()掉了。后果是列表中永远只显示第一个文件后续条目全部丢失。正确逻辑应该判断FindNextFile()的返回值也就是bContinue。当它变为FALSE时才说明检索结束此时才需要Close()并释放对象。修正后的写法就是 2.2 节给出的版本循环条件用返回值驱动循环结束后统一释放。这个错误的隐蔽性在于目录里只有一个文件时程序表现完全正常文件一多列表就缺项。很多人会误以为是 FTP 服务器目录权限问题或者通配符写错了实际上纯粹是对象释放时机不对。这也是我在这个章节特意把修正版放前面的原因——照着原稿敲你大概率会在调试上浪费很久。3. 查询到上传下载事件驱动链路与对话框状态机的完整闭环3.1 列表选择事件触发后的控件仲裁程序界面上的按钮和编辑框在不同状态下可用性完全不同。这相当于一个轻量级状态机而切换状态的触发点是列表框的LBN_SELCHANGE事件。void CFTP_ClientDlg::OnSelchangeList1() { // 用户选中文件后禁止修改服务器地址、用户名、密码 m_EDIT1.EnableWindow(FALSE); m_EDIT2.EnableWindow(FALSE); m_EDIT3.EnableWindow(FALSE); // 禁止查询和上传激活下载 m_btn1.EnableWindow(FALSE); // IDC_BUTTON1查询 m_btn2.EnableWindow(FALSE); // IDC_BUTTON2上传 m_btn3.EnableWindow(TRUE); // IDC_BUTTON3下载 }控件的启用状态可以看作一张状态表状态三个编辑框查询按钮上传按钮下载按钮连接前初始状态可用可用可用禁用列表选中文件后禁用禁用禁用可用上传/下载执行中禁用禁用禁用禁用右键取消选择后可用可用可用禁用这里有一个资源附带的坑控件清单表里三个按钮写的都是IDC_BUTTON1这是复制粘贴造成的错误。对照 resource.h 里的定义查询是IDC_BUTTON1上传是IDC_BUTTON2下载是IDC_BUTTON3。你在建立类向导添加对应函数时务必要按这个对应关系来否则点击事件会错位。用户如果选中文件后又不想下载了程序提供了右键取消路径。OnRButtonDown里把编辑框和两个按钮重新启用下载按钮禁用void CFTP_ClientDlg::OnRButtonDown(UINT nFlags, CPoint point) { m_EDIT1.EnableWindow(); m_EDIT2.EnableWindow(); m_EDIT3.EnableWindow(); m_btn1.EnableWindow(); m_btn2.EnableWindow(); m_btn3.EnableWindow(FALSE); // 下载按钮回到禁用态 CDialog::OnRButtonDown(nFlags, point); }注意EnableWindow()不带参数等价于EnableWindow(TRUE)这是我个人更习惯的写法和带参数完全等价。3.2 下载链路OnDOWNLOAD与GetFile的参数和异常处理下载按钮的响应函数OnDOWNLOAD()做的事比看起来多先取当前选中的列表项文本判断是文件还是目录再弹出CFileDialog让用户选择本地保存路径最后调用GetFile。void CFTP_ClientDlg::OnDOWNLOAD() { UpdateData(TRUE); int nSel m_List.GetCurSel(); if (nSel LB_ERR) return; CString fname; m_List.GetText(nSel, fname); if (fname.GetAt(0) ! _T([)) // [ 开头的是目录标记 { CString pname; CFileDialog dlg(FALSE, _T(), _T(*.*)); if (dlg.DoModal() IDOK) { pname dlg.GetPathName(); // 本地保存路径 if (GetFile(fname, pname)) AfxMessageBox(_T(下载成功!), MB_OK | MB_ICONINFORMATION); else AfxMessageBox(_T(下载失败!), MB_OK | MB_ICONSTOP); } } else { AfxMessageBox(_T(此为目录不能下载!), MB_OK | MB_ICONSTOP); } // 操作结束恢复所有控件到初始状态 m_EDIT1.EnableWindow(); m_EDIT2.EnableWindow(); m_EDIT3.EnableWindow(); m_btn1.EnableWindow(); m_btn2.EnableWindow(); m_btn3.EnableWindow(FALSE); }CFileDialog的第一个参数FALSE表示这是“保存”对话框不是“打开”。第二个参数给空字符串表示不限定默认扩展名第三个参数给*.*作为初始文件名内容。在 Windows 高版本系统上这个 MFC 文件对话框默认使用 Vista 风格如果遇到点击无响应的情况可以设置dlg.m_bVistaStyle FALSE切回传统风格。真正的网络下载动作在GetFile里它同样走“建会话→建连接→下载→释放”的老路BOOL CFTP_ClientDlg::GetFile(CString fname, CString pname) { CInternetSession *pSession NULL; CFtpConnection *pConn NULL; pSession new CInternetSession(AfxGetAppName(), 1, PRE_CONFIG_INTERNET_ACCESS); try { pConn pSession-GetFtpConnection(m_dname, m_user, m_psw); } catch (CInternetException *e) { e-Delete(); pConn NULL; delete pSession; // 连接失败时会话对象也要释放 return FALSE; } if (pConn ! NULL) { if (!pConn-GetFile(fname, pname)) { pConn-Close(); delete pConn; delete pSession; return FALSE; } pConn-Close(); delete pConn; } delete pSession; return TRUE; }CFtpConnection::GetFile的原型是BOOL GetFile(LPCTSTR pstrRemoteFile, LPCTSTR pstrLocalFile, ...)第一个参数是服务器端文件名第二个是本地保存路径。这个函数内部完成RETR命令和文件写入失败时返回FALSE。要注意的是下载失败后不能直接 return必须先关闭连接、释放会话否则会留下一个半开状态下次操作可能触发内存或句柄问题。原稿在异常分支里漏掉了delete pSession直接return FALSE。当GetFtpConnection抛异常时会话对象已经创建成功只是连接没建立这时候不释放每次失败下载都会泄漏一个会话。这个我在避坑章会再强调一次。3.3 上传链路OnUPLOAD与PutFile的对应关系上传的逻辑和下载几乎对称区别在文件对话框的用途和参数顺序。void CFTP_ClientDlg::OnUPLOAD() { UpdateData(TRUE); // 上传过程中的控件仲裁 m_EDIT1.EnableWindow(FALSE); m_EDIT2.EnableWindow(FALSE); m_EDIT3.EnableWindow(FALSE); m_btn1.EnableWindow(FALSE); CString fname, pname; CFileDialog dlg(TRUE, _T(), _T(*.*)); // TRUE打开对话框 if (dlg.DoModal() IDOK) { fname dlg.GetPathName(); // 本地文件完整路径 pname dlg.GetFileName(); // 服务器端保存的文件名 if (PutFile(fname, pname)) AfxMessageBox(_T(文件上传成功!), MB_OK | MB_ICONINFORMATION); else AfxMessageBox(_T(文件上传失败!), MB_OK | MB_ICONSTOP); } else { AfxMessageBox(_T(请选择要上传的文件!), MB_OK | MB_ICONSTOP); } // 恢复控件状态 m_EDIT1.EnableWindow(); m_EDIT2.EnableWindow(); m_EDIT3.EnableWindow(); m_btn1.EnableWindow(); }CFileDialog(TRUE, , *.*)的TRUE表示打开文件对话框此时GetPathName()返回完整的本地路径GetFileName()只返回文件名部分。上传到服务器时可以选择以原文件名保存也可以改成别的名字——这里pname dlg.GetFileName()用的是原名。PutFile 的实现同样要注意异常分支的释放BOOL CFTP_ClientDlg::PutFile(CString fname, CString pname) { CInternetSession *pSession NULL; CFtpConnection *pConn NULL; pSession new CInternetSession(AfxGetAppName(), 1, PRE_CONFIG_INTERNET_ACCESS); try { pConn pSession-GetFtpConnection(m_dname, m_user, m_psw); } catch (CInternetException *e) { e-Delete(); pConn NULL; delete pSession; return FALSE; } if (pConn ! NULL) { if (!pConn-PutFile(fname, pname)) { pConn-Close(); delete pConn; delete pSession; return FALSE; } pConn-Close(); delete pConn; } delete pSession; return TRUE; }PutFile的第一个参数是本地文件完整路径第二个是服务器端文件名。和GetFile相比这里容易搞反。很多从下载逻辑改上传代码的人习惯性把两个参数都照搬下载顺序结果上传后服务器上多出一个包含完整路径的文件名或者直接失败。3.4 每次都重建会话到底值不值这份设计的核心思路是三个功能各自独立“查询”建立会话、用完释放“下载”再建一次“上传”再建一次。从性能角度说这很低效——一次 Ftp 连接的建立涉及 TCP 握手、登录认证、目录初始化反复建立会带来明显延迟拷贝大量小文件时尤其吃亏。但从工程角度说这个写法在课程设计里是聪明的。它把状态清理变成了一条直线每个操作入口都是全新会话操作结束无论成败都统一释放。所谓“状态残留”问题被彻底绕开了不需要在多个按钮之间共享连接状态也就少了一类最难查的 bug。如果你要扩展成正式工具可以把会话对象改成成员变量或者用单例持有在关闭程序时统一释放。那样的话连接复用能省下一大截开销但控件的状态仲裁要重新设计——因为“查询后连接一直存在”上传下载就没有必要重新登录失败重试的路径也会复杂很多。4. 避坑与排查把这份源码真正跑起来的五个翻车点做 MFC 和 WinInet 程序这些年我最深的体会是编译错误反而是最好解决的真正浪费时间的是那些“代码看起来没问题但运行就是不对”的问题。这里把这份资源落地时最常踩的五个坑按“现象 → 原因 → 解决”逐一列出。4.1 连不上服务器先检查WinInet读取的代理配置现象程序里填了正确的服务器地址和账号点击查询界面长时间无响应最后弹连接失败但同一台机器上用 FileZilla 客户端却可以正常登录。原因CInternetSession第三个参数PRE_CONFIG_INTERNET_ACCESS会让 WinInet 去读系统里的代理配置。如果本机 Internet 选项里设了代理WinInet 会把 FTP 请求也丢给代理去处理而内网 FTP 服务器通常在代理的例外列表之外于是连接直接超时。解决把会话构造参数改成INTERNET_OPEN_TYPE_DIRECT也就是不经过代理直连。pSession new CInternetSession(AfxGetAppName(), 1, INTERNET_OPEN_TYPE_DIRECT);如果你确定要走代理可以用INTERNET_OPEN_TYPE_PROXY并在构造函数的第四个参数里显式给出代理地址。但就这个程序的使用场景——连接内网或校园网 FTP——直接用INTERNET_OPEN_TYPE_DIRECT最省心。4.2 中文文件名乱码字符集不统一的表现和修正现象查询时英文文件名正常显示中文名显示成乱码下载中文名文件时提示找不到文件但服务器上确实存在。原因经典 MFC 工程的默认字符集是 ANSI而 FTP 服务器返回的目录信息往往是 UTF-8 编码。CFtpFileFind::GetFileName()拿到的字节被按 ANSI 解码显示中文自然就拧了。解决把工程的字符集改成 Unicode。在 Visual Studio 项目属性里选择“常规 → 字符集 → 使用 Unicode 字符集”然后把代码里的字符串都加上_T()宏。_T()在 ANSI 工程里是char*在 Unicode 工程里是wchar_t*MFC 的CString会根据编译选项自动匹配这样GetFileName()返回的宽字符才能被正确转换和显示。4.3 列表只显示第一个文件FindNextFile的错误写法现象服务器目录里明明有几十个文件列表却只显示第一个。删掉第一个文件后再查询显示新的第一个但后面依然空缺。原因这就是 2.3 节指出的原稿缺陷FindNextFile()之后用pfilefind ! NULL判断是否结束导致检索对象在第一次迭代后就被关闭。解决以FindNextFile()的返回值作为循环条件。不要自己判断pfilefind是否为NULL查询对象在有效期内永远不是NULL。所有文件遍历完成后再统一调用Close()并delete。修正代码在 2.2 节已经给出直接替换ListContent()函数体即可。4.4 选中文件被提示“此为目录”显示标记不能当类型判断现象列表里有一个名叫[prefix]readme.txt的文件选中后点下载程序直接提示“此为目录不能下载”。原因程序用filename.GetAt(0) ! [来判断是否目录。服务器返回的文件名本身以[开头时这个判断误判。解决不要靠显示层的标记做逻辑判断。在向列表框添加文件名时用CListBox::SetItemDataPtr把远程文件的真实类型或者完整路径存起来选中时再取回来做判断。简单做法是定义一个结构体struct FTP_ENTRY { BOOL bDir; CString sName; CString sPath; };添加条目前先把信息填进结构体AddString后再SetItemDataPtr。需要判断类型时从GetItemDataPtr还原结构体读bDir字段。程序退出时再释放这些结构体内存。这样显示和逻辑分离目录判断就稳定了。4.5 异常分支泄漏CInternetException的Delete与psession清理现象程序连续多次下载失败后进程内存持续增长或者偶发“内存无法读取”的崩溃崩溃点在CInternetException相关代码。原因两个叠加的问题。第一MFC 的CInternetException是通过AfxThrowInternetException抛出的内部有引用计数必须调用Delete()释放直接delete e会造成计数错乱。第二原稿GetFile的 catch 分支里只return FALSE没释放已经创建的pSession每次连接失败都泄漏一个会话对象。解决所有异常分支统一按这个模板清理catch (CInternetException *e) { e-Delete(); pConn NULL; delete pSession; // 会话对象必须释放 return FALSE; }这一点用调试器看不出明显问题但用任务管理器观察内存变化就能确认。网络程序里的泄漏往往都藏在异常路径正常路径释放得再干净也没用。5. 把代码搬到新版Visual Studio并验收迁移四步与五分钟测试5.1 迁移四步字符集、控件ID、静态库和头文件老代码要迁移到 VS2015 以上版本我一般至少做四件事。第一新建项目时选“在静态库中使用 MFC”避免目标机器没装 MFC DLL 导致程序起不来。第二把字符集设为 Unicode解决 4.2 节的中文乱码。第三检查控件 ID 映射。这篇文档的控件清单表格里三个按钮 ID 全是 IDC_BUTTON1必须按 resource.h 的真实定义修正为查询 IDC_BUTTON1、上传 IDC_BUTTON2、下载 IDC_BUTTON3。第四确认stdafx.h里有#include afxinet.h没有这个头文件CInternetSession根本编译不过。以上四步做完编译就只是体力活。真正需要用心的反而是运行验证。5.2 五分钟验收用pyftpdlib在本地起一个FTP服务器没有真实 FTP 服务器时我习惯用 Python 的 pyftpdlib 在本地临时起一个。它不需要安装额外服务一个脚本文件就能跑非常适合做客户端程序的验收环境。先准备目录和脚本from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer authorizer DummyAuthorizer() # perm 参数含义e切换目录l列目录r下载a追加d删除f重命名m创建目录w上传 authorizer.add_user(test, test123, ./ftproot, permelradfmw) handler FTPHandler handler.authorizer authorizer server FTPServer((0.0.0.0, 21), handler) server.serve_forever()在当前目录下创建ftproot文件夹放入几个测试文件注意至少包含一个中文名文件和一个以[开头的文件这两个是专门用来验证 4.2 和 4.4 的。跑起脚本后在客户端程序里填127.0.0.1、test、test123依次执行查询、下载、上传、右键取消流程全部正常这个程序就算验收通过。验收时要重点观察三件事列表是否完整列出所有文件下载中文名文件后内容是否一致上传后的文件是否以所选文件名出现在服务器目录里。另外测试账号test/test123这种弱口令只能用在本地环境别拿到生产 FTP 服务器上去试这是最基本的安全习惯。从那以后我拿到任何一份带 MFC 源码的课程设计都会先检查三件事字符集是 ANSI 还是 Unicode控件 ID 映射有没有复制粘贴错误工作结束后所有会话对象是否都退出了。这三项查完编译能过只是开始跑起来不死循环、不泄漏才算真正吃透。这份 FTP 客户端的代码结构足够简单简单到你可以放心地在它上面做裁剪和扩展希望这套拆解步骤能帮到你。本文还有配套的精品资源点击获取