☰
C# 实现 SFTP 上传下载带进度条:SSH.NET 原理与避坑指南
2026/10/8 14:35:20 网站建设 项目流程

简介:面向C#开发者的SFTP文件传输进度实现资源,基于Renci.SshNet库完成上传与下载操作,重点解决传输过程中缺少进度反馈的问题。资源附带了完整的Visual Studio工程示例,涵盖普通上传与带回调(Action<ulong,ulong>)的进度版上传/下载方法,开发者可直接运行查看控制台输出或改用WinForms/WPF进度条控件。压缩包为RAR格式,共26个文件,文件类型以C#源码(.cs)、可执行程序(.exe)、Renci.SshNet动态库(.dll)和资源文件(.resx)为主,另含解决方案(.sln)等工程文件,整体体积仅533KB,非常适合快速参考和二次开发。目前已有1680人学习,资源内附可编译的SFTPtest工程,能够帮助理解SFTP连接、文件流读写、进度百分比计算等关键细节,减少自行排查Renci.SshNet调用方式的时间成本。

1. 用 C# 给 SFTP 加上进度条:先搞清楚它解决的是哪类问题

做上位机或者桌面工具时,跟 Linux 服务器交换文件是躲不掉的活。很多人的第一反应是用 FTP,但 FTP 在公网和半信任网络里裸奔,账号密码和文件内容全是明文;另一个常见选择是网络共享,可跨网段、跨系统时权限配置能把人折腾到怀疑人生。SFTP(SSH File Transfer Protocol)走的是 SSH 通道,加密、标准、跨平台,C# 工程里接入并不复杂,麻烦的倒是那个「有进度条」——很多人做完上传下载才发现,文件是传完了,界面上的进度条却不会动。

这个标题背后的真实诉求,其实是三类:一是要一个能在 WinForm / WPF 里跑起来的 SFTP 上传下载封装;二是要能实时看到进度,而不是让用户对着假死的界面干等;三是要处理大文件、断线、权限这些边角问题。本文就把这套东西拆开,从选库到封装,从参数到避坑,按能直接抄作业的方式写一遍。

2. SSH.NET 是首选:为什么它比命令行和 FTP 方案更值得投入

2.1 三个可选方案,我为什么只留 SSH.NET

C# 里做 SFTP,方案大概有三条路:进程外调命令行、用 WinSCP .NET 库、用 SSH.NET。命令行方案最糙,拿 Process 去调 sftp.exe,输出解析靠猜,进度条只能读标准输出流,一旦服务器返回的提示语本地化,脚本当场翻车。WinSCP 库功能全,但它依赖 WinSCP.exe 这个外部程序,部署时要多带一个 exe,版本升级还得跟着它走。

SSH.NET 是纯托管代码,NuGet 上直接搜“SSH.NET”就能装,命名空间是 Renci.SshNet,完全不需要外部进程。它的 SftpClient 天然支持上传下载的 progress 回调,加密、压缩、断线重连这些底层细节都封装好了。对于「C# 实现 SFTP 文件上传和下载,有进度条」这个需求,它就是最直接的答案——一个包解决通道和进度,不用拼凑。

2.2 先把连接这层皮扒了:ConnectionInfo 与 HostKey

在用 SftpClient 之前,得先理解它依赖的 ConnectionInfo。这个类负责把主机、端口、用户名、认证方式和指纹校验打包在一起。常见的做法是用户名加密码,但生产环境更推荐用私钥文件,避免把密码硬编码进配置文件。

// 组装连接信息 var connectionInfo = new ConnectionInfo( host: "192.168.1.100", // 服务器地址 port: 22, // SFTP 默认端口,不要拿它跟 FTP 的 21 搞混 username: "deploy", // 登录账号 new PasswordAuthenticationMethod("deploy", "your_password") // 认证方式 // new PrivateKeyAuthenticationMethod("deploy", new PrivateKeyFile(@"C:\keys\id_rsa")) // 推荐改用私钥 ); using var client = new SftpClient(connectionInfo); client.Connect();

