新闻详情

新闻详情

首页 / 资讯中心 / 详情

C# SSH连接与SshNet库封装实战:上位机远程命令执行与SFTP文件传输

发布时间:2026/9/29 4:24:27来源:尧图网络
C# SSH连接与SshNet库封装实战:上位机远程命令执行与SFTP文件传输
简介一个基于C#实现的SSH连接半成品工程原为某主项目的子功能模块适合需要二次开发或学习SSH协议与WinForms交互的.NET开发者。从包体看共70个文件涵盖C#源码6个、动态链接库12个、可执行程序6个以及配置、解决方案与打包工程等压缩包约7.39MB目录结构规整便于快速定位项目入口、依赖项与打包配置。资源作者经验丰富上传前已严格测试保证可直接运行目前已有48人学习/浏览。拿到后不仅可获得完整可复现的源码工程还能参考其界面交互、NuGet依赖组织、Setup安装工程等细节适合用于课程设计、毕业设计或SSH工具类项目起步也能在此基础上扩展认证、命令交互等功能。1. C# 做 SSH 连接这个半成品能直接抄进上位机项目吗我拆过不少 C# 上位机项目发现一个高频需求WinForm 或 WPF 程序里要远程登录 Linux 设备、批量执行命令、拉取日志文件。市面上现成的 SSH 工具很多但放到自己的工业软件里做集成还得靠代码。手头这个半成品原本是某个大项目里剥离出来的 SSH 子功能模块核心逻辑是完整的只是没包界面、没做异常兜底属于那种“懂的人拿去十分钟能跑通不懂的人看着一堆类不知道从哪下手”的状态。它对谁最有用一是做上位机、设备管理软件需要跟远程 Linux 终端或嵌入式设备通信的 C# 开发者二是想了解 SshNet 库怎么组织代码、怎么把 SSH 会话封装成公共类的学习者。这包代码解决的核心问题只有一个用最小成本在 .NET 环境里建立 SSH 连接、执行远程命令、拿回输出结果。本文我会把它拆开告诉你哪些代码直接能用、哪些需要补、以及我实际踩过的坑最后给出一套可以复用的封装模板让你拿到手就能改造进自己的项目。2. 先搞清楚技术选型为什么 C# 做 SSH 几乎绕不开 SshNet2.1 SSH 协议与 C# 原生支持的边界SSH 协议本身是建立在 TCP 之上的安全传输层协议涉及密钥交换、加密算法协商、用户认证等多个环节。.NET 框架自带的System.Net.Sockets只提供 TCP 连接能力不包含 SSH 协议的实现所以你不能像用HttpClient那样直接用原生库连 SSH 服务器。这是很多新手第一次翻车的地方——拿 TcpClient 去连 22 端口收到的全是乱码因为根本没有做协议握手。实际开发中C# 生态里能选的开源 SSH 库主要有两个Renci.SshNet 和 SharpSSH。SharpSSH 是老牌库但维护停滞多年API 设计还停留在 .NET Framework 2.0 时代泛型和异步支持都不好Renci.SshNet 是目前使用最广泛的支持 .NET Framework 4.0 到 .NET 6/8API 清晰文档和示例也容易搜到。我经手的项目里九成以上用的都是它这个半成品也不例外。2.2 半成品里的关键类SshClient 与 RunCommand 是核心打开这个半成品的源码你会发现核心代码量其实不大。最关键的类就两个SshClient负责建立连接SshCommand负责执行命令并返回结果。典型的调用流程可以简化成下面这段using Renci.SshNet; // 建立连接配置 var connectionInfo new ConnectionInfo( host: 192.168.1.100, port: 22, username: root, authenticationMethod: new PasswordAuthenticationMethod(root, your_password) ); // 创建客户端并连接 using (var client new SshClient(connectionInfo)) { client.Connect(); // 执行远程命令 var command client.RunCommand(uname -a); Console.WriteLine(command.Result); client.Disconnect(); }这段代码里有几个值得注意的参数ConnectionInfo的构造函数支持传入认证方法数组这意味着你可以同时配置多个认证方式SshNet 会按顺序尝试。PasswordAuthenticationMethod是用户密码认证如果服务器配置了密钥登录就要换成PrivateKeyAuthenticationMethod需要传入私钥文件路径和密码短语如果有的话。RunCommand是同步方法适合执行短命令如果要跑持续输出的命令后面我会讲流式读法。这里我补充一个容易忽略的细节SshClient实现了IDisposable务必用using包裹。如果连接中途异常退出连接对象没有释放会造成 TCP 连接泄漏设备侧会积累大量 TIME_WAIT 状态的连接最终导致连不上。半成品代码里可能没有处理这些你自己封装时一定要补上。2.3 不完整的部分缺什么、有什么、要补什么既然是半成品就得盘点清楚。我打开源码包看了看里面包含一个SshHelper类提供了基础的连接和命令执行封装一个配置文件示例写的是连接参数还有一个 WinForm 测试窗体能输入 IP、用户名、密码点按钮执行命令看输出。这些对于快速验证 SSH 通不通足够了。但缺的东西也很明显。第一没有异步版本UI 线程上直接调RunCommand会卡界面连一个不通的 IP 能卡十几秒体验很差第二异常处理基本空白密码错误、网络不通、密钥不匹配这几种异常都直接抛出来没有分类提示第三没有处理命令输出编码连中文系统或者输出含中文时大概率乱码第四没有超时控制默认连接超时是按 TCP 层默认走的实际场景下很可能不够用。所以我的建议是连接和基础命令直接复用其余部分按我后面给的封装模板补齐。3. 把半成品改造成可复用模块从裸代码到公共类的完整步骤3.1 第一步抽离配置项别把连接参数写死在代码里拿到半成品代码第一件事不是改功能而是先把连接参数抽出来。我看到不少项目直接把 IP、用户名、密码硬编码在页面按钮的 Click 事件里换个设备就得改代码重新编译这在调试阶段还能忍一旦进入交付阶段就会非常痛苦。常见的做法是写一个SshConfig类来承载连接参数并支持从配置文件读取。我一般这样写public class SshConfig { public string Host { get; set; } 192.168.1.100; public int Port { get; set; } 22; public string Username { get; set; } root; public string Password { get; set; } ; public string PrivateKeyPath { get; set; } ; public string PrivateKeyPassphrase { get; set; } ; public int ConnectTimeoutMs { get; set; } 10000; public int CommandTimeoutMs { get; set; } 30000; }然后把读取配置的逻辑放在SshConfig.LoadFromAppSettings()静态方法里用ConfigurationManager.AppSettings读取键值。这样换设备只需要改配置文件不需要重新编译。注意密码写在配置里有安全隐患正式项目建议用加密配置或者改用密钥认证我这里给的是开发阶段的折中方案。3.2 第二步封装连接管理支持超时、异常分类、重连机制这是整个改造里最重要的一步。SshNet 的SshClient.Connect()方法在某些网络异常情况下会抛SocketException或SshConnectionException但不会主动告诉你“这是登录失败”还是“这是网络不通”需要自己判断。我封装了一个连接方法核心逻辑如下public bool Connect(SshConfig config, out string errorMessage) { errorMessage ; try { var authMethods new ListAuthenticationMethod(); if (!string.IsNullOrEmpty(config.PrivateKeyPath)) { var keyFile new PrivateKeyFile(config.PrivateKeyPath, config.PrivateKeyPassphrase); authMethods.Add(new PrivateKeyAuthenticationMethod(config.Username, keyFile)); } else { authMethods.Add(new PasswordAuthenticationMethod(config.Username, config.Password)); } var connInfo new ConnectionInfo( config.Host, config.Port, config.Username, authMethods.ToArray() ); _client new SshClient(connInfo); _client.Connect(); return true; } catch (SshAuthenticationException ex) { errorMessage $认证失败请检查用户名或密码{ex.Message}; return false; } catch (SocketException ex) { errorMessage $网络连接失败{ex.Message}; return false; } catch (Exception ex) { errorMessage $未知异常{ex.Message}; return false; } }参数层面有几个值得解释的点。ConnectionInfo的构造支持传AuthenticationMethod[]数组我故意用了ListAuthenticationMethod再转数组方便以后扩展成“优先密钥、失败再密码”的多认证模式。SshAuthenticationException是登录凭据错误时抛出的异常类型SocketException是网络层错误这两类必须区分开因为给用户的提示文案完全不同。_client是类字段保存连接实例供后续命令方法使用。注意Connect()里我没处理超时原因是 SshNet 的连接超时不是靠ConnectTimeout属性设置的而是要在ConnectionInfo构造后设置Timeout属性。这个坑我后面详细讲。3.3 第三步封装命令执行区分普通命令与有超时要求的命令连接好了之后执行命令是最常用的操作。半成品里的RunCommand是同步的我封装时加了超时控制因为远程命令可能因为设备负载高、网络抖动等原因长时间不返回不能无限等下去。public string ExecuteCommand(string commandText, int timeoutSeconds 30) { if (_client null || !_client.IsConnected) { throw new InvalidOperationException(SSH 连接未建立请先调用 Connect()); } using (var cmd _client.CreateCommand(commandText)) { cmd.CommandTimeout TimeSpan.FromSeconds(timeoutSeconds); var result cmd.Execute(); return result; } }这里用CreateCommand替代RunCommand两者差异在于CreateCommand返回SshCommand对象让你能设置CommandTimeout。RunCommand内部虽然也创建命令但超时用的是默认值实际使用时经常不够。cmd.Execute()返回的是远程命令的标准输出字符串如果命令写到了 stderr需要靠检查cmd.Error和cmd.ExitStatus来判断执行是否成功。我见过不少人在这一步翻车命令执行了但报错了result里却是空字符串其实是输出在 stderr 里没看Error属性。3.4 第四步补上 SFTP 文件传输这是实际项目里最常用的附属功能SSH 连接的价值不只是执行命令还有文件传输。很多场景是远程设备生成日志文件上位机需要定时拉取。SshNet 对 SFTP 支持得相当完整封装的复杂度主要在于目录检查、文件覆盖策略和流关闭。下面是我整理好的 SFTP 上传和下载方法using Renci.SshNet.Sftp; public bool UploadFile(string localPath, string remotePath) { if (_client null || !_client.IsConnected) return false; try { using (var sftp new SftpClient(_client.ConnectionInfo)) { sftp.Connect(); // 检查远程目录是否存在不存在则创建 var dir remotePath.Substring(0, remotePath.LastIndexOf(/)); if (!sftp.Exists(dir)) { sftp.CreateDirectory(dir); } using (var fileStream File.OpenRead(localPath)) { sftp.UploadFile(fileStream, remotePath); } sftp.Disconnect(); } return true; } catch (Exception ex) { Console.WriteLine($上传失败: {ex.Message}); return false; } }这里要说清楚一个选择我之所以new SftpClient(_client.ConnectionInfo)而不是从已有连接上直接拿 SFTP是因为 SftpClient 和 SshClient 是并列关系各自持有连接状态。SftpClient 可以复用同一个ConnectionInfo这意味着配置是一次性的但底层 TCP 连接是两个。如果你要在一个已经用 SshClient 连上的会话里同时跑命令和传文件注意它们不是同一个连接传输性能上也没有共享优化的效果。上传前检查远程目录是否存在是个好习惯因为 SFTP 的CreateDirectory在目录已存在时会抛异常你不提前判断就会中断流程。3.5 第五步写一个调试用的命令行测试入口改造完成后不要直接丢进正式项目里先做一个控制台或 WinForm 测试入口验证三步连接、执行命令、SFTP 上下传。我一般用控制台项目来做因为启动快、日志好收集。测试主逻辑长这样class Program { static void Main(string[] args) { var config SshConfig.LoadFromAppSettings(); var helper new SshHelper(); if (!helper.Connect(config, out string err)) { Console.WriteLine($连接失败: {err}); return; } Console.WriteLine(连接成功); string output helper.ExecuteCommand(free -m, 15); Console.WriteLine(output); helper.Disconnect(); } }这一步作为验收手段能帮你快速判断改完的封装类是否可用、参数是否配对了。全部通过后再把这个封装类文件拷进正式项目删除或保留调试代码取决于你的习惯我倾向保留一个单独调试工程方便现场排查设备通信问题。4. SshNet 实战避坑五个高频问题的现象、原因与解法4.1 中文输出全是乱码问题出在编码参数现象执行cat /etc/os-release或查看中文日志文件返回结果是一类的乱码。原因SSH 通道传输的是字节流服务器输出的是 UTF-8 编码但 SshNet 默认按 ISO-8859-1 解码两种编码不匹配。解决方式是在ConnectionInfo或命令执行前指定编码。需要说明的是SshNet 的SshCommand.Result属性没有直接暴露编码参数但可以通过向服务器发送export LANGen_US.UTF-8这类命令来影响服务端输出编码更彻底的做法是使用SshClient.CreateShellStream并指定编码。我在实际项目中验证过最可靠的方式是在建立连接前用命令前置的方式固定服务端语言环境export LANGen_US.UTF-8; export LC_ALLen_US.UTF-8; 实际命令这样能解决绝大多数字符集问题同时保持服务端与客户端的编码一致。4.2 连接超时设置无效Timeout 属性必须写在 Connect 之前现象设置了client.ConnectionInfo.Timeout 5000但连接一个不可达 IP 时还是要等 20 秒才报错。原因SshNet 的ConnectionInfo.Timeout属性控制的是连接建立的等待时间但它必须在Connect()调用前赋值才生效。很多人是在Connect()之后才设置自然没用。正确做法var connInfo new ConnectionInfo(host, port, username, authMethod); connInfo.Timeout TimeSpan.FromSeconds(5); _client new SshClient(connInfo); _client.Connect();注意这里Timeout单位是TimeSpan不是毫秒整数。如果你传的是5000编译器会报类型不匹配但如果你传TimeSpan.FromMilliseconds(5000)效果等价于 5 秒。现场调试时我习惯把超时调到 3 秒快速试探网络通不通通了再调回正常值。4.3 同一 IP 频繁连接导致连接被拒TCP 端口耗尽在作祟现象程序在循环里反复 Connect-Disconnect跑到几十次后报 “SocketException: 由于目标计算机积极拒绝无法连接”。原因每次Disconnect()只是关闭了 SSH 会话底层 TCP 连接可能没有彻底释放客户端处于 TIME_WAIT 状态大量堆积后超出了系统默认的端口范围或连接数限制。解决方法是确保SshClient被using包裹或调用Dispose()同时把ConnectionInfo.Timeout调低让失效连接更快回收。更稳的做法是一个会话持续复用不要频繁建立销毁把SshHelper设计成单例来用。4.4 执行 sudo 命令卡住SSH 通道里没有交互式密码输入位置现象执行sudo reboot或sudo rm -rf /tmp/test命令返回空或者直接抛SshException: Socket error。原因sudo默认需要从终端读取密码而RunCommand是非交互式的它不关联伪终端PTYsudo 等不到密码输入就超时挂了。解决方式分两种一种是临时给用户配置免密 sudo另一种是用CreateShellStream模拟终端交互。开发测试阶段我常用第一种生产环境务必走权限管理流程。示范用的免密配置是在远程的/etc/sudoers文件里加一行username ALL(ALL) NOPASSWD: ALL但这行仅限测试用途真实生产环境请遵循最小权限原则。4.5 私钥认证报错 “Private key file is incorrect”现象用私钥文件连接时报PrivateKeyException: Invalid private key file。原因不是文件损坏多半是格式不兼容。OpenSSH 7.8 以后默认生成的是新格式私钥开头是-----BEGIN OPENSSH PRIVATE KEY-----Renci.SshNet 在旧版本上只认 RSA 格式开头是-----BEGIN RSA PRIVATE KEY-----。解决方法是把私钥转换成 PEM 格式ssh-keygen -p -m PEM -f ~/.ssh/id_rsa参数说明-p表示修改私钥口令可以留空表示保持原样-m PEM指定输出格式为 PEM-f指定私钥文件路径。转换完成后重新加载文件SshNet 就能识别了。这个坑在 CentOS 7 和 Ubuntu 18.04 以上版本特别常见。5. 把 SSH 子功能完整跑起来从下载到复现的验证链路5.1 环境准备Windows 开发机连接 Linux 目标机这个半成品最典型的复现场景是Windows 上位机通过 SSH 连接局域网里的 Linux 工控机或服务器。开发环境建议用 Visual Studio 2019 及以上目标框架选 .NET Framework 4.7.2 或 .NET 6分别对应 WinForm 和现代应用。调试之前先把 NuGet 包SSH.NET装上版本建议不低于 2020.0.0我实测过 2016 年以前的版本对新版 OpenSSH 兼容性很差。目标机也就是被连接的一端需要确保 sshd 服务在运行systemctl status sshd如果没启动用systemctl start sshd启动并确认 22 端口在监听netstat -tlnp | grep :22。Windows 开发机这边先在命令行验证能连通ssh root192.168.1.100命令行通了再跑程序减少变量干扰。5.2 验证链路连接、执行、断连三步实测拿到半成品后我建议你先按原样跑通再动手改。验证链路分三段连接阶段看是否能握手成功执行阶段看命令输出是否完整、编码是否正确断连阶段看资源是否正常释放。// 验证链路完整示例 static void TestSshConnection() { try { using (var client new SshClient(192.168.1.100, root, 123456)) { client.Connect(); Console.WriteLine(阶段1-连接成功); var cmd client.CreateCommand(uptime df -h | head -5); cmd.CommandTimeout TimeSpan.FromSeconds(10); string output cmd.Execute(); Console.WriteLine(阶段2-命令输出); Console.WriteLine(output); } Console.WriteLine(阶段3-连接已释放); } catch (Exception ex) { Console.WriteLine($验证失败: {ex.Message}); } }using块结束时SshClient的Dispose()会自动断开连接并释放底层 Socket这比手动Disconnect()更可靠。执行uptime df -h是为了同时验证普通命令与管道命令的支持情况。如果输出里有 IP 地址、用户、时间、磁盘使用率这几项说明基本的执行链路没问题。5.3 引入到正式项目时的三个落地检查半成品改造完成后正式集成前要过三个检查。第一个是线程模型SshNet 不是线程安全的多个线程共享同一个SshClient实例执行命令会互相干扰。我用过一个并发拉取多台设备状态的场景每个设备建一个独立的SshClient线程池里各用各的不要共享。第二个是配置文件敏感信息密码不要明文写在App.config里至少要做简单混淆或者使用 Windows 凭据管理器存储。第三个是日志记录现场出问题时第一件事就是要看 SSH 层日志我习惯在封装类里加一个Log事件把每次连接耗时、命令执行耗时、异常堆栈都打出来方便远程排查。这三个检查都过了这个半成品才算真正从“能跑”变成“能交付”。6. 进阶ShellStream 流式交互与多命令会话的实战技巧半成品里的RunCommand是一次性执行每条命令独立建立会话。但真实项目里常常要连续执行多条命令且命令之间有状态依赖比如先cd /var/log再tail -f app.log或者需要运行进入交互式程序比如top、python、数据库客户端。这种场景ExecuteCommand就力不从心了因为每次调用都是一次全新的命令会话上一个命令的工作目录、环境变量都不会保留。解决方式是使用CreateShellStream它在 SSH 会话里建立一个虚拟终端PTY你往里写命令它返回输出流命令之间共享同一个 Shell 上下文。核心用法如下using (var shell _client.CreateShellStream(xterm, 80, 24, 800, 600, 1024)) { // 向虚拟终端发送命令 shell.WriteLine(cd /var/log); Thread.Sleep(500); shell.WriteLine(ls -l *.log); Thread.Sleep(500); // 读取终端输出 string output shell.Read(); Console.WriteLine(output); }CreateShellStream的参数有六个依次是终端类型通常填xterm、列数、行数、屏幕缓冲区宽度、屏幕缓冲区高度、缓冲区大小。这些参数本质上是 PTY 的几何信息很多服务器端程序会根据终端类型和尺寸决定输出格式比如top在窄终端下会省略部分列。Read()方法读取的是缓冲区里当前累积的所有输出返回值是 string所以你要自己控制读取节奏否则可能读到半行命令输出。我一般用Thread.Sleep等待命令执行完成再读也可以读shell.DataAvailable事件做真正的异步流式读取。这种流式交互的核心价值在于命令状态是连续的能跑cd之后再跑相对路径命令能进入交互式程序并模拟输入。我做过一个批量巡检工具就是通过 ShellStream 登录设备后依次执行uname -a、free -m、df -h、tail -n 50 syslog全程共用同一个会话速度比逐条RunCommand快了三倍以上。另外一个值得记忆的技巧是ShellStream 模式下注意清理回显。因为虚拟终端会回显你发送的命令Read()拿到的输出里往往包含你刚才输入的cd /var/log这一行所以解析结果前要过滤掉命令回显。简单做可以按行拆分跳过与输入命令完全一致的行再去匹配目标内容。从那次以后我接到任何 SSH 相关的需求都会问一句“是一次性命令还是连续会话”如果是连续会话我直接走 ShellStream不做多余的RunCommand封装。这个判断能避开大量返工。希望这个拆解和封装模板能帮你在自己的项目里少踩几个坑把半成品变成真正顺手的工具。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Linux系统运行时间详解:uptime、/proc/uptime与重启排查 2026/9/29 5:08:34

