Renci SSH.NET在Linux和.Net Core中如何正确使用RemoveForwardedPort()
问题根因
Renci SSH.NET 公开的 SshClient.RemoveForwardedPort() 方法内部默认会先校验SSH连接状态,同时会尝试向服务端发送端口解绑请求,连接断开状态下自然会抛出Client not connected异常;哪怕你后续重连成功,旧的转发端口实例绑定的是之前旧连接的会话上下文,和新连接不匹配,调用公开方法依然会触发校验失败。
可行解决方案
方案1:保留旧SshClient实例,手动清理残留端口
不要在ErrorOccurred或端口异常的回调里直接执行移除操作,异常触发时SshClient可能处于中间状态,容易出现线程安全问题,按以下步骤操作:
- 先把所有需要清理的旧端口实例存入一个线程安全的队列(比如
ConcurrentQueue<ForwardedPortRemote>),单独开清理线程异步处理 - 先调用端口实例自身的
Stop()方法,该方法不需要和服务端交互,连接断开时也可正常执行,会释放本地绑定的端口资源 - 通过反射绕过公开方法的连接校验,直接移除SshClient内部维护的端口列表里的旧实例,参考代码:
using System.Reflection; using Renci.SshNet; using Renci.SshNet.Forwarded; // 执行移除逻辑 portRemote.Stop(); var forwardedPortsField = typeof(SshClient).GetField("_forwardedPorts", BindingFlags.Instance | BindingFlags.NonPublic); if (forwardedPortsField != null) { var portList = (IList<ForwardedPort>)forwardedPortsField.GetValue(sshClient); portList.Remove(portRemote); } // 端口资源会自动被GC回收,不需要额外Dispose
方案2:重连时直接重建SshClient实例(更推荐,稳定性更高)
不需要清理旧实例的残留端口,直接销毁旧实例重建即可,完全避开校验问题:
- 连接断开触发重连逻辑时,先对旧的SshClient执行
Dispose(),释放所有关联的端口、连接资源 - 重新实例化一个新的SshClient对象,配置好连接参数
- 把所有需要的转发端口重新注册到新的SshClient实例,再启动连接和端口转发即可
- 旧端口实例如果要复用,只需要先执行
Stop(),再重新绑定到新的SshClient上启动即可,不需要重新创建端口实例
注意事项
- 所有对SshClient、转发端口的操作都要加互斥锁,避免异常回调、重连逻辑、业务逻辑多线程并发操作导致状态错乱
- 重连前要确认旧的端口资源已经完全释放,避免出现本地端口被占用的报错
内容的提问来源于stack exchange,提问作者Kris
相关产品推荐
相关产品推荐