注意PasswordAuthenticationMethod和PrivateKeyAuthenticationMethod是互斥的,二选一。如果公司安全策略不允许密码登录,就注释掉密码行,换成私钥那行。另一个隐藏点是 HostKey 校验——SSH.NET 默认会接受服务器指纹,但严谨的做法是校验指纹以防中间人攻击。这一步很多人忽略,等哪天被人用假服务器套了密码才后悔。

3. 上传带进度条:从裸传到界面能看的最小代码

3.1 不用 BackgroundWorker,用 Progress 回调

很多教程还在用 BackgroundWorker 更新进度条,但 .NET 4.5 之后更干净的做法是配合Progress<T>或者直接用 SftpClient 的UploadFile重载。UploadFile的重载里有一个Action<ulong>类型的 progress 回调,这个回调在独立线程上跑,所以不能直接在回调里改 UI 控件,要借助Progress<T>封一层。

// 上传本地文件到服务器指定目录 private void UploadWithProgress() { var progress = new Progress<long>(value => { progressBar1.Value = (int)value; // 安全地更新 UI,Progress<T> 捕获了当前同步上下文 }); using var client = new SftpClient(connectionInfo); client.Connect(); var fileSize = new FileInfo(localFilePath).Length; // 提前拿总字节数,用于算百分比 long uploadedBytes = 0; using var fileStream = File.OpenRead(localFilePath); client.UploadFile(fileStream, remoteFilePath, uploaded => { uploadedBytes = uploaded; // 这是已上传的累计字节数 var percent = (int)((uploadedBytes * 100) / fileSize); ((IProgress<long>)progress).Report(percent); // 把百分比报告给 UI 线程 }); Console.WriteLine($"上传完成,共 {uploadedBytes} 字节"); }

逻辑说明:UploadFile的第三个参数是回调委托,每传一块数据就调用一次,参数uploaded是累计上传的字节数。用FileInfo.Length拿总大小,算百分比后通过Progress<T>.Report发到 UI 线程。这里有个性能细节:回调触发的频率很高,如果直接在回调里 Report,UI 线程可能被频繁刷新拖垮,所以一般会对百分比做一个节流判断,比如只在变化量超过 1% 时才 Report。

参数说明:UploadFile的第一个参数是本地文件流,第二个是远程路径,第三个是进度回调。远程路径要写绝对路径或相对用户主目录的路径,比如/home/deploy/uploads/file.zip。如果服务器上的目录不存在,上传会抛异常,后面避坑章会专门说。

3.2 上传大文件时的缓冲区和超时设置

大文件上传,缓冲区大小直接决定速度。SSH.NET 的BufferSize属性默认是 4KB,这在高速局域网里是明显瓶颈。常见做法是按网络环境调整,我一般会把它设成 64KB 或 256KB。

client.BufferSize = 256 * 1024; // 256KB 缓冲区,适合百兆以上局域网 client.OperationTimeout = TimeSpan.FromSeconds(30); // 单个操作超时,太短会误杀慢速连接 client.ConnectionTimeout = TimeSpan.FromSeconds(15); // 建立连接的超时,按实际网络延迟调

注意BufferSize不是越大越好。设成 1MB 以上时,SSH 通道的滑动窗口可能跟不上,反而触发服务端窗口调整,速度掉一半。256KB 是一个经过较多生产验证的平衡值。OperationTimeout如果设得太短,比如 10 秒,在线路抖动时会看到上传中断;设太长,又会让界面卡很久才报错。30 秒是个起点,按你实际最慢的服务器来定。

4. 下载带进度条:同样的回调,不同的边界

4.1 DownloadFile 的进度回调与断点续传的取舍

下载的逻辑和上传对称,但有两个边界要注意:一是下载到本地时目标目录不存在会抛异常,二是断点续传没有内置支持。SSH.NET 的DownloadFile没有像 HTTP 那样的 Range 断点续传,只有全量下载。

