C# FTP下载实例源码:断点续传与中文文件名处理实战

发布时间:2026/10/26 23:10:59
C# FTP下载实例源码:断点续传与中文文件名处理实战

简介:这份C# FTP下载实例源码面向具备一定.NET基础的开发者,帮助其理解并实现基于FTP协议的文件传输功能。源码围绕FtpWebRequest与FtpWebResponse类展开,涵盖连接服务器、设置登录凭据、启用SSL/TLS、获取文件列表、读取响应流并写入本地文件等完整流程,同时涉及异常处理、进度显示与多线程下载等扩展思路,适合作为网络编程入门与项目参考。资源包共21个文件,以cs源码、exe可执行文件、resx资源文件、pdb调试符号及resources资源为主,另含sln解决方案、csproj项目文件与settings配置,压缩包约65KB,结构完整可直接编译运行。目前已有693人学习下载,读者可借此掌握FTP下载的核心原理,并在此基础上扩展断点续传、文件大小检查等功能,满足更复杂的实际应用场景。

1. C# FTP下载实例源码:网络操作里最容易被低估的一环

很多做 C# 上位机、工控采集、MES 对接的朋友,第一次接到「从 FTP 服务器定时拉文件」的需求时,都会觉得这活儿简单——WebClient.DownloadFile一行就完事了。真上线才发现:大文件传到一半断了、中文文件名变乱码、被动模式下卡死、断点续传没做导致每次重头再来。FTP 下载在 C# 网络操作里属于典型的「入门容易、稳定难」,它牵扯到控制连接与数据连接的双通道模型、主动/被动模式协商、编码、超时、断线重连这一整套东西。

这篇笔记围绕 C# FTP 下载实例源码展开,把FtpWebRequest和WebClient两条主流路线的选型、可复制的下载代码、断点续传实现、以及我踩过的坑讲清楚。适合正在写 C# 上位机、需要和 FTP 服务器做文件同步的开发者,也适合想搞懂网络操作底层细节的 C# 入门到进阶的读者。读完你能直接抄走一份能跑、能续传、能处理中文名的下载源码。

2. 选型先立住:FtpWebRequest 还是 WebClient

2.1 两种 API 的能力边界对比

在动手写代码前,先把选型讲清楚,否则后面返工的成本远大于现在多花十分钟。C# 里做 FTP 下载,常见做法有三种:WebClient、FtpWebRequest、以及第三方库(如 FluentFTP)。标准库自带的就前两个,第三方库不在本文讨论范围,因为很多工控现场不允许随便引外部依赖。

WebClient的定位是「快速下载」,DownloadFile一行搞定,适合小文件、一次性拉取、不需要进度和续传的场景。它的黑匣子在于:内部其实也是封装FtpWebRequest,但把很多可调参数藏起来了,你没法直接控制被动模式、超时、缓冲区大小。

FtpWebRequest是底层 API,继承自WebRequest,能精确控制Method、UsePassive、UseBinary、Timeout、ReadWriteTimeout、ContentLength、Credentials。断点续传、进度回调、自定义编码都得靠它。代价是代码量大,异常处理要自己写全。

维度WebClientFtpWebRequest
代码量极少中等
断点续传不支持支持(ContentOffset)
进度回调需事件手动读流
被动模式控制隐式显式 UsePassive
超时控制有限完整
适用场景小文件、脚本大文件、生产环境

结论很直接:生产环境的 FTP 下载,一律用 FtpWebRequest。WebClient 只适合写个临时脚本验证服务器通不通。

2.2 主动模式与被动模式的坑

FTP 和普通 HTTP 最大的区别是它用两条连接:控制连接(21 端口)传命令,数据连接传文件内容。数据连接谁主动发起,就分成了主动(Active)和被动(Passive)两种模式。

主动模式下,客户端告诉服务器「你来连我的某个端口」,服务器主动发起数据连接。问题在于客户端往往在 NAT 或防火墙后面,服务器连不进来,直接卡死。被动模式反过来,客户端去连服务器开的一个临时端口,穿透性更好。

在 C# 里,FtpWebRequest.UsePassive默认是true,也就是被动模式。但很多老工控服务器、某些 IIS FTP 配置下被动模式端口范围没开全,反而要改成主动。我的血泪经验是:先试被动,连不上再切主动,别一上来就写死。切换就一行:

request.UsePassive = true; // 默认被动,穿透 NAT 友好;服务器端口受限时改 false

UseBinary也必须设成true,否则服务器按 ASCII 传输,二进制文件(exe、图片、压缩包)会被改字节,下载下来直接损坏。这个坑非常隐蔽,文件大小看着对,打开就是坏的。

request.UseBinary = true; // 二进制传输,防止文件被 ASCII 转换破坏 request.UsePassive = true; // 被动模式 request.Method = WebRequestMethods.Ftp.DownloadFile;

2.3 最小可运行下载代码

先给一份能直接跑的最小实例,把骨架立起来。这段代码下载单个文件到本地,包含基本的异常处理。

using System; using System.IO; using System.Net; public class FtpDownloader { public static void Download(string ftpUrl, string user, string pass, string localPath) { // 创建 FTP 请求,ftpUrl 形如 ftp://host/path/file.zip FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUrl); request.Method = WebRequestMethods.Ftp.DownloadFile; request.Credentials = new NetworkCredential(user, pass); request.UseBinary = true; // 二进制,避免文件损坏 request.UsePassive = true; // 被动模式 request.Timeout = 30000; // 控制连接超时 30 秒 request.ReadWriteTimeout = 60000; // 数据读写超时 60 秒 using (FtpWebResponse response = (FtpWebResponse)request.GetResponse()) using (Stream responseStream = response.GetResponseStream()) using (FileStream fileStream = new FileStream(localPath, FileMode.Create)) { byte[] buffer = new byte[8192]; // 8KB 缓冲区 int read; while ((read = responseStream.Read(buffer, 0, buffer.Length)) > 0) { fileStream.Write(buffer, 0, read); } } } }

逻辑说明:WebRequest.Create根据ftp://前缀返回FtpWebRequest;GetResponse这一步才真正建立连接并发送命令,异常基本都在这里抛;GetResponseStream拿到的是数据连接的内容流,循环读到本地文件。

参数说明:Timeout管的是控制连接的命令响应等待,ReadWriteTimeout管的是数据流读写,两个都要设,只设一个另一个会用默认值(默认是 300 秒,现场网络抖动时体验极差)。缓冲区 8KB 是通用值,局域网可以调到 64KB 提速,公网保持 8KB 更稳。

3. 断点续传与进度回调:把下载做成生产级

3.1 用 ContentOffset 实现断点续传

大文件下载最怕断线重来。FTP 协议本身支持REST命令指定从哪个字节开始传,C# 里对应FtpWebRequest.ContentOffset属性。思路是:下载前先看本地已存在文件多大,然后从那个偏移量继续。

public static void DownloadWithResume(string ftpUrl, string user, string pass, string localPath) { long existingLength = 0; // 本地已有文件则记录已下载字节数,作为续传起点 if (File.Exists(localPath)) { existingLength = new FileInfo(localPath).Length; } FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUrl); request.Method = WebRequestMethods.Ftp.DownloadFile; request.Credentials = new NetworkCredential(user, pass); request.UseBinary = true; request.UsePassive = true; request.ContentOffset = existingLength; // 关键:从已下载位置继续 using (FtpWebResponse response = (FtpWebResponse)request.GetResponse()) using (Stream responseStream = response.GetResponseStream()) // 续传用 Append,首次下载用 Create using (FileStream fileStream = new FileStream(localPath, existingLength > 0 ? FileMode.Append : FileMode.Create)) { byte[] buffer = new byte[8192]; int read; while ((read = responseStream.Read(buffer, 0, buffer.Length)) > 0) { fileStream.Write(buffer, 0, read); } } }

逻辑说明:ContentOffset告诉服务器「从第 N 字节开始发」,服务器返回 206 或 350 响应。本地文件用FileMode.Append追加,保证已下载部分不被覆盖。

参数说明:ContentOffset必须和本地文件实际长度严格一致,否则文件会错位损坏。所以续传前最好校验一下服务器端文件大小(用GetFileSize方法),本地长度超过服务器长度说明文件已损坏,要删掉重下。这个校验别省,我见过因为本地文件被别的进程写坏导致续传后整个包解压失败的案例。

3.2 进度回调与状态栏更新

上位机里下载没进度条,用户会以为程序卡死。进度回调的核心是:先拿到文件总大小,再在读取循环里累加已读字节,算百分比。总大小通过GetFileSize单独发一次请求获取。

public static long GetFileSize(string ftpUrl, string user, string pass) { FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUrl); request.Method = WebRequestMethods.Ftp.GetFileSize; // 只取大小,不传内容 request.Credentials = new NetworkCredential(user, pass); request.UseBinary = true; request.UsePassive = true; using (FtpWebResponse response = (FtpWebResponse)request.GetResponse()) { return response.ContentLength; // 服务器返回的文件字节数 } }

拿到总大小后,在下载循环里回调进度。注意进度回调要跨线程更新 UI,WinForm 里用Invoke,别直接改控件,否则会抛跨线程异常。

long total = GetFileSize(ftpUrl, user, pass); long downloaded = existingLength; byte[] buffer = new byte[8192]; int read; while ((read = responseStream.Read(buffer, 0, buffer.Length)) > 0) { fileStream.Write(buffer, 0, read); downloaded += read; int percent = (int)(downloaded * 100 / total); // 回调进度,UI 层负责 Invoke 到主线程 onProgress?.Invoke(percent); }

参数说明:GetFileSize会多一次网络往返,文件多的时候可以缓存结果。百分比用long运算再转int,避免大文件(超过 2GB)时int溢出。缓冲区大小和进度回调频率要平衡,8KB 一次回调对 UI 来说太频繁,可以每读 10 次回调一次,减少 UI 线程压力。

3.3 中文文件名与编码处理

中文文件名乱码是 FTP 下载的高频翻车点。FTP 协议早期用 ASCII,中文名靠服务器和客户端约定编码。C# 里FtpWebRequest默认用 UTF-8,但很多老服务器用 GBK。

// 服务器用 GBK 时,需要显式指定编码 request.Headers["Accept-Encoding"] = "gzip"; // 与编码无关,示例占位 // 更可靠的做法:URL 里的中文先做 URL 编码 string encodedName = Uri.EscapeUriString("测试文件.zip"); string ftpUrl = $"ftp://host/path/{encodedName}";

逻辑说明:Uri.EscapeUriString把中文转成百分号编码,服务器按自己的编码解码。如果服务器是 GBK,光靠这个还不够,得在拿到文件名后手动用Encoding.GetEncoding("GBK")转换。

参数说明:判断服务器编码最直接的办法是用 FileZilla 连一次,看它显示的文件名对不对。FileZilla 能正确显示,说明服务器用的是它自动探测的编码,通常是 UTF-8。如果 FileZilla 也乱码,那服务器多半是 GBK,代码里就得手动转。这个排查方法比翻文档快得多。

4. 避坑与排查:FTP 下载最常见的 5 个翻车现场

4.1 现象:连接卡死,GetResponse 一直不返回

原因:被动模式下服务器数据端口没在防火墙开放,或者主动模式下客户端在 NAT 后面服务器连不进来。这是 FTP 最经典的网络问题。

解决:先切UsePassive的值试另一个模式。如果服务器在内网、客户端也在内网,主动模式往往更快;跨公网一律被动。还不行就用 FileZilla 连同一台服务器,看它日志里数据连接用的哪个端口,对比防火墙策略。别在代码里死磕,先用工具确认网络通不通。

4.2 现象:下载的文件大小对,但打开损坏

原因:UseBinary没设成true,服务器按 ASCII 模式传输,把\r\n做了转换,二进制文件字节被改。

解决:request.UseBinary = true必须显式设置。注意WebClient默认也是二进制,但FtpWebRequest的默认值在不同 .NET 版本里行为有差异,别赌默认值,写死。

4.3 现象:中文文件名下载报 550 找不到文件

原因:URL 里的中文没做编码,或者客户端编码和服务器不一致。

解决:URL 里的中文用Uri.EscapeUriString编码。如果还不行,用Encoding.GetEncoding("GBK")手动转一遍再拼 URL。排查时先用 FileZilla 确认服务器上文件名的真实编码。

4.4 现象:大文件下载到 99% 失败,重试又从 0 开始

原因:没做断点续传,或者续传时ContentOffset和本地文件长度不一致。

解决:用第 3 章的续传代码,续传前校验本地文件长度。另外ReadWriteTimeout要设够,大文件传输时间长,默认 300 秒可能不够,公网环境设到 120 秒以上,局域网可以短一些。

4.5 现象:多线程同时下载,部分文件内容串了

原因:多个FtpWebRequest共用同一个连接池,或者本地文件路径冲突。

解决:每个下载任务用独立的FtpWebRequest实例,本地文件名加唯一后缀(如 GUID)避免冲突。FTP 服务器对并发连接数有限制,一般 2 到 5 个并发就够,别开几十个线程,服务器会拒绝连接。这个限制在 IIS FTP 里可以配,但生产环境别去动服务器配置,客户端控制并发更稳妥。

5. 进阶技巧:把下载封装成可复用的类库

5.1 一个带重试和校验的下载封装

前面几章的代码是散的,实际项目里应该封装成一个类,把重试、校验、日志都收进去。下面这个封装我用了好几年,核心是「失败自动重试 + 大小校验 + 断点续传」三件套。

public class FtpClient { private readonly string _host, _user, _pass; public FtpClient(string host, string user, string pass) { _host = host; _user = user; _pass = pass; } // 带重试的下载,maxRetry 次失败后抛异常 public void Download(string remotePath, string localPath, int maxRetry = 3) { for (int i = 0; i < maxRetry; i++) { try { DoDownload(remotePath, localPath); // 下载后校验大小,不一致视为失败 long remoteSize = GetFileSize(remotePath); long localSize = new FileInfo(localPath).Length; if (remoteSize == localSize) return; throw new IOException($"大小不一致 远端{remoteSize} 本地{localSize}"); } catch (Exception ex) { if (i == maxRetry - 1) throw; System.Threading.Thread.Sleep(2000 * (i + 1)); // 退避重试 } } } private void DoDownload(string remotePath, string localPath) { // 复用第 3 章的续传逻辑 // ... } private long GetFileSize(string remotePath) { // 复用第 3.2 节的 GetFileSize // ... } }

逻辑说明:Download是外层重试壳,DoDownload是实际下载,下载完立刻校验大小。大小不一致说明传输被截断,抛异常触发重试。重试间隔用2000 * (i + 1)做退避,避免服务器刚恢复就被打满。

参数说明:maxRetry默认 3 次,工控现场网络差可以调到 5。退避基数 2000ms 是经验值,太短没意义,太长用户等不及。校验大小这步别省,它是发现「静默损坏」的唯一手段——有些网络问题不会抛异常,但文件就是少了几 KB。

5.2 用哈希校验替代大小校验

大小校验有个盲区:文件大小对但内容错了(比如续传偏移错位)。更严格的做法是下载完算 MD5 或 SHA256,和服务器端比对。服务器端哈希可以通过 FTP 的SITE命令或单独的文件获取,但很多服务器不支持。退而求其次,可以在服务器上放一个.md5校验文件,下载时一起拉下来比对。

using System.Security.Cryptography; public static string ComputeMd5(string filePath) { using (var md5 = MD5.Create()) using (var stream = File.OpenRead(filePath)) { byte[] hash = md5.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", "").ToLower(); } }

逻辑说明:MD5.Create创建哈希算法实例,ComputeHash一次性读完文件算摘要。大文件用流式读取,别把整个文件读进内存。

参数说明:MD5 对文件完整性校验足够,不用上 SHA256,性能差一截。哈希比对失败说明文件损坏,删掉重下,别尝试修复。这个校验在文件数量多的时候会增加 IO 开销,可以只在关键文件上做。

5.3 我踩过的最后一个坑

封装类库时,我一度把FtpWebRequest实例做成静态复用,想着省创建开销。结果多线程下载时连接状态互相污染,A 文件的响应流读到了 B 文件的内容,排查了两天才定位到。FtpWebRequest 绝对不能跨线程复用,每次下载都要新建实例。这个教训让我后来所有网络操作的封装都坚持「一次请求一个实例」的原则,宁可多创建几次,也不共享状态。

另外,FtpWebResponse和Stream一定要用using包住,或者手动Dispose。FTP 连接不释放会占着服务器的连接数配额,下载几十个文件后服务器直接拒绝新连接,现象是「前面都好好的,突然全部失败」。这个后悔药不好吃,养成随手释放的习惯。

希望帮到你。

本文还有配套的精品资源,点击获取