用C#上传文件到FTP服务器,最直接的方法是使用System.Net.FtpWebRequest类,通过创建FTP请求、写入文件流并获取响应来完成,整个流程大约十几行核心代码就能跑通。如果你正在折腾文件同步或备份功能,这篇文章会一步步讲透从基础上传到断线重传的完整思路。
C# FTP上传的核心逻辑与准备工作
FTP上传说白了就是把本地文件通过FTP协议推送到远程服务器,C#里没有比FtpWebRequest更原生的方案了,它属于.NET框架自带的能力,不需要额外安装NuGet包,在控制台、WinForm、ASP.NET Core里都能用。
动手写代码前,先确认三件事:
- 服务器地址要带上协议头,比如
ftp://192.168.1.100/upload/,别漏了ftp://前缀。 - 如果服务器路径对应的是子目录,先确认目录存在,或者代码里写创建目录的逻辑。
- FTP账号密码建议从配置文件读取,别硬编码在源码里,不然以后换密码还得重新编译。
核心思路分四步:建立连接、设置请求类型、把文件字节写入请求流、等待服务器返回状态,下面看具体代码。
最简实现:用FtpWebRequest逐步上传
假设你本地有个D:\temp\report.pdf,要传到ftp://192.168.1.100/files/report.pdf,账号是user,密码是pass,直接看这段:
using System;
using System.IO;
using System.Net;
public static void UploadFile(string localPath, string remotePath, string ftpHost, string ftpUser, string ftpPass)
{
string fullUri = ftpHost + remotePath;
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(fullUri);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = new NetworkCredential(ftpUser, ftpPass);
request.UseBinary = true;
request.UsePassive = true;
request.KeepAlive = false;
byte[] fileData = File.ReadAllBytes(localPath);
request.ContentLength = fileData.Length;
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(fileData, 0, fileData.Length);
}
using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
{
Console.WriteLine($"上传完成,状态:{response.StatusDescription}");
}
}
注意几个细节:
UseBinary = true表示二进制传输,传图片、压缩包、PDF必须设成true,否则文件会损坏。UsePassive = true走被动模式,大多数服务器都支持,主动模式容易被防火墙卡住。KeepAlive = false每次上传完就断开连接,避免连接池占着不放。
这段代码能跑,但有个小问题:File.ReadAllBytes会把整个文件加载进内存,传大文件(比如几个GB的视频)内存直接爆掉,所以下面给出流式上传的写法。
大文件流式上传:不占内存的写法
把文件切成小块,边读边写,内存占用几乎恒定:
public static void UploadFileStream(string localPath, string remoteUrl, NetworkCredential cred)
{
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(remoteUrl);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = cred;
request.UseBinary = true;
request.UsePassive = true;
request.KeepAlive = false;
request.Timeout = 30000;
using (FileStream fs = File.OpenRead(localPath))
{
request.ContentLength = fs.Length;
using (Stream reqStream = request.GetRequestStream())
{
byte[] buffer = new byte[8192]; // 8KB缓冲块
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
reqStream.Write(buffer, 0, bytesRead);
}
}
}
using (var resp = (FtpWebResponse)request.GetResponse())
{
Console.WriteLine($"状态码:{(int)resp.StatusCode}");
}
}
这里用File.OpenRead打开文件流,请求流是网络流,两个流之间用固定大小的缓冲搬运数据,CPU和内存都稳,业内专家指出,大部分FTP服务器对单次连接的超时限制在30秒左右,所以Timeout设置成30000毫秒是比较稳妥的起步值。
上传目录自动创建与文件名冲突处理
很多实际场景中,远程目录是按日期或业务ID动态生成的,比如ftp://xxx/2026/04/12/,FTP协议本身没有“创建目录”的标准命令,但可以通过MakeDirectory方法实现。
递归创建远程目录的实用技巧
思路很简单:把远程路径按切段,一段一段检查,不存在就创建,不过服务器不提供判断目录是否存在的直接接口,通常用GetFileSize或ListDirectory试探,抛出异常就说明不存在。
public static void EnsureRemoteDirectory(string ftpHost, string directoryPath, NetworkCredential cred)
{
string[] segments = directoryPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
string current = ftpHost.TrimEnd('/');
foreach (string seg in segments)
{
current += "/" + seg;
if (!DirectoryExists(current, cred))
{
FtpWebRequest req = (FtpWebRequest)WebRequest.Create(current);
req.Method = WebRequestMethods.Ftp.MakeDirectory;
req.Credentials = cred;
using (req.GetResponse()) { }
}
}
}
private static bool DirectoryExists(string dirUrl, NetworkCredential cred)
{
try
{
FtpWebRequest req = (FtpWebRequest)WebRequest.Create(dirUrl + "/");
req.Method = WebRequestMethods.Ftp.ListDirectory;
req.Credentials = cred;
using (req.GetResponse()) { }
return true;
}
catch (WebException)
{
return false;
}
}
注意DirectoryExists里dirUrl后面要加,否则部分服务器会当成文件来查。
同名文件覆盖还是跳过
这取决于业务需求,没有绝对答案,C#里控制覆盖只需一个FtpWebRequest属性:request.Method = UploadFile本身就是覆盖上传,服务器默认会替换同名文件,如果你想保留远端旧文件,得先检查是否存在:
// 检查远程文件是否存在
private static bool RemoteFileExists(string fileUrl, NetworkCredential cred)
{
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(fileUrl);
request.Method = WebRequestMethods.Ftp.GetFileSize;
request.Credentials = cred;
try
{
using (request.GetResponse()) { return true; }
}
catch (WebException ex)
{
FtpWebResponse response = (FtpWebResponse)ex.Response;
if (response != null && response.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable)
return false;
throw;
}
}
行业共识认为,判断文件是否存在的最可靠方式是发送GetFileSize请求,返回成功说明文件在,返回550错误说明不存在,这个方法比ListDirectory再解析文件名更省事。
断点续传与进度显示
传了一半断网,重新传整个文件太亏,FTP协议支持AppendFile模式,从文件末尾继续写,可惜FtpWebRequest对Append的支持有些坑,.NET Framework里可以用request.Method = WebRequestMethods.Ftp.AppendFile,但.NET Core/5+里这个方法有兼容性问题,会抛出ProtocolError。
可靠的做法是手动实现续传逻辑:先查远程文件大小,从那个偏移量开始读本地文件,然后设置请求头的ContentOffset。
带进度回调的断点续传实现
public static void UploadWithResume(string localPath, string remoteUrl, NetworkCredential cred, Action<long, long> progressCallback)
{
long existingSize = GetRemoteFileSize(remoteUrl, cred);
long offset = existingSize > 0 ? existingSize : 0;
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(remoteUrl);
request.Method = WebRequestMethods.Ftp.UploadFile; // 注意:还是UploadFile
request.Credentials = cred;
request.UseBinary = true;
request.UsePassive = true;
request.ContentOffset = offset; // 关键:从偏移量开始写
using (FileStream fs = File.OpenRead(localPath))
{
if (offset >= fs.Length)
{
Console.WriteLine("文件已存在且大小一致,无需上传");
return;
}
fs.Seek(offset, SeekOrigin.Begin);
long totalBytes = fs.Length;
byte[] buffer = new byte[8192];
int bytesRead;
long sentBytes = offset;
using (Stream reqStream = request.GetRequestStream())
{
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
reqStream.Write(buffer, 0, bytesRead);
sentBytes += bytesRead;
progressCallback?.Invoke(sentBytes, totalBytes);
}
}
}
using (var resp = (FtpWebResponse)request.GetResponse())
{
Console.WriteLine(resp.StatusDescription);
}
}
ContentOffset这个属性是FtpWebRequest专门用来续传的,客户端告诉服务器“我这次只传后半段”,服务器会从指定位置接着写,不过要注意,部分FTP服务器软件(比如早期Serv-U)不支持这个特性,会直接报错,遇到这种情况,要么换WebClient配合自定义协议,要么放弃续传直接全量覆盖。
进度回调的好处是你可以接一个进度条,progressCallback里收到的是已传字节和总字节,算百分比就完事了。
异步上传与超时重试机制
UI线程阻塞是WinForm和WPF里常犯的错,上传大文件时界面卡死,用户以为程序崩溃了,用async/await配合Task.Run或者直接调用异步方法能解决。
基于FtpWebRequest的异步方法封装
FtpWebRequest本身没有内置异步方法,但可以借助Task.Factory.FromAsync包装,或者直接放到线程池里跑,更简洁的做法是用WebClient的UploadFileAsync,它支持异步和进度事件:
using (WebClient client = new WebClient())
{
client.Credentials = cred;
client.UploadProgressChanged += (s, e) =>
{
Console.WriteLine($"已上传 {e.BytesSent} / {e.TotalBytesToSend}");
};
await client.UploadFileTaskAsync(remoteUrl, localPath);
}
UploadFileTaskAsync是.NET 4.5以上提供的异步方法,返回Task,配合await使用,UI线程不阻塞,缺点是没有ContentOffset,不能断点续传,所以小文件优先用WebClient,大文件用FtpWebRequest手动封装。
上传失败重试策略
网络抖动是家常便饭,直接失败重传最稳妥,简单的重试循环:
public static async Task UploadWithRetryAsync(string localPath, string remoteUrl, NetworkCredential cred, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
await UploadFileTaskAsync(localPath, remoteUrl, cred);
return;
}
catch (Exception ex) when (attempt < maxRetries)
{
Console.WriteLine($"第{attempt}次上传失败:{ex.Message},2秒后重试");
await Task.Delay(2000);
}
}
}
这里UploadFileTaskAsync是自己写的异步封装,可以用上面的UploadWithResume改造,也可以直接用UploadFileStream的异步版本,重试间隔别太短,2秒是比较中庸的选择。
推荐第三方库:WinSCP .NET与FluentFTP
如果你觉得FtpWebRequest太底层,处理不了SFTP、加密、复杂目录树,那就不用硬扛,社区里有两个成熟方案:
| 库名 | 协议支持 | 断点续传 | 异步支持 | 适用场景 |
|---|---|---|---|---|
| FluentFTP | FTP, FTPS | 纯FTP场景,API简洁 | ||
| WinSCP .NET | FTP, SFTP, FTPS | 需要SFTP,或想用WinSCP GUI逻辑 |
FluentFTP用法就像写普通代码:
using (var conn = new FtpClient("192.168.1.100", "user", "pass"))
{
conn.Connect();
conn.UploadFile(localPath, remotePath, FtpRemoteExists.Overwrite, true, new Progress<double>(p => Console.WriteLine(p)));
}
它内部处理了目录创建、重试、编码等琐碎问题,适合快速交付,如果你问c#怎么将文件上传到ftp服务器最省心,直接引入FluentFTP就是答案。
WinSCP .NET更适合对可靠性要求极高的场景,它封装的是WinSCP命令行逻辑,支持断点续传、同步、权限设置,而且背后的WinSCP有二十年历史,稳定性经过大量验证。
常见问题排查
上传报错很大一部分出在路径和权限上,下面列几个高发问题:
| 异常信息 | 原因 | 解决方法 |
|---|---|---|
(550) File unavailable |
远程目录不存在 | 先调用EnsureRemoteDirectory |
(530) Not logged in |
账号密码错误,或IP被限制 | 检查凭据,确认FTP账号是否只能从特定IP登录 |
(425) Can't open data connection |
被动模式被服务器防火墙拦截 | 切换UsePassive = false试试 |
(500) Invalid command |
服务器不支持当前FTP命令 | 换WebRequestMethods.Ftp里的其他枚举,或改用SFTP |
System.Net.Sockets.SocketException |
服务器域名解析失败或端口不通 | 用telnet ip 21测试端口连通性 |
补充一条:公司内网里的FTP服务器,如果绑定的是活动目录用户,密码就不能随便改,改之前要通知IT。
从本地上传的完整示例代码
把前面零散的逻辑拼成一个可直接运行的类,包含目录创建、上传、重试:
public class FtpUploader
{
private readonly string _host;
private readonly NetworkCredential _cred;
public FtpUploader(string host, string user, string pass)
{
_host = host.TrimEnd('/') + "/";
_cred = new NetworkCredential(user, pass);
}
public void Upload(string localPath, string remoteSubPath)
{
string fullRemotePath = _host + remoteSubPath;
string remoteDir = fullRemotePath.Substring(0, fullRemotePath.LastIndexOf('/'));
EnsureRemoteDirectory(remoteDir);
UploadFileStream(localPath, fullRemotePath);
}
// 具体方法实现参考前文...
}
这段代码可以直接贴进项目里改改路径就能用,或者结合你自己的日志模块把上传状态记下来。
掌握了FtpWebRequest就等于掌握了C# FTP上传的地基,再配合FluentFTP这类工具,应对日常文件同步绰绰有余,别忘了上传前检查目录、大文件用流式读写、关键时刻断点续传,这三点做好了,你的FTP功能就能稳定跑很久。
关于C# FTP上传的常见问题
C#怎么将文件上传到FTP服务器并自动创建子目录?
先按拆分远程路径,逐级调用MakeDirectory创建不存在的目录,再执行上传,可以复用前文EnsureRemoteDirectory方法,注意每次创建后要释放响应流,否则连接数会涨。
FTP上传大文件时进度条卡住不动,怎么回事?
多半是同步上传阻塞了UI线程,改用UploadFileTaskAsync或者把上传逻辑放进Task.Run,同时在UploadProgressChanged事件里更新进度条控件,界面就会流畅更新。
FluentFTP和FtpWebRequest哪个更适合做批量上传?
批量上传涉及遍历本地目录、多线程并发和错误重试,FluentFTP提供了现成的UploadDirectory方法,能递归上传整个文件夹,FtpWebRequest则要自己写循环和异常处理,开发效率要求高就选FluentFTP,想减少依赖就自己写底层。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/785497.html