private void DownloadWithProgress(string remotePath, string localPath) { using var client = new SftpClient(connectionInfo); client.Connect(); var remoteSize = client.GetAttributes(remotePath).Size; // 远程文件大小,用于算进度 using var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write); client.DownloadFile(remotePath, fileStream, downloaded => { // 这里拿到的 downloaded 是已经写入的字节数 var percent = (int)((downloaded * 100) / remoteSize); // 节流到 UI 进度条 }); }

注意GetAttributes(remotePath).Size返回的是ulong,转换成 long 做除法时小心别溢出。另一个细节是FileMode.Create会覆盖本地已有文件,如果需要断点续传,得自己判断本地文件大小并重写下载逻辑,SSH.NET 不直接支持。若你的场景真有续传需求,常见做法是先下载到临时文件,完成后再改名覆盖,避免下载一半把旧文件毁了。

4.2 用 SftpFileStream 处理服务器端文件读取

如果你要下载服务器上的文件到内存,或者边读边处理,而不是直接落盘,SSH.NET 提供了SftpFileStream。它能像本地 FileStream 一样操作远程文件,适合处理日志这类需要按行读的场景。

using (var remoteStream = client.OpenRead(remoteFilePath)) using (var reader = new StreamReader(remoteStream)) { string line; while ((line = reader.ReadLine()) != null) { // 逐行处理,适合看日志、统计行数 processLine(line); } }

这个方案的好处是内存可控,不会一次性把大文件全部拉下来。但SftpFileStream没有进度事件,你只能自己数读了多少字节,如果想在界面显示进度,就要在while循环里手动 Report。它更适合后台静默处理,而不是给用户看进度条的场景。

5. 避坑清单:5 条让 SFTP 进度条翻车的真实原因

5.1 进度条卡死:回调线程和 UI 线程的同步没做对

现象:文件在服务器上确实在增长,但进度条一动不动,界面还能拖得动。

原因:UploadFile的回调跑在线程池线程上,你直接在回调里写了progressBar1.Value = x,而 WinForm 控件的属性只能在创建它的 UI 线程更新。跨线程赋值要么抛异常,要么静默失效。

解决:用Progress<T>或Control.BeginInvoke把更新调度到 UI 线程。Progress<T>在创建时会捕获当前 SynchronizationContext,所以务必在 UI 线程上new Progress<long>。用 BeginInvoke 的话注意别被高频回调淹没,做个百分比的去重判断。

5.2 上传成功后进度条停在 99%

现象:进度条走到 99%,停了几百毫秒才跳到 100%,用户以为卡了。

原因:UploadFile回调里统计的是已通过 SSH 通道发送的字节数,而服务器最终 fsync 落盘还要一点时间。回调先结束,UI 的百分比已经反映 100%,但函数还没返回,界面被阻塞,进度条自然停住。

解决:不要在 UploadFile 调用返回后才把进度条设为 100%,而是在回调里检测到uploaded == fileSize时就手动把进度条打满,然后再做后续的收尾。或者用异步模式,把 UI 更新和文件传输解耦。

5.3 远程路径的目录不存在,直接抛 SftpPathNotFoundException

现象:上传或下载时报「SftpPathNotFoundException: No such file」,路径明明看着没问题。

原因:SFTP 没有自动创建目录的能力,UploadFile不会帮你 mkdir。常见的坑是远程路径写成/home/deploy/2025/log.zip,但2025这个目录不存在。

解决:上传前先确认目录存在,不存在就client.CreateDirectory。注意 CreateDirectory 只能创建一级目录,多级目录要递归创建。

private void EnsureRemoteDirectory(SftpClient client, string remoteFilePath) { var dir = Path.GetDirectoryName(remoteFilePath).Replace("\\", "/"); if (string.IsNullOrEmpty(dir)) return; if (!client.Exists(dir)) { // 逐级创建,因为 SftpClient.CreateDirectory 不支持一次建多级 var parts = dir.Split('/'); var current = ""; foreach (var part in parts) { if (string.IsNullOrEmpty(part)) continue; current += "/" + part; if (!client.Exists(current)) client.CreateDirectory(current); } } }

5.4 服务器主动断开:OperationTimeout 和 “SFTP error 103”

现象:传大文件传了一半,界面报错,日志里出现类似SFTP error 103的提示,重连也连不上。

原因:103 是 SSH_FX_FAILURE 一类的情况,对应到操作上通常是服务器端的会话被强制关闭,比如sshd_config里ClientAliveInterval太短导致空闲连接被杀,或者防火墙对长连接的超时做了限制。SFTP 不是无状态协议,连接一断,之前的进度全部白干。

解决:在代码里给KeepAliveInterval设一个值,比如 30 秒,让连接保持活跃。此外要把上传下载逻辑封装进重试机制——捕获SshConnectionException后,重新 Connect 并接着上次的百分比继续。但要注意,SSH.NET 没有服务端的断点续传接口,重试后要从头传。

5.5 文件名编码导致服务器上显示乱码

现象:用 WinForm 上传一个测试报告.zip,服务器上 ls 看到的是乱码,下载回来文件名也变了。

原因:SFTP 默认用 UTF-8 传输文件名,但服务器端 sshd 可能没有设置 UTF-8 locale,或者你用了 Windows 的 GBK 编码去拼路径。

解决:把ConnectionInfo的编码属性设为 UTF-8。具体是在创建ConnectionInfo时传入Encoding.UTF8作为字符编码参数。如果没有显式指定,SSH.NET 默认 UTF-8,但服务器端若配置不对,就得两端一起核对。最省心的策略是:项目的文件命名统一用 ASCII,避免中文名走 SFTP,可以省掉一整类玄学问题。

6. 把上传下载封装成带校验和重试的服务类:最后一步落地产物

到这里,单独的上传和下载你已经能跑通了。接下来建议把它们收进一个SftpTransferService类,把连接管理、进度报告、重试、校验放在一起,这样不管你是做 WinForm 工具还是 WPF 上位机,UI 层只需要调用一个方法。

public class SftpTransferService { private readonly ConnectionInfo _connectionInfo; private readonly int _maxRetries = 3; public SftpTransferService(ConnectionInfo connectionInfo) => _connectionInfo = connectionInfo; public async Task UploadAsync(string localPath, string remotePath, IProgress<int> progress, CancellationToken ct) { for (int attempt = 1; attempt <= _maxRetries; attempt++) { try { using var client = new SftpClient(_connectionInfo); client.Connect(); await Task.Run(() => { // 在这里执行 UploadFile,并把进度转发给 IProgress<int> }, ct); return; } catch (SshConnectionException) when (attempt < _maxRetries) { await Task.Delay(TimeSpan.FromSeconds(2 * attempt), ct); // 指数退避,2秒、4秒 } } } }

重试要小心:SftpClient一旦连接断开,同一个实例不能直接重连继续用,得重新 new 一个。这里用using var保证每次尝试都是全新实例。进度报告用IProgress<int>而不是Progress<T>,这样调用方可以自己决定在哪个线程更新 UI,更灵活。

最后说一个我自己的习惯:无论是上传还是下载,完成后都要做一次大小核对——拿GetAttributes(remotePath).Size和本地文件大小比一下,不一致就报警。因为 SSH 通道传输过程中如果被网关静默截断,文件大小能对上才会有鬼。这个小习惯帮我挡掉过至少两次凌晨两点的紧急电话。整个方案做下来,你会发现「C# 实现 SFTP 文件上传和下载,有进度条」的本质不是那两个 API,而是线程模型的切换、超时的取舍、目录和重试的边界。把这些想清楚,换任何库都能写,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询