ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用VC++手写FTP服务器与客户端:从协议到断点续传的完整实践

2026/9/21 1:54:31 拓冰建站 浏览量
用VC++手写FTP服务器与客户端:从协议到断点续传的完整实践 简介面向VC/MFC网络通信开发者的FTP客户端与服务器完整源码适合具有一定C基础、想深入理解FTP协议实现与Socket编程的读者。资源共26个文件以7个cpp源文件和7个h头文件为核心辅以dsw/dsp工程配置、txt说明文件及图标资源压缩包仅42KB结构简洁便于快速定位关键代码。项目分为基于控制台的FTP服务器和带界面的FTP客户端两部分服务器已覆盖大部分FTP服务端功能内置扩展接口可在此基础上演化出更复杂的业务逻辑客户端封装了FTPClass、FTPSocket等模块包含连接管理、文件上传下载、目录列表等典型流程。所有源码均在VC6.0中调试通过并附有账号密码toldo与主目录c:/temp等运行说明上手成本低。已有290人学习适合作为课程设计、毕设参考或网络通信入门提升的实践素材。 先交代一下背景。之前在内网搭文件服务图省事直接装了Filezilla Server结果同事那边连不上、连上了又说“不安全的服务器”Windows资源管理器倒是能进但传大文件动不动断断点续传还不支持。折腾了一阵子我索性用VC从零写了整套FTP服务器和客户端服务端能多用户并发、支持被动模式、能处理中文文件名客户端能上传、下载、断点续传、显示进度。做完之后回头再看FTP这东西虽然老但“客户端/服务端”的通信范式在今天的工控、运维、嵌入式场景里仍然是基本功。这篇就把整个项目从设计到踩坑的过程完整记下来给想用VC/C写网络程序的人做个参考。1. 连不上和“不安全”背后为什么我决定用VC重写一套FTP先讲个反直觉的观察FTP的问题往往不在FTP本身而在“你以为你懂它”。市面上免费的FTP服务端一大把但真放到固定场景里用总有几个绕不过去的坎。第一个坎是加密策略。新版Filezilla Server默认要求客户端启用TLS加密客户端一上来就是明文连接界面直接报“不安全的服务器”。如果你在纯内网环境、全是自己的设备这个提示纯粹是添乱可你要是为了消除提示去配置证书又要维护一套证书生命周期。第二个坎是权限模型。我现在做的项目要给不同设备绑定不同目录设备序列号对应独立空间Filezilla Server那种基于用户配目录的方式要跟业务系统动态联动非常别扭。第三个坎是集成。文件传完之后要触发后续处理比如固件包解析、配置校验现有服务端做不到。所以当时我的判断是与其在现成工具上打补丁不如自己写一套完整版把服务端和客户端都攥在手里。这套东西要做到三点不依赖第三方库用WinSock和标准C/C跑通支持主动和被动两种数据连接模式中文文件名不乱码。做完之后发现FTP协议本身并不复杂真正的复杂度全在细节里。这套代码适合谁第一是VC学习者你可以在里面看到socket编程、多线程、字符串处理的完整工程实践。第二是需要内网文件传输、又希望传输逻辑能被自己控制的开发人员。第三是想深入理解FTP协议的人——自己实现一遍服务端和客户端比看十遍RFC都管用。2. 控制连接和数据连接服务端架构的第一步FTP最容易被新手忽略的设计是它用了两条TCP连接。一条叫控制连接固定走21端口用来传命令和响应另一条叫数据连接用来传目录列表、文件内容。我服务端第一版就把这两条连接混在一起处理结果LIST命令返回了目录数据客户端傻等命令响应最后超时。这个架构问题理清楚之后后面的代码才顺了。2.1 控制连接的命令循环长什么样控制连接本质上是一个文本协议客户端发一行命令服务端回3位数字响应码和一段说明文字。比如客户端发USER admin服务端回331需要密码再发PASS 123456回230登录成功。我实现的命令循环大概是这样的while (recvCommand(clientSock, cmdBuf)) { parseCommand(cmdBuf, cmd, arg); if (cmd USER) handleUser(arg); else if (cmd PASS) handlePass(arg); else if (cmd PWD) handlePwd(); else if (cmd CWD) handleCwd(arg); else if (cmd PASV) handlePasv(); else if (cmd PORT) handlePort(arg); else if (cmd LIST) handleList(arg); else if (cmd RETR) handleRetr(arg); else if (cmd STOR) handleStor(arg); else if (cmd REST) handleRest(arg); else if (cmd QUIT) { sendResponse(clientSock, 221 Bye); break; } }服务端收到命令后的响应必须按协议规定来。比如登录成功返回230路径不存在返回550命令无法处理返回502。响应码错了客户端就会做出各种奇怪行为——这算是FTP调试里最常有的事故源。2.2 主动模式和被动模式选哪个数据连接的建立有两种方式这是FTP最核心的机制。主动模式PORT下客户端告诉服务端“你连我某个端口”然后服务端主动发起数据连接。问题是如果客户端在NAT后面服务端根本连不到客户端的IP。被动模式PASV反过来服务端开一个新端口告诉客户端“你来连我”由客户端主动发起数据连接。这样NAT穿透问题就解决了。我做了个对照表方便选型项目主动模式(PORT)被动模式(PASV)数据连接发起方服务端向客户端发起客户端向服务端发起服务端端口使用20端口临时打开一个高端口客户端防火墙友好度较差需开放入站较好只做主动出站服务端防火墙要求放行20和21放行21和PASV端口段NAT场景兼容不推荐推荐服务端PASV的响应格式有个经典坑返回的是IP和端口的组合而且格式不是给人看的。比如服务端IP是192.168.1.100开的数据端口是5001050010除以256得195余40那响应就是227 Entering Passive Mode (192,168,1,100,195,40)。客户端拿到之后要自己拼回IP和端口。我当时算端口的时候直接用了端口号50010客户端怎么都解析不对后来才想起来要拆高字节和低字节。2.3 每个会话一个线程的理由服务端的并发模型我最初考虑过select、IOCP后来还是选择了“每客户端一个线程”。原因很直接FTP会话的交互频率低大部分时间阻塞在recv上等客户端命令一个线程处理一个客户端逻辑清晰、调试方便。IOCP适合大量短连接和高并发请求的场景用在FTP里属于过度设计。主线程只做accept每个新连接创建一个工作线程SOCKET clientSock accept(listenSock, ...); _beginthreadex(NULL, 0, ClientWorker, (void*)clientSock, 0, NULL);在线程里要注意一点每个线程的回调函数里必须做一次WSAStartup初始化不要以为主线程初始化了就万事大吉。我第一版在主线程只初始化了一次结果新线程里调send直接返回WSANOTINITIALISED排查了半天。3. 服务端最容易踩的三个坑我一个个说这层坑是最典型的包括路径遍历、中文乱码和被动端口规划。排序按踩坑的痛苦程度来。3.1 路径穿越CWD .. 就能读到根目录之外如果说哪个漏洞让FTP服务端看起来像个筛子路径穿越排第一。攻击者发CWD ../../命令配合RETR命令就能把服务端磁盘上的任意文件拉走。这个问题不是FTP独有任何按字符串拼接路径的文件服务都有。网上能看到很多“简易FTP服务端”代码基本都是直接操作字符串把用户输入的目录拼到根目录后面这是最容易出事的写法。我处理的方式是定义一个规范化函数把收到的相对路径转成绝对路径并强制校验其前缀必须在根目录内。关键代码如下bool NormalizePath(const std::string rootPath, const std::string input, std::string safePath) { std::string fullRoot GetFullPathA(rootPath); std::string fullInput GetFullPathA(rootPath \\ input); if (fullInput.compare(0, fullRoot.size(), fullRoot) ! 0) { return false; // 越界 } safePath fullInput; return true; }这里GetFullPathA对CWD ..的处理是逐级消解的所以只要前缀校验过了就说明用户永远跳不出根目录。这个函数要放在所有涉及路径的命令前面统一调用包括CWD、RETR、STOR、DELE、MKD、RMD。3.2 中文文件名乱码是编码问题不是字体问题FTP协议最初只定义了ASCII文件名怎么办RFC 2640建议用UTF-8但Windows资源管理器自带的FTP客户端默认用的是本地ANSI代码页中文环境是GBK。这就导致一个奇妙的局面服务端按UTF-8存中文名Windows资源管理器列目录显示乱码服务端按GBK处理支持UTF-8的客户端看到乱码。我不建议二选一而是做了一个可配置的编码开关。默认跟随系统GBK面向Windows资源管理器再加一个UTF-8模式面向标准FTP客户端。核心字符串转换函数用两个API就够了// UTF-8 转 GBK std::string Utf8ToAnsi(const std::string utf8) { int wlen MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0); wchar_t* wbuf new wchar_t[wlen]; MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wbuf, wlen); int alen WideCharToMultiByte(CP_ACP, 0, wbuf, -1, NULL, 0, NULL, NULL); char* abuf new char[alen]; WideCharToMultiByte(CP_ACP, 0, wbuf, -1, abuf, alen, NULL, NULL); std::string ansi(abuf); delete[] wbuf; delete[] abuf; return ansi; }反向的GBK转UTF-8就是把两个API的参数对调。另外还要注意工程属性如果是“多字节字符集”代码写起来省事但数据流向一旦涉及外部系统会非常痛苦。我建议直接把项目设置改成“使用Unicode字符集”内部统一用wchar_t或std::wstring只在socket收发边界做编码转换。3.3 被动模式端口没规划好客户端永远连不上PASV模式下服务端会临时开一个1024以上的端口。如果你让操作系统随机分配就会出现这种情况控制连接通了登录也成功了但一发LIST就卡住或者报“无法与服务器建立连接”——因为随机端口被防火墙拦了。解决方案是在服务端配置一个被动端口段比如50000到50100然后在这段范围内挑选空闲端口绑定。代码长这样SOCKET pasvSock INVALID_SOCKET; for (int port 50000; port 50100; port) { pasvSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_port htons(port); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(pasvSock, (sockaddr*)addr, sizeof(addr)) 0) { break; } closesocket(pasvSock); pasvSock INVALID_SOCKET; } listen(pasvSock, 1);端口段开好之后防火墙入站规则要么按程序放行要么显式放行这个端口范围。这个细节解决了大部分“能登录但列不出来目录”的问题。我在服务端日志里会记录每次PASV选中的实际端口方便排查连不上是端口问题还是网络问题。4. 客户端不是“连上就完”还有界面和断点只写服务端不写客户端这套代码就缺了一条腿。客户端这边我也遇到了一堆界面与网络纠缠的问题——大量的新手项目都在这里翻车。4.1 用原生Socket还是WinINet封装做FTP客户端摆在面前有两条路。一条是MFC里现成的CFtpConnection内部封装了WinINet API代码量非常小连接、上传、下载几句话就搞定。另一条是用原生socket手写协议从连接到命令交互全部自己控制。我当时两个都试了最后在正式版里选的还是原生socket。原因在于CFtpConnection对PASV模式的处理不够透明而且断点续传支持有限遇到自定义服务端扩展命令也没法用。用原生socket虽然代码多几百行但每一步在做什么你都清楚出问题能用抓包工具对着看心里有底。如果你只是写个临时脚本CFtpConnection可以如果你想摸透协议原生socket才是正道。4.2 一次标准登录和传输的完整流程客户端和服务端的完整交互序列大致是这样的1. TCP连接服务器21端口 2. 接收 banner220 开头 3. 发送 USER 用户名接收 331 4. 发送 PASS 密码接收 230 5. 发送 TYPE I二进制模式接收 200 6. 发送 PASV接收 227解析出数据端口 7. 新建socket连接该数据端口不急着发数据 8. 发送 RETR 文件名或 LIST、STOR接收 150 9. 通过数据socket收发数据结束后关闭数据socket 10. 接收 226传输完成注意第7步和第8步的顺序不能反。很多人把RETR和数据连接建立搞成先后阻塞结果互相等死。正确做法是先连数据端口再发RETR命令。客户端发明文命令没问题但接收响应时要处理多行响应的情况尤其是banner和某些服务器的欢迎语可能占多行。4.3 界面上传下载的线程与进度处理我见过不少VC新手把socket收放在按钮事件里结果一传大文件界面直接假死。Windows界面消息循环和网络阻塞调用天生冲突必须把网络操作放到单独线程。我用AfxBeginThread创建一个工作线程界面线程只负责收集用户输入和刷新列表。工作线程每收到一段数据就用PostMessage通知界面线程更新进度条而不是SendMessage。为什么因为SendMessage会等界面线程处理完才返回如果界面线程正在处理其他重任务网络线程会卡住。PostMessage把消息丢进队列就返回两边互不阻塞。线程收尾也要小心用户点了“取消下载”不能直接强杀线程否则socket资源来不及释放下次再连会发现端口冲突。正确做法是设置一个取消标志位网络线程在每次读数据前检查通过closesocket来唤醒阻塞的recv从而实现温和退出。4.4 断点续传REST命令是关键断点续传的实现分三步第一步上传或下载前发SIZE命令获取远程文件大小第二步本地文件从偏移量开始读写第三步发REST offset命令告诉服务端从指定位置开始传。REST命令要在RETR或STOR之前发而且要通过控制连接发——这个顺序错了服务端会返回503错误。要注意的是断点续传要求服务端也明确支持REST命令。我在自写服务端里把REST记为会话状态在后续的RETR/STOR时用fseek定位文件指针。这套机制组合起来大文件传一半断网重连之后能从断点继续体验比Filezilla连内网时稳定得多。5. 拿Filezilla和Windows资源管理器联调的那些坑自己写的东西要拿到真实环境里接受考验。我把自写服务端和客户端放到网上再用不同工具去连果然炸出一堆细节问题。5.1 Filezilla提示“不安全的服务器”究竟在说什么新版Filezilla默认要求服务端支持FTP over TLSFTPS如果客户端检测到服务端纯明文就弹出“不安全的服务器”警告。这个提示本身是负责任的安全设计但在纯内网环境里会把人绕晕。解决方式有两种。第一种是服务端加TLS支持用SChannel或OpenSSL实现AUTH TLS指令和加密通道切换。第二种是让Filezilla客户端改为“仅使用普通FTP”——在站点管理器里把加密选项改成“只用普通FTP”。我自写服务端第一版没有TLS所以测试时用第二种但真放到稍微重要一点的环境我会强烈建议直接上FTPS因为明文FTP在内网也有被抓包的风险这个后面细说。写客户端的时候也要注意类似问题如果连的是公网FTP服务器、又支持TLS客户端最好也能发AUTH TLS。我在客户端里预留了FTPS扩展位置只不过默认关闭。5.2 Windows资源管理器“无法与服务器建立连接”怎么排除用Windows资源管理器访问FTP最常见的问题就是能弹出登录框、但点进去后目录为空或者提示无法建立连接。这基本是数据连接的问题不是账号密码错了。从我的实际经验看排除步骤很简单在资源管理器地址栏直接输入ftp://IP:21如果能连上说明21端口通。登录后在界面执行“查看”目录如果卡住或者报错90%是PASV数据端口不通。去服务端防火墙上确认21和PASV端口段都放行了。如果服务端在NAT后面检查服务端PASV响应里返回的IP是不是内网IP资源管理器用这个IP连不上就废了。解决办法是服务端做PASV响应时把回包里的IP替换成外网映射IP。第4条是最隐蔽的坑。我当时用路由器映射了21端口客户端登录顺利一列目录就超时抓包发现客户端在尝试连接192.168.x.x这个内网地址当然连不上。后来在PASV响应构造时加入可配置的公网IP问题才算解决。5.3 部署到别的机器时别忘了VC运行库代码在本机跑得好好的拷到测试机一运行提示“无法启动程序因为计算机中丢失VCRUNTIME140.dll”——这是VC Redistributable没装。用Visual Studio默认编译出的程序依赖动态运行库目标机器没有对应版本的运行库就会这样。解决方式有两个一是项目属性-代码生成-运行库改成多线程静态链接/MT这样程序体积大一点但不需要对方装运行库二是发布时把vc_redist.x64.exe一起带上部署时静默安装。我在自己的发布包里选择了后者因为服务器上还可能跑其他VC程序装了运行库一劳永逸。如果你用的是Debug编译还要注意Debug版依赖调试运行库目标机器几乎不可能装全正式分发一律用Release。6. 个人实践这套代码在真实环境里怎么用项目做完之后我自己把它用在一个工业设备的固件升级场景里。每台设备用一个序列号当登录名对接的设备目录就是用户名权限也只能在这个目录里活动。设备端不需要Windows资源管理器是嵌入式的FTP客户端直接连服务端做文件传输。整个流程稳定跑了几个月没出过传输中断的事。记录几个真实的使用心得供后来者参考。第一FTP服务端千万不要在公网裸奔。明文的账号密码和文件内容可以被抓包直接看到。如果必须对外提供服务要么给这套代码加TLSFTPS要么干脆改用SFTP走SSH协议不要因为“只是自己用”就图省事。第二被动端口段一定要在配置里固定下来不要用系统随机分配这是服务端部署最省心的做法。第三传输大文件时日志要记录命令时间戳和传输字节数出了问题时抓包和日志对照着看能快速定位。坦白说自己实现一遍FTP之后再去接触HTTP、MQTT这类协议都会有种“原来是异曲同工”的通透感。所有的客户端/服务端模式核心都是建立连接、发请求、收响应、管理资源。FTP的文本命令和清晰的状态码恰好是最适合入门的范本。如果接下来要扩展我最想给这套代码加上的是FTPS支持以及一个简单的Web管理后台。有动手想法的朋友也可以沿着这两条路继续做下去。本文还有配套的精品资源点击获取