简介:面向C#开发者的FTP客户端完整源码包,基于MFC对话框程序框架,覆盖FTP协议中控制连接、数据连接以及主动/被动模式等核心概念。源码共17个文件、约26KB,以.h头文件与.cpp源文件为主,配套.dsp/.dsw/.plg等工程文件可直接编译查看,另有.rc资源描述、ReadMe说明与版权文本,便于对照代码理解FtpWebRequest、FtpClient等常用API的实际调用,以及目录切换、上传下载、断点续传、异常捕获与被动模式适配NAT/防火墙等典型实现逻辑。对需要快速上手FTP编程、动手调试C/S文件传输场景的初学者尤为适合;对正在做局域网文件管理、FTP工具二次开发的学习者也有参考价值。使用时还需根据实际服务器IP调整连接参数,可进一步熟悉登录验证与目录解析流程。已有202人浏览学习,体积小巧但功能模块完整,适合作为入门研读或课堂实验的示例工程。
1. FtpDlg 源码工程:一份名不副实但骨架完整的 FTP 客户端参考
解压 Ftp.rar,看到的不是 .cs 文件,而是 FtpDlg.cpp、StdAfx.h、Ftp.dsw 这一批 VC++ 6.0 时代的工程文件。资源描述标注为 C# 实现的 FTP 客户端,实际源码载体却是 MFC 对话框程序,这类错位在早期源码共享站点很常见,动手改造前最好先区分工程类型。好在代码骨架完整:连接管理、身份验证、目录解析、上传下载被拆到了 Ftp 类和 FtpDlg 类两层里,FtpDlg.cpp 管界面和按钮事件,Ftp.cpp 管 FTP 命令与响应。对写 C# 上位机、局域网文件传输工具的人来说,这份源码的价值不在直接编译运行,而是它把 FTP 客户端该处理的细节完整暴露了一遍。把这套语义迁移到 .NET 的 FtpWebRequest 或 FluentFTP 上重新实现,才是更实用的方向。
2. FTP 双通道连接机制与 C# 客户端选型
2.1 控制连接与数据连接:先厘清协议边界
FTP 是双连接协议,控制连接默认 21 端口,负责 USER、PASS、PWD、CWD、PASV 这些命令和 220、230、331 等数字响应;数据连接只在列目录、上传、下载时建立,端口由主动或被动模式决定。FtpDlg 的设计里最容易被忽略的是这条边界——按钮点击后先发 FTP 命令,命令通过后才开数据连接。用 C# 重写时不需要管底层 Socket,但必须理解数据连接失败时对应的现象:能登录、能切目录,dir 或 get 卡住;被动模式下服务器返回 227 响应携带内网地址;主动模式被防火墙拦断时报 425 Can't open data connection。这几类问题都指向数据连接,与 21 号控制连接无关。不少刚接触 FTP 的开发者把 21 端口放开了事,最后卡在能登录不能传文件,排查思路一开始就要分两条线走。
2.2 FtpWebRequest 与 FluentFTP:选型要看目录解析和日志
C# 侧做 FTP 客户端无非两条路:用 .NET 内置的 FtpWebRequest,或者引入 FluentFTP 这类第三方库。FtpWebRequest 的优势是零依赖,.NET Framework 与 .NET 6/8 都能用;缺点是目录列表只给原始字符串,解析要靠自己写,断点续传需要手动设置 ContentOffset。FluentFTP 则把目录解析、断点续传、TLS、响应日志都做进了库内部,代价是多一层 NuGet 依赖和版本兼容工作。按 FtpDlg 这种对话框工具的使用习惯,我更倾向 FluentFTP,因为目录解析这类脏活它已经做掉,调试时还能看到完整命令交换;如果只是给内部工具加一条上传逻辑,用 FtpWebRequest 更省心。
| 对比项 | FtpWebRequest | FluentFTP |
|---|---|---|
| 依赖 | .NET 内置 | NuGet 第三方 |
| 目录列表解析 | 自己解析 LIST 字符串 | 内置 FtpListItem |
| 断点续传 | 手动设置 ContentOffset | 下载/上传方法带断点选项 |
| FTPS 支持 | EnableSsl 开关 | EncryptionMode 枚举 |
| 主动/被动 | UsePassive 属性 | Config.ActivePorts 等配置 |
| 响应日志 | 需自己抓包 | LogToConsole |
选择时还要考虑整个团队是否熟悉第三方库的行为细节,FluentFTP 版本更新快,个别接口在 30.x 到 40.x 之间有破坏性变更,老工程升级前需要看变更记录。
2.3 被动模式与主动模式:C# 里必须显式控制
主动模式下客户端发 PORT 命令告知自身监听端口,服务器反向连接数据端口;被动模式下客户端发 PASV,服务器打开监听端口并在 227 响应里返回地址与端口。经过 NAT 或 Windows 防火墙的机器,主动模式几乎必失败,新写的客户端默认应走被动模式。用 FtpWebRequest 时需要把被动模式、二进制传输和连接复用行为显式控制住:
static FtpWebRequest MakeRequest(string url, string method) { var req = (FtpWebRequest)WebRequest.Create(url); req.Credentials = new NetworkCredential("ftp_user", "ftp_password"); req.Method = method; // UploadFile / DownloadFile / ListDirectoryDetails req.UsePassive = true; // 走 PASV,避免服务器回连失败 req.UseBinary = true; // 二进制传输,图片与压缩包不能少 req.KeepAlive = false; // 单请求单连接,便于释放 req.Timeout = 10000; // 控制连接超时,单位毫秒 return req; }UsePassive 对应协议层的 PASV,UseBinary 对应 TYPE I。KeepAlive 设为 false 是刻意为之:FtpWebRequest 复用连接时,如果服务器在夜间重启、网络切换,旧连接残留状态会影响下一次命令,对话框工具对性能不敏感,干脆断开最稳。Timeout 只覆盖控制连接等待响应的时长,不覆盖文件传输过程中的数据流读写,大文件场景还要单独设置 ReadWriteTimeout。
3. FtpDlg 源码结构拆解:连接、认证与目录解析
3.1 VS 6.0 工程文件与职责划分
拿到 Ftp.rar 后按文件后缀分类,就能判断谁管什么。Ftp.dsw 是工作区文件,Ftp.dsp 是项目文件,对应 C# 工程的 .sln 与 .csproj;Ftp.cpp/Ftp.h 是 FTP 操作封装类;FtpDlg.cpp/FtpDlg.h 是 MFC 对话框实现,承载按钮事件与控件逻辑;Ftp.rc 与 resource.h 定义界面资源,StdAfx.cpp/StdAfx.h 是预编译头,迁移到 C# 后这些都不需要保留。读老代码时最有效的切入点是 FtpDlg 的按钮事件,看每个按钮背后发了哪些 FTP 命令,再回到 Ftp.cpp 看命令与响应的分包、超时和重传处理,改造速度远快于逐行读。
| 文件 | 原工程角色 | .NET 对应 |
|---|---|---|
| Ftp.dsw / Ftp.dsp | 工程与工作区 | .sln / .csproj |
| Ftp.cpp / Ftp.h | FTP 命令封装 | FtpClient 封装类 |
| FtpDlg.cpp / FtpDlg.h | MFC 对话框逻辑 | WinForms/WPF 窗口 |
| Ftp.rc / resource.h | 对话框资源 | 窗体设计器文件 |
| StdAfx.cpp / StdAfx.h | 预编译头 | 无需对应 |
FtpDlg 界面一般只有服务器 IP、端口、用户名、密码和几个功能按钮,端口经常被写死为 21。改成 C# 版本时建议把端口做成可配置项,因为 FTPS 和 SFTP 的默认端口不同,同一套代码接不同环境时需要切换。
3.2 身份验证:USER、PASS 与 Credentials 的对应
登录流程实际包含三步:连接 21 端口、等待服务器 220 欢迎码、发送 USER 与 PASS。响应 331 表示需要密码,230 表示登录成功,530 表示认证失败。C# 中这些被压缩进一行 Credentials 赋值,但有两个细节要处理:匿名登录留空密码时,某些服务器会把空 PASS 后收到的 332 当错误,需要捕获后按匿名处理;密码含特殊字符时,Credentials 内的字符串不能再做 URL 编码,否则密码会被改写。FluentFTP 的账号密码设置在 FtpClient 构造函数的第二个和第三个参数里,底层同样映射到 USER 与 PASS 命令,行为上没有本质差异。
3.3 目录列表解析:从 LIST 原始文本到文件名字段
FtpDlg 列目录时发送 LIST 命令,返回的每一行都带着服务器操作系统的痕迹。Windows IIS 返回类似 02-20-24 10:30AM
static string ExtractNameFromListLine(string line) { var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 4) return line.Trim(); // Unix 风格:权限位开头,日期时间形如 Jan 20 10:30 if (line[0] == '-' || line[0] == 'd' || line[0] == 'l') { for (int i = 0; i < parts.Length; i++) { if (parts[i].Contains(':')) return string.Join(" ", parts, i + 1, parts.Length - i - 1); } } // Windows 风格:第一段日期,第二段时间,第三段可能是 <DIR> if (parts[0].Contains("-") && parts[1].Contains(":")) { int start = parts[2] == "<DIR>" ? 3 : 2; return string.Join(" ", parts, start, parts.Length - start); } return line.Trim(); }代码先按空白符拆分,再判断行首是权限位还是日期,定位到时间字段后拼接剩余部分,文件名中的空格得以保留。注意 Windows 风格里目录标记<DIR>位于文件名之前,需要跳过。更干净的方案是改用 MLSD 命令获取机器可读的目录列表,但老 FTP 服务器不一定支持,客户端要做能力探测再降级到 LIST。
3.4 响应码分支与异常截获
FTP 响应码第一位代表类别:2xx 成功、3xx 需要更多输入、4xx 临时失败、5xx 永久失败。FtpDlg 时代最容易踩的坑是把 220、331、230 都当成功,把 425、426 这类数据连接错误误报成登录失败。C# 的 FtpWebRequest 对非 2xx 响应直接抛 WebException,可以从异常里取回 FtpWebResponse 继续分析:
try { using (var resp = (FtpWebResponse)req.GetResponse()) { Console.WriteLine($"Status: {resp.StatusCode} {resp.StatusDescription}"); } } catch (WebException ex) { if (ex.Response is FtpWebResponse ftpResp) { // 530 认证失败,550 文件不存在或权限不足 Console.WriteLine($"FTP Error: {ftpResp.StatusCode} - {ftpResp.StatusDescription}"); } }StatusCode 是 FtpStatusCode 枚举,StatusDescription 带服务器原始描述。分支判断用枚举,日志输出用原始描述,两者不要混用。登录失败需要区分 530 和 332:530 是账号密码错,332 是服务器要求显式发送 ACCT 命令,多数客户端没有实现这条命令,遇到时要么换服务器配置,要么补一个空 ACCT。
4. 文件上传下载与断点续传的 C# 实现
4.1 上传:文件流写入数据连接
FtpDlg 底层把上传拆成 STOR 命令与数据连接写入两步。C# 中先将 Method 设为 UploadFile,再把本地文件流拷贝到 GetRequestStream() 返回的流里。这个流是控制连接成功协商后的数据通道,写入完成后必须显式关闭,服务器才收到 EOF 并结束传输进程:
using (var fs = File.OpenRead(@"C:\data\report_20250120.bin")) using (var reqStream = MakeRequest( "ftp://192.168.1.50/upload/report_20250120.bin", WebRequestMethods.Ftp.UploadFile).GetRequestStream()) { fs.CopyTo(reqStream); // 逐块拷贝,避免整文件读入内存 reqStream.Close(); // 显式关闭代表文件传输结束 }MakeRequest 内部已设置二进制传输与被动模式,这段逻辑只关心流拷贝。CopyTo 默认按 81920 字节分块,对局域网大文件足够友好。路径里的反斜杠要换成正斜杠,服务器端目录不存在时会返回 550,需要在上传前先调用 MakeDirectory 创建目录。文件大小为零时某些服务器不会建立数据连接,客户端需要单独处理空文件的情况。
4.2 下载:从响应流落到本地磁盘
下载方向相反,读取 GetResponseStream 后写入 FileStream。关键点是边读边计数,因为网络流读进时抛出的 IOException 可能是服务器断连导致的,也可能是正常读取到一半被中断,只有读空流才是正常结束。用 ContentLength 判断结束并不可靠,部分服务器返回的 ContentLength 与实际字节数不一致:
using (var resp = (FtpWebResponse)MakeRequest( "ftp://192.168.1.50/backup/db.json", WebRequestMethods.Ftp.DownloadFile).GetResponse()) using (var netStream = resp.GetResponseStream()) using (var fs = File.Create(@"D:\downloads\db.json")) { byte[] buf = new byte[65536]; int read = 0; long total = 0; while ((read = netStream.Read(buf, 0, buf.Length)) > 0) { fs.Write(buf, 0, read); total += read; if (total % (1024 * 1024) == 0) Console.WriteLine($"downloaded {total / 1024} KB"); } }缓冲区 65536 字节是通用选择,弱网环境可以降至 4096,但循环次数和 CPU 占用会上升。进度输出放在循环里是为了直观看出传输是否停滞,生产环境应换成日志委托。下载前先与本地文件大小比较,若远端小于本地且小于阈值,走断点续传逻辑还是直接覆盖,要根据业务决定。
4.3 断点续传:REST 偏移量与 Append 模式的取舍
FTP 断点续传的协议基础是 REST 命令,在数据传输前告知服务器偏移量,随后再发 RETR 或 STOR。FtpWebRequest 没把 REST 做成独立方法,而是通过 ContentOffset 属性传递给底层指令;FluentFTP 则把断点能力集成到下载和上传方法里,同时加入失败重试次数等参数。两种实现的接口对应关系如下:
| 场景 | FtpWebRequest | FluentFTP |
|---|---|---|
| 下载续传 | ContentOffset = 本地已有长度 | DownloadFile(..., FtpLocalExists.NoCheck) |
| 上传续传 | ContentOffset = 远端已有长度 | UploadFile(..., FtpRemoteExists.Append) |
| 失败重试 | 手动循环 | Config.RetryAttempts |
FluentFTP 的上传续传示例:
var client = new FtpClient("192.168.1.50", "ftp_user", "ftp_password"); client.Config.RetryAttempts = 3; // 失败重试 3 次 client.Config.SocketKeepAlive = true; // 长连接保活 var result = client.UploadFile( @"C:\data\big.bin", "/upload/big.bin", FtpRemoteExists.Append, // 文件存在则追加 true); // 自动创建远端目录UploadFile 的第三个参数表示文件存在时的处理策略,第四个参数为 true 表示自动创建缺失的远端目录。注意 FtpRemoteExists.Append 在部分服务器上实现为 APPE,与 REST+STOR 行为略有差异,若服务器不支持 APPE 会返回 502。重试机制只处理网络层错误,不会检查文件内容,续传前需要比较本地与远端文件大小,避免拼接出损坏文件。大文件场景下更稳妥的办法是传输完成后比较双方大小,不一致则删除重来。
4.4 连接释放与命令行级别的日志观察
FtpDlg 时代的连接泄漏常表现为服务器会话数被占满、新用户登录被拒。C# 端要养成两个习惯:KeepAlive 显式设为 false 并把请求放进 using 中;FluentFTP 则用 Dispose 释放连接。第二个习惯是打开日志。FluentFTP 的 LogToConsole 会把原始命令与响应完整打印出来:
> USER ftp_user < 331 Password required > PASS ftp_password < 230 Logged in这类输出会显示服务器返回的原始响应码,处理 501、530 时能直接判断问题出在哪条命令上。用 FtpWebRequest 时看不到这类内部命令,抓包成本高,这也是我在复杂调试场景更倾向 FluentFTP 的原因。
5. FTP 响应 501 与 Windows 防火墙被动模式的排查路径
5.1 501 参数错误的常见来源
响应 501 的官方含义是命令参数语法错误,实际场景多由三类原因造成:路径带空格未转义、中文路径编码被拆散、部分服务器要求 LIST 命令显式带 -aL。C# 里用 Uri.EscapeDataString 对路径逐段编码,而不是全局替换空格后再传输:
string safePath = string.Join("/", path.Split('/').Select(Uri.EscapeDataString));抓包时若发现客户端发的路径里出现 %20 残留,说明编码发生二次转义,服务器拿到的是 %25 开头的字符串,自然报 501。对中文目录,先确认服务器端文件系统编码是 UTF-8 还是 GBK,必要时对路径做对应转码。
5.2 Windows 服务器 FTP 防火墙与被动端口放行
"能登录无法传文件"先查 Windows 防火墙是否阻挡数据连接。IIS FTP 服务需要先在 IIS 管理器里给站点配置被动模式端口范围,再把该范围加入防火墙规则。命令行可执行:
netsh advfirewall firewall add rule name="FTP Passive 5000-6000" dir=in action=allow protocol=TCP localport=5000-6000同时设置 FTP 防火墙支持,填入服务器公网 IP,否则 PASV 响应携带内网地址,外部客户端连不上 227 返回的端口。配合抓包验证:客户端发出 PASV 后,227 响应里的 IP 是否就是客户端能访问到的地址。
5.3 用 Windows 自带 ftp.exe 的 debug 模式做协议对照
最后的手段是用系统 ftp.exe 在命令行手工重现一次完整传输,打开调试开关观察每条命令:
ftp> debug ftp> open 192.168.1.50 ftp> user ftp_user ftp_password ftp> passive ftp> dirdebug 模式会在每条命令前打印 ---> 前缀,响应码原样保留。拿这份输出与 FluentFTP 日志或者 FtpWebRequest 的异常信息做对照,能确定 501 是发生在 USER 前还是 STOR 后,也能确认 PASV 是否真正发出,多数 FTP 客户端疑难因此从猜测变成具体命令的定位。
本文还有配套的精品资源,点击获取