简介面向 C# 初学者的 FTP 下载实例源码完整演示基于 System.Net 的 FTP 网络操作适合需要在 WinForm 项目中实现文件传输的开发者。源码覆盖连接 FTP 服务器、登录鉴权、创建 FtpWebRequest 请求、启用 SSL、获取响应流、流式写入本地文件与资源释放等关键环节掌握后可自行扩展断点续传、下载进度显示和多线程并行下载等功能。压缩包共 21 个文件大小约 65KB主要包含 cs 源文件、exe 可执行程序、sln/csproj 工程文件以及 resx/resources 资源文件另有说明文档和调试符号其中 txt 说明可辅助梳理工程结构与使用方式解压后可快速用 Visual Studio 打开并查看完整工程结构。代码将界面逻辑与下载操作分离目录结构清晰便于逐段阅读和二次开发。目前已有 692 人学习适合希望通过具体案例理解 FtpWebRequest 与 FtpWebResponse 工作原理、提升网络编程能力的开发者。1. 先别急着写代码C# FTP下载实例源码到底要解决什么做C#上位机和桌面工具的人基本都绕不开C# FTP下载这类网络操作。很多人第一反应是WebClient.DownloadFile一行搞定可真要在生产环境里跑主动/被动模式、断点续传、中文文件名哪一步都能让程序在客户现场翻车。这篇要讲的是我一直在用的基于FtpWebRequest的C# FTP下载实例源码覆盖从连接、下载、进度回传到断点续传的完整链路适合做WinForms、WPF、Windows服务里文件同步和FTP监控的开发者。跟着代码走一遍能避开大多数线上才暴露的问题。2. FTP下载前的底层准备协议模式与C#网络操作选型2.1 主动模式和被动模式为什么连上了却下不了多半是模式问题写C# FTP下载之前先得把FTP的工作方式搞清楚。FTP和HTTP不一样它不是一条连接打天下而是有两条独立的连接控制连接默认走21端口用来传USER、PASS、PASV、PORT这类命令数据连接才是真正传文件内容的通道。具体怎么建立数据连接就是主动Active和被动Passive模式的差别。主动模式是客户端告诉服务器一个IP和端口服务器主动连过来。听起来挺合理但这里的客户端告诉服务器的IP有个历史包袱PORT命令里带的IP是客户端在控制连接里声明的地址。如果客户端在NAT后面、或者在Windows防火墙上只放了出站规则服务器根本连不回这个端口表现出来就是控制连接正常、登录也成功一执行RETR下载就卡住等超时直接报错。而被动模式正好反过来是服务器开一个端口告诉客户端你连这个吧客户端作为发起方走的是出站连接。对绝大多数C#上位机、Windows服务来说出站连接比入站连接容易放行得多所以FtpWebRequest把UsePassive默认设成true是有道理的。但实际项目里会遇到反过来的情况。有些老式FTP服务器、或某些定制嵌入式设备的FTP精简实现只支持主动模式你开被动模式它要么不响应PASV要么返回的227 Entering Passive Mode里带的是内网地址客户端隔了一段网络根本连不过去。我曾在客户现场遇到过一台设备被动模式返回的IP是它自己局域网的地址我的程序跑在另一段网络里连接直接超时。这种问题光看代码看不出名堂只能把UsePassive改成false再试试通了就按主动模式处理。所以在网络操作联调时模式是第一要确认的变量不要迷信默认值。2.2 FtpWebRequest 和 WebClient 怎么选下载场景里别只看代码行数很多刚接触C# FTP下载的开发者会在WebClient和FtpWebRequest之间纠结。WebClient确实省事一行DownloadFile就能把文件拉下来但它把FTP细节全封装死了你拿不到进度、控制不了主动被动模式、做不了断点续传超时策略也很死板。一旦文件超过几百兆或网络不稳定WebClient几乎只能要么成功要么重下在FTP监控这类需要增量同步的场景里根本不够用。FtpWebRequest是System.Net里专门为FTP设计的类。它的API确实老气属性名也不够现代但它把FTP的每个关键要素都暴露出来了Method决定你发什么命令UsePassive控制数据连接模式ContentOffset对应REST命令做断点续传KeepAlive控制控制连接是否复用ReadWriteTimeout单独管流读写超时。这些属性不是摆设每一组都对应实际网络操作里的一个坑。我的选择标准很简单一次性小文件、不考虑恢复可以用WebClient只要涉及进度条、断点续传、定时同步、多文件批量下载就老老实实写FtpWebRequest。还有一个因素是环境约束。不少C#上位机项目跑在离线内网里NuGet包拉不下来或者公司安全策略不允许引入第三方依赖。在这种前提下FtpWebRequest是.NET Framework和.NET 6都自带的方案不用装任何东西。如果允许引第三方包FluentFTP确实好用功能全、还解决了中文编码但我的建议依然是先用框架自带API把下载流程跑通再决定值不值得引依赖。自己手写的实例源码出了任何问题你都能一层层剥开看这比依赖包里的黑匣子好排查得多。2.3 连接前先确认的三件事URI格式、凭据和端口写代码前先把这三件事确认好不然代码一跑全是低级报错。第一FTP URI的格式是ftp://host:port/path/filehost可以是域名也可以是IP。端口不是21的时候必须写进URI漏了端口会连到21上去连不上一看就是半天。路径里的目录和文件名最好统一用正斜杠Windows用户习惯的反斜杠在FTP URI里会被转义搞乱在WinForms里敲路径尤其容易踩这个坑。第二凭据的写法。最安全的方式是request.Credentials new NetworkCredential(userName, password)不要去拼ftp://用户名:密码host这种带凭据的URI因为密码里有、#这类特殊字符时URL解析会直接出错而且日志里一旦打出URI就会把密码带出去。匿名FTP可以传空凭据但现在绝大多数FTP软件都默认禁匿名登录所以还是跟服务器管理员要到真实账号再测。第三要分清服务器端用的是主动还是被动以及被动模式下开了哪个端口段。你可以在cmd下敲ftp命令连上去输入quote PASV看返回的227响应里给的IP和端口。能覆盖到这一步说明服务器工作正常问题大概率出在客户端网络。如果返回的是500 Illegal PORT command或425 Use Port or Pasv first基本可以断定服务器配置或客户端模式有问题别急着改代码先去查服务器。2.4 先用十行代码验证连通性把网络操作排错范围缩小在写下载逻辑之前我习惯先做一个最小连通测试用FtpWebRequest请求一次文件大小。这个请求如果通了说明凭据、URI、网络路径都没问题后面下载失败就只可能出在数据流环节。public static bool TestFtpConnection(string ftpFileUri, string userName, string password) { try { FtpWebRequest request (FtpWebRequest)WebRequest.Create(ftpFileUri); request.Method WebRequestMethods.Ftp.GetFileSize; request.Credentials new NetworkCredential(userName, password); request.UseBinary true; request.UsePassive true; request.Proxy null; using (FtpWebResponse response (FtpWebResponse)request.GetResponse()) { return true; } } catch (WebException ex) { return false; } }逻辑说明GetFileSize对应FTP的SIZE命令服务器支持就会返回文件长度不支持会报550这里把返回当作连通标志只做粗略验证。request.Proxy null这行在Windows工作站上尤其关键因为FtpWebRequest默认会走系统IE代理设置有些内网环境里自动配置脚本会用HTTP代理去代理FTP导致明明直连能通代码里却报错。设成null就是明确告诉它不走代理。参数说明ftpFileUri必须是文件的完整路径不能是目录SIZE命令只能查文件。userName/password按服务器实际配置填。UsePassive先按true填验证不通用再改成false对比这本身就是一次网络模式探测。这个方法返回的是bool线上环境不建议这么简化但作为联调入口非常合适。这也说明一个问题FTP排错一定要分层。协议层通不通、认证通不通、数据连接通不通、传输完不完整这四层分开看不要拿着下载代码一把梭。3. 写出可复用的下载实例源码从单文件到带进度回调3.1 最小可运行的下载方法FtpWebRequest七个关键属性先给最基础的版本。逻辑很直白创建一个FtpWebRequest配好方法和凭据拿到响应流把流复制到本地文件。public static void DownloadFile(string ftpUri, string localPath, string userName, string password) { FtpWebRequest request (FtpWebRequest)WebRequest.Create(ftpUri); request.Method WebRequestMethods.Ftp.DownloadFile; request.Credentials new NetworkCredential(userName, password); request.UseBinary true; request.UsePassive true; request.KeepAlive false; request.Timeout 10000; request.ReadWriteTimeout 30000; using (FtpWebResponse response (FtpWebResponse)request.GetResponse()) using (Stream ftpStream response.GetResponseStream()) using (FileStream fileStream new FileStream(localPath, FileMode.Create, FileAccess.Write)) { ftpStream.CopyTo(fileStream); } }这段代码在大多数FTP服务器下都能跑通。FtpWebRequest的GetResponse()会在内部走完发CWD、发TYPE、发PASV、发RETR这几个命令拿到Response后再GetResponseStream()取数据流。这里有几个属性是按经验设的UseBinary必须要true否则控制连接会按ASCII模式传输图片、压缩包、exe下下来直接损坏。UsePassive刚才讲过默认true除非你明确知道服务器只支持主动模式。KeepAlive设成false让每次请求用完就断行为更可控在批量下载场景里KeepAlivetrue能省握手时间但代价是控制连接容易变半开后面避坑章会单独讲。Timeout是建立连接的超时ReadWriteTimeout是读数据流时等待字节的间隔超时后者才是下载到一半卡住多久报错的关键默认30秒在慢速专线里会被误报。FileMode.Create直接覆盖本地文件如果要做断点续传就不能用它。补充一点GetResponse()抛WebException时如果没把response释放会造成控制连接泄漏。所以生产代码里catch到WebException后要读ex.Response并Dispose再把它封装成自己的下载异常。这个版本没有进度没有重试没有续传但它是后面所有代码的骨架。先用它验证能不能完整落一个文件再往上加功能。3.2 用Stream边读边写把下载进度回调用起来CopyTo方便但看不到进度。要做进度必须手动开一块缓冲区循环读。下面这个版本把已读字节数换算成百分比通过一个Action 回调抛出去。public static void DownloadFileWithProgress(string ftpUri, string localPath, string userName, string password, Actiondouble progressCallback) { FtpWebRequest request (FtpWebRequest)WebRequest.Create(ftpUri); request.Method WebRequestMethods.Ftp.DownloadFile; request.Credentials new NetworkCredential(userName, password); request.UseBinary true; request.UsePassive true; request.KeepAlive false; using (FtpWebResponse response (FtpWebResponse)request.GetResponse()) using (Stream ftpStream response.GetResponseStream()) using (FileStream fileStream new FileStream(localPath, FileMode.Create, FileAccess.Write)) { long totalBytes response.ContentLength; byte[] buffer new byte[81920]; long readBytes 0; int read 0; while ((read ftpStream.Read(buffer, 0, buffer.Length)) 0) { fileStream.Write(buffer, 0, read); readBytes read; if (totalBytes 0) { progressCallback?.Invoke((double)readBytes / totalBytes * 100.0); } } } }逻辑说明手写循环替代CopyTo每读一块就写一块天然就是流式处理内存占用和文件大小无关。缓冲区我用81920字节这是Stream.CopyTo默认的缓冲区大小折合80KB在多数网络环境下读写比都合适改小到4096也行但Read次数增加CPU压力变大改大不一定更好FTP数据流在慢速网络上读一大块往往要等很久。其中totalBytes来自FTP响应中的Content-Length服务器不支持SIZE命令时它是-1所以要在循环里判断只有totalBytes 0才报百分比不然会出现除以负数的尴尬。这个版本在WinForms里直接用有个坑Action回调是在当前线程触发的。如果下载跑在Task或BackgroundWorker里回调就是工作线程直接去改ProgressBar.Value会跨线程访问控件。常见做法是用IProgress 它内部会自动封送到SynchronizationContext代码改成progress.Report(percent)就行也可以自己在回调里判断InvokeRequired再做BeginInvoke。C#上位机里我建议用IProgress少写模板代码。还需要注意一个现象有些服务器在返回ContentLength之前就进入了数据传输第一次Read可能已经读到了数据此时ContentLength还是-1所以不能拿ContentLength先拿到当作可靠依据。最稳妥的是以实际读到的字节数作为下载完成判断也就是while (read 0)跳出循环而不是比较readBytes是否等于totalBytes。3.3 列目录做批量下载FTP监控场景怎么拿到远端清单单文件下载只是第一步实际需求大多是定时把某个目录下出现的新文件拉到本地这就是FTP监控的典型形态。要监控先得能列目录。FtpWebRequest提供了两种方法ListDirectory和ListDirectoryDetails。ListDirectory只返回文件名每一行一个解析容易但拿不到大小和修改时间想做增量下载、判断远端文件是否变化还是得用ListDirectoryDetails它返回的每行里带权限、大小、日期解析格式受服务器类型影响。下面是列目录的代码public static Liststring ListFtpDirectory(string ftpDirectoryUri, string userName, string password) { FtpWebRequest request (FtpWebRequest)WebRequest.Create(ftpDirectoryUri); request.Method WebRequestMethods.Ftp.ListDirectoryDetails; request.Credentials new NetworkCredential(userName, password); request.UseBinary true; request.UsePassive true; request.Proxy null; Liststring lines new Liststring(); using (FtpWebResponse response (FtpWebResponse)request.GetResponse()) using (StreamReader reader new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { string line; while ((line reader.ReadLine()) ! null) { lines.Add(line); } } return lines; }这段代码返回的是原始行集合。接下来要解析。Windows IIS FTP的格式长这样03-12-24 10:30AMuploadsLinux vsftpd的格式长这样-rw-r--r-- 1 ftp ftp 1048576 Jan 12 10:30 report.zip。两种格式差异很大硬写一个解析函数兼容两者很繁琐。我的做法是如果文件数量不大先用ListDirectory拿到文件名再对每个文件发一次GetFileSize和GetDateTimestamp用两次FTP请求换稳定的元数据如果文件上千再考虑解析ListDirectoryDetails并把解析函数做成可替换的接口。批量下载的骨架是列目录、对每个文件名拼接完整URI、调用上一节的DownloadFileWithProgress。需要注意目录URI末尾要加/如果漏了尾斜杠有些FTP服务器会返回550或把请求当成文件处理。另外批量任务里每建一个FtpWebRequest就是一个新的控制连接频繁上下线在大量小文件场景会显得很慢可以考虑用KeepAlivetrue复用但前提是你得正确处理控制连接空闲超时否则跑一晚上就卡死。这个点我在避坑章会专门展开。4. 断点续传与传输异常恢复下载到一半断了怎么接上4.1 用ContentOffset做断点续传服务器支持REST才有得玩断点续传用到的FTP命令是RESTFtpWebRequest里对应的属性叫ContentOffset。设置了这个属性请求发送时会先发REST offset服务器从指定偏移处开始传输。客户端的任务就是在下载开始前看看本地临时文件已经写了多少字节然后把这个长度设成ContentOffset。public static void DownloadFileResume(string ftpUri, string localPath, string userName, string password, Actionlong progressCallback) { long localSize File.Exists(localPath) ? new FileInfo(localPath).Length : 0; FtpWebRequest request (FtpWebRequest)WebRequest.Create(ftpUri); request.Method WebRequestMethods.Ftp.DownloadFile; request.Credentials new NetworkCredential(userName, password); request.UseBinary true; request.UsePassive true; request.ContentOffset localSize; request.KeepAlive false; using (FtpWebResponse response (FtpWebResponse)request.GetResponse()) using (Stream ftpStream response.GetResponseStream()) using (FileStream fileStream new FileStream(localPath, FileMode.Append, FileAccess.Write)) { long remoteRemainSize response.ContentLength; long totalSize remoteRemainSize localSize; byte[] buffer new byte[81920]; long totalRead localSize; int read 0; while ((read ftpStream.Read(buffer, 0, buffer.Length)) 0) { fileStream.Write(buffer, 0, read); totalRead read; if (totalSize 0) { progressCallback?.Invoke(totalRead); } } } }逻辑说明FileMode.Append是关键它会打开已有文件并往后追加。如果误用FileMode.Create断点续传的文件会被清空等于前功尽弃。ContentOffset设成localSize后ftpStream从远端剩余部分开始读响应里的ContentLength是剩余字节数不是整个文件的大小所以计算总大小要用response.ContentLength localSize进度百分比用totalRead / totalSize。这里必须处理一个边界情况如果服务器上的文件被覆盖或重置过远端文件比本地临时文件还小这时ContentOffset已经大于远端实际大小服务器可能返回550或把文件写坏。所以在续传前应该先做一次GetFileSize比较远端大小与本地大小如果远端 本地直接删掉本地文件全部重下不要硬续传。这个逻辑相当重要我见过同事没加这个判断断点续传续出一个前边是旧内容、后边是新内容的拼接文件。另一个前提是服务器支持REST命令。FileZilla Server、vsftpd、IIS FTP一般支持但部分嵌入式设备上的精简FTP不支持。不确定的话可以先在cmd下用ftp命令连上去输入rest 100看看服务器返回350还是500350表示支持500就是没戏。不支持REST的服务器断点续传只能靠客户端记录已下载的块、跳过这些块做多线程分块下载来实现复杂度会高一个级别一般场景不值得。4.2 传输中断的典型异常把远程主机强迫关闭拆开看C#做FTP下载报错文本里最高频的就是The underlying connection was closed和无法将数据写入传输连接: 远程主机强迫关闭了一个现有的连接。这两个都出现在写数据或读数据的过程中意思很直接你这一端的连接还在等数据对端已经关了只是关之前没有按TCP协议正常发FIN而是发了RST。为什么会RST常见三种第一种是服务器控制连接空闲超时。FTP服务器默认空闲300秒左右就会断开无动作的连接你的下载如果耗时很长控制连接先去睡觉了等数据传输完服务器要收ACK时发现控制连接都没了索性把数据连接也一起断掉。解决思路是缩短单次下载时间、或者下载期间保持心跳但我见过更实用的做法是接受现实做断点续传让它断了再接。第二种是防火墙/NAT设备把空闲数据连接会话回收了。办公网出口的NAT会话老化时间可能只有几十秒如果服务器在被动模式下返回的连接长时间没流量中间设备先释放了映射于是客户端收到RST。第三种是服务器主动踢连接比如同时超过最大连接数、或管理端手动断开。这类问题从代码里很难根治但可以在客户端侧做重试。排查这类异常要看两个层次。第一层是WebException的Response.StatusCodeFTP服务器在断开前通常已经发出了状态码。第二层是InnerException里的SocketErrorCode和NativeErrorCode可以帮你定位到TCP层是被RST还是被超时。catch (WebException ex) { if (ex.Response is FtpWebResponse ftpResponse) { FtpStatusCode status ftpResponse.StatusCode; // 550: 文件不存在或权限不足不重试 // 421: 服务不可用稍后重试 // 426: 连接关闭传输中止可断点续传 // 451: 本地处理错误可重试 } if (ex.InnerException is SocketException socketEx) { // SocketError.ConnectionReset / TimedOut / HostUnreachable } }逻辑说明FtpStatusCode是枚举但很多状态码在.NET里对应不太直观比如426对应ConnectionClosed。这里不要裸catch要把状态码记录下来配合日志看规律。比如每天都固定在下午三点报421那很可能不是代码问题而是服务器运维在定时重启FTP服务这种就只能错开那段时间。还要说一句别在每个异常点都做万能重试。对550这种确定性的错误重试10次都是白费对450/421这种服务器临时状态退避重试才有效。把重试策略和状态码绑定是网络操作落地时最重要的纪律。4.3 临时文件与MD5校验给断点续传一颗后悔药断点续传的隐患在于本地临时文件的存在让下载完成这件事变得模糊。如果上一次下载因为网络断开只写了300MB下一次续传前服务器上的文件又被别人改过了那么客户端最终得到的本地文件前300MB是旧版本后200MB是新版本长度对得上内容却拼接错了。所以我的下载流程固定是三步先下载到同目录的隐藏临时文件下载完成后跟服务器比对文件大小再重命名为正式文件。校验最常见的是MD5或SHA256。FTP服务器可以放一个同名.md5文件或者在文件清单里带上哈希客户端下载完算本地哈希做比对。给出一个计算文件MD5的方法private static string CalculateMD5(string filePath) { using (FileStream stream File.OpenRead(filePath)) using (MD5 md5 MD5.Create()) { byte[] hash md5.ComputeHash(stream); return Convert.ToHexString(hash); } }逻辑说明ComputeHash会把整个文件读一遍所以大文件的MD5计算本身有几秒到几十秒的开销但如果连这几个校验步骤都不做线上出问题后连文件到底从哪个环节开始坏的都查不出来。校验的最佳时机是下载完、改名之前。如果校验不通过就把临时文件删除整个文件重下不要继续在坏文件上追加。需要提醒的是Convert.ToHexString是.NET 5的API.NET Framework项目里要用BitConverter.ToString(hash).Replace(-, )自己拼接。另外服务器上MD5文件也可能下载不完整所以比对前要先判断MD5字符串长度顺手做一次长度校验。对于文件数量比较大的FTP监控场景全量MD5不现实可以用先比对长度、再抽几段字节算校验和的方式把开销降下来。这个降级方案在文件同步工具里很常见属于性能与可信度的取舍。5. FTP下载常见问题排查与避坑五条血泪经验5.1 现象cmd下ftp能下载C#程序卡死原因主动/被动模式不一致这是C# FTP下载最经典的一坑。很多工程师排查问题时习惯先用cmd里的ftp命令验证一下服务器cmd里能通就认为服务器没问题。但cmd自带ftp客户端默认走主动模式而C#的FtpWebRequest默认走被动模式。两边模式不一样于是出现命令行走得通程序里连不上的诡异现象。解决不要凭直觉认定是代码问题。第一步在代码里把UsePassive从true改成false各跑一遍记录哪一遍通哪一遍不通。第二步如果被动模式不通、能确认是防火墙阻止了服务器端口就去服务器侧或防火墙侧放行如果程序是Windows服务跑在后台还要额外确认服务账户的防火墙策略和当前登录用户是否一致。这类问题排查到最后多半是环境差异代码一行没改。5.2 现象报500 Illegal PORT command或425 Use Port or Pasv first原因命令序列错乱或代理参与这两个报错是FTP控制连接层面的典型错误。500 Illegal PORT command通常出现在主动模式下客户端发的PORT命令携带的IP和端口不被服务器接受425 Use Port or Pasv first则是还没建好数据连接就执行了传输命令。在FtpWebRequest里最常见的触发场景是KeepAlivetrue时上一个请求异常控制连接处于不干净状态下一个请求的FTP命令序列就从错误位置开始。另一个隐蔽原因就是代理Windows系统里如果IE设置了自动代理FtpWebRequest默认会绕道代理代理对FTP协议处理得不好就会把命令序列搅乱。解决传输请求尽量独享连接KeepAlive设false如果必须复用在每次catch到WebException后主动调用request.Abort()把连接彻底放弃。另一点是request.Proxy null这个一行代码能解决大量本地不设代理就正常、客户现场设了代理就报错的疑难杂症。5.3 现象英文文件名正常中文文件名乱码或下载失败原因编码不一致FTP协议历史上没有规定文件名编码Windows自带的IIS FTP常用GBKLinux上的vsftpd默认UTF-8。如果客户端用UTF-8解析GBK的目录列表中文就全是乱码用错编码去请求路径服务器直接返回550。这还是FTP监控里最常见的翻车点之一。解决先确认服务器编码再决定FtpWebRequest和StreamReader用什么编码。目录列表解析时把编码参数抽出来做成可配置项Encoding ftpEncoding Encoding.GetEncoding(GBK); using (StreamReader reader new StreamReader( response.GetResponseStream(), ftpEncoding))注意Encoding.GetEncoding(GBK)在.NET Core上默认不可用要先在程序启动时注册代码页Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)。构造下载URI时中文目录或文件名要做UrlEncode因为FTP路径在URI里的非ASCII部分如果不转义FtpWebRequest可能把字符原样发出去服务器匹配不到。这里有个细节只对路径里的目录名/文件名做转义不要对整个URI转义否则ftp://的斜杠也会被转成%2F服务器直接报错。5.4 现象FTP监控任务跑几天后变慢、卡死原因KeepAlive长连接积累半开连接定时同步场景里有人为了省去频繁建立TCP连接的握手成本把KeepAlive设为true期望同一台服务器的下载复用连接。结果程序跑了一天多下载速度越来越慢最后完全卡住。原因不复杂FTP服务器空闲超时是默认行为控制连接超时被服务器关闭后客户端不知道下一次请求继续复用这条连接发出去没人响应客户端卡在等待中。等到TCP超时重试中间又积累了一堆已经断开的连接。解决定时同步任务别用长连接每个定时周期新建FtpWebRequestKeepAlivefalse用完即弃。这样每次多花一次握手时间但换来了确定性。如果确实需要KeepAlive必须在每次GetResponse后主动检查连接状态并处理WebException后调用Abort把连接从连接池里摘掉。FTP监控场景我的默认配置就是KeepAlivefalse简单可靠。5.5 现象大文件下载时内存暴涨原因把响应流整体读进了内存这个坑多是新人在网上抄代码抄来的为了处理下载结果有人直接用StreamReader.ReadToEnd把整个FTP响应流读成字符串几百MB的文件下完进程内存直接涨到GB级别。FTP下载是流式的正确做法是边读边写缓冲区固定大小不要试图把整个文件装进内存。解决参考第三章的写法用byte[81920]循环Read/Write。还有一个容易被忽略的点FileStream写文件时默认走系统缓存大批量小文件下载会造成大量小写入可以在创建FileStream时加上FileOptions.SequentialScan提示操作系统我会顺序读帮助预读策略优化。另外不要忘了释放流using是基本要求但要注意using里一旦抛异常流释放顺序是反的响应流没有被及时Dispose可能影响下一次下载。稳妥起见在finally里手动Dispose关键流或者把每个using拆开写。6. 给实例源码加上重试与限速一个可落地的收尾技巧下载逻辑铺开后最后加两块实用的增强指数退避重试和下载限速。重试的代码骨架是循环加条件判断只在服务器临时状态码时才重试限速则是控制每秒读入的字节数避免FTP监控任务把生产带宽吃满。重试骨架for (int attempt 1; attempt 3; attempt) { try { DownloadFileWithProgress(uri, localPath, user, pwd, null); break; } catch (WebException ex) { FtpStatusCode code ((FtpWebResponse)ex.Response)?.StatusCode ?? FtpStatusCode.Undefined; if (code FtpStatusCode.ActionNotTakenFileUnavailable) throw; // 550 不重试 Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, attempt))); } }退避间隔用2的幂次递增2秒、4秒、8秒简单够用。限速的实现是让每个结算周期读进窗口的数据量不超过配额long windowBytes 0; Stopwatch sw Stopwatch.StartNew(); byte[] buffer new byte[81920]; while ((read ftpStream.Read(buffer, 0, buffer.Length)) 0) { fileStream.Write(buffer, 0, read); windowBytes read; if (windowBytes maxBytesPerSecond) { long elapsedMs sw.ElapsedMilliseconds; int needMs (int)(windowBytes * 1000 / maxBytesPerSecond) - (int)elapsedMs; if (needMs 0) Thread.Sleep(needMs); sw.Restart(); windowBytes 0; } }限速逻辑里needMs算出来是按当前速度读完这些配额应当花的时间和实际花的时间的差值高速网络下需要睡一会儿带宽不足时不需要。最后说一个我自己的习惯每次下载都会把临时文件路径、远端路径、状态码、重试次数逐条写进日志哪怕当时用不上等客户说文件不对时这些日志能直接告诉你文件是从哪个环节坏的。这个习惯救过我很多次也希望帮到你。本文还有配套的精品资源点击获取