Linux系统运行时间详解:uptime、/proc/uptime与重启排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
加速度计测的是力而非加速度:MEMS原理与校准实践 2026/9/29 5:08:28

加速度计测的是力而非加速度:MEMS原理与校准实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Strands Agents Harness SDK 实战:从手写循环到工程化 Agent 运行时 2026/9/29 5:08:27

Strands Agents Harness SDK 实战:从手写循环到工程化 Agent 运行时

1. 为什么“手写 Agent 循环”正在变成一种负债如果你最近半年在折腾 AI Agent,大概率经历过这个阶段:一开始觉得 Agent 不就是“LLM 工具调用 循环”嘛,自己撸一个 while 循环,几十行代码就能跑起来,还挺有成就感。…

阅读更多 →
VS Code+EIDE开发51单片机:SDCC编译链实战指南 2026/9/29 5:08:27

VS Code+EIDE开发51单片机:SDCC编译链实战指南

1. 为什么放弃Keil改用VS Code EIDE?一个老手的真实动因我带过三届单片机实训课,前两届学生清一色用Keil uVision——界面熟悉、资料多、教程满天飞。但去年带第三届时,我主动把整套开发环境换成了VS Code EIDE插件,连配套讲义都…

阅读更多 →
Django线上教育平台大数据分析:从系统开发到业务洞察的毕设实战指南 2026/9/29 5:08:27

Django线上教育平台大数据分析:从系统开发到业务洞察的毕设实战指南

每年到这个时候,总有一批人被“毕设题目”折磨得寝食难安,尤其是计算机类的同学。你打开导师给的选题列表,一眼扫过去,“基于XX框架的XX管理系统”占了大半,看多了脑子都是木的。但今天想聊的这个题目不太一样——基于…

阅读更多 →
APB总线协议实战:从两拍半时序到Verilog从机实现与验证 2026/9/29 5:08:26

APB总线协议实战:从两拍半时序到Verilog从机实现与验证

做SoC集成的朋友大概都有过这种体验:外设寄存器手册写得明明白白,代码一跑读出来不是 0 就是 0xFFFFFFFF,抓波形看上半天,最后发现是自己没吃透 APB 那两拍半的时序。AMBA 总线协议这个体系里,AXI4 总线协议和 AHB 总线…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