C# 实现 SFTP 上传下载带进度条:SSH.NET 原理与避坑指南

📅 发布时间:2026/10/8 14:35:22
C# 实现 SFTP 上传下载带进度条:SSH.NET 原理与避坑指南
简介面向C#开发者的SFTP文件传输进度实现资源基于Renci.SshNet库完成上传与下载操作重点解决传输过程中缺少进度反馈的问题。资源附带了完整的Visual Studio工程示例涵盖普通上传与带回调Actionulong,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 在公网和半信任网络里裸奔账号密码和文件内容全是明文另一个常见选择是网络共享可跨网段、跨系统时权限配置能把人折腾到怀疑人生。SFTPSSH File Transfer Protocol走的是 SSH 通道加密、标准、跨平台C# 工程里接入并不复杂麻烦的倒是那个「有进度条」——很多人做完上传下载才发现文件是传完了界面上的进度条却不会动。这个标题背后的真实诉求其实是三类一是要一个能在 WinForm / WPF 里跑起来的 SFTP 上传下载封装二是要能实时看到进度而不是让用户对着假死的界面干等三是要处理大文件、断线、权限这些边角问题。本文就把这套东西拆开从选库到封装从参数到避坑按能直接抄作业的方式写一遍。2. SSH.NET 是首选为什么它比命令行和 FTP 方案更值得投入2.1 三个可选方案我为什么只留 SSH.NETC# 里做 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 之后更干净的做法是配合ProgressT或者直接用 SftpClient 的UploadFile重载。UploadFile的重载里有一个Actionulong类型的 progress 回调这个回调在独立线程上跑所以不能直接在回调里改 UI 控件要借助ProgressT封一层。// 上传本地文件到服务器指定目录 private void UploadWithProgress() { var progress new Progresslong(value { progressBar1.Value (int)value; // 安全地更新 UIProgressT 捕获了当前同步上下文 }); 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); ((IProgresslong)progress).Report(percent); // 把百分比报告给 UI 线程 }); Console.WriteLine($上传完成共 {uploadedBytes} 字节); }逻辑说明UploadFile的第三个参数是回调委托每传一块数据就调用一次参数uploaded是累计上传的字节数。用FileInfo.Length拿总大小算百分比后通过ProgressT.Report发到 UI 线程。这里有个性能细节回调触发的频率很高如果直接在回调里 ReportUI 线程可能被频繁刷新拖垮所以一般会对百分比做一个节流判断比如只在变化量超过 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 线程更新。跨线程赋值要么抛异常要么静默失效。解决用ProgressT或Control.BeginInvoke把更新调度到 UI 线程。ProgressT在创建时会捕获当前 SynchronizationContext所以务必在 UI 线程上new Progresslong。用 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, IProgressint progress, CancellationToken ct) { for (int attempt 1; attempt _maxRetries; attempt) { try { using var client new SftpClient(_connectionInfo); client.Connect(); await Task.Run(() { // 在这里执行 UploadFile并把进度转发给 IProgressint }, ct); return; } catch (SshConnectionException) when (attempt _maxRetries) { await Task.Delay(TimeSpan.FromSeconds(2 * attempt), ct); // 指数退避2秒、4秒 } } } }重试要小心SftpClient一旦连接断开同一个实例不能直接重连继续用得重新 new 一个。这里用using var保证每次尝试都是全新实例。进度报告用IProgressint而不是ProgressT这样调用方可以自己决定在哪个线程更新 UI更灵活。最后说一个我自己的习惯无论是上传还是下载完成后都要做一次大小核对——拿GetAttributes(remotePath).Size和本地文件大小比一下不一致就报警。因为 SSH 通道传输过程中如果被网关静默截断文件大小能对上才会有鬼。这个小习惯帮我挡掉过至少两次凌晨两点的紧急电话。整个方案做下来你会发现「C# 实现 SFTP 文件上传和下载有进度条」的本质不是那两个 API而是线程模型的切换、超时的取舍、目录和重试的边界。把这些想清楚换任何库都能写希望帮到你。本文还有配套的精品资源点击获取