Ruby 的 Net::SSH 库把 SSH 连接中的逻辑通道抽象为 Net::SSH::Connection::Channel 对象。这个对象承载了从打开通道、发送请求到接收数据、关闭通道的完整生命周期,而回调机制是其中最容易被误解的部分。尤其当代码里同时出现 on_failure、exec、request_pty 这些概念时,很多开发者会下意识地把 on_failure 当作所有失败情况的统一处理入口。实际上,on_failure 只负责通道打开阶段的失败回调,而各类请求类型的失败处理走的是完全不同的路径。
要理清这个问题,需要先回到 SSH 协议的消息层。通道打开请求对应的是 CHANNEL_OPEN 消息,如果服务器拒绝,会返回 CHANNEL_OPEN_FAILURE。而通道建立之后发送的 exec、pty-req、env 等请求,如果失败,服务器返回的是 CHANNEL_FAILURE。这两个消息在 Net::SSH 内部映射到了不同的回调机制,不能混为一谈。
一、Channel.on_failure 到底监听什么
在 ssh.open_channel 的块中,通常可以注册多个回调,其中两个直接决定通道是否可用:on_confirm 和 on_failure。前者在服务器接受通道打开请求后触发,后者只在服务器返回 CHANNEL_OPEN_FAILURE 时触发。也就是说,on_failure 是通道打开失败的回调,而不是命令执行失败或请求被拒绝的回调。
on_failure 的回调签名一般为 |channel, reason_code, description|。reason_code 是一个数字,description 是人类可读的失败描述。例如当服务器配置禁止打开 session 类型通道时,可能收到 ADMINISTRATIVELY_PROHIBITED 对应的代码;当请求的通道类型不在服务器支持列表中时,则可能收到 UNKNOWN_CHANNEL_TYPE。通过区分这些原因代码,可以更精确地进行错误提示或降级处理。
下面这段代码演示了如何设置 on_failure。注意它只处理通道打开阶段的失败,后续任何 exec 或 pty-req 请求被拒绝都不会进入这个回调。
require 'net/ssh'
Net::SSH.start('ipipp.com', 'deploy', keys: ['~/.ssh/id_rsa']) do |ssh|
channel = ssh.open_channel do |ch|
ch.on_confirm do |ch|
puts "通道已打开"
# 后续请求在这里发起
end
ch.on_failure do |ch, code, desc|
puts "通道打开失败 #{code}: #{desc}"
end
end
ssh.loop
end
如果 on_confirm 和 on_failure 同时注册,二者互斥触发,服务器不会同时返回成功和失败。因此可以把通道准备工作放在 on_confirm 中,而把连接级降级逻辑放在 on_failure 中。
二、请求类型的发送与响应机制
通道打开之后,客户端可以向服务器发送多种请求类型,常见的包括 pty-req、env、shell、exec、subsystem、window-change 等。Net::SSH 为其中一部分提供了便捷方法,例如 request_pty、exec、env、subsystem,底层统一调用 send_channel_request 来发送 CHANNEL_REQUEST 消息。
send_channel_request 的核心作用是组装请求类型名称和对应数据,并注册一个回调,当服务器返回 CHANNEL_SUCCESS 或 CHANNEL_FAILURE 时执行该回调。回调的参数通常为 |channel, success|,其中 success 为 true 表示服务器接受了请求,为 false 表示请求被拒绝。不同请求类型对数据结构有严格要求,比如 pty-req 需要终端类型、行列数和终端模式字符串,而 env 需要变量名和变量值两个字符串。
以 request_pty 和 exec 为例,它们内部都会把块传递给 send_channel_request,因此请求成功或失败可以在块中通过 success 判断。下面的代码展示了一个常见的嵌套回调:先请求终端,成功后再发送 exec 请求。
channel.on_confirm do |ch|
ch.request_pty do |ch, success|
if success
puts "PTY 请求成功"
ch.exec("uname -a") do |ch, success|
if success
puts "exec 请求已被服务器接受"
else
puts "exec 请求被拒绝"
ch.close
end
end
else
puts "PTY 请求被拒绝"
ch.close
end
end
end
这里容易产生一个误解:exec 的块在命令执行完成后才触发。实际上,该块在服务器返回 exec 请求的响应时就会触发,通常远早于命令结束。命令的输出通过 on_data 和 on_extended_data 回调获取,退出状态则通过 on_request 或后续的 exit-status 请求处理。
对于没有专门便捷方法的请求类型,可以直接调用 send_channel_request 并手动传入数据。例如发送 env 请求可以写成:
channel.send_channel_request("env", "LANG", "en_US.UTF-8") do |ch, success|
puts "env 请求 #{success ? '成功' : '失败'}"
end
这种直接调用方式在需要扩展自定义请求时非常灵活,但要注意第二个参数起的数据结构必须符合协议规范,否则服务器可能直接拒绝请求,块中的 success 会返回 false。
三、请求失败处理与 on_failure 的配合
理解了 on_failure 与请求回调的区别之后,就可以设计更合理的错误处理流程。通道打开失败属于连接级错误,应优先处理;而请求失败属于操作级错误,通常可以通过关闭通道或回退到其他请求来恢复。两者的处理位置不同,但可以在一个完整的通道生命周期中配合使用。
例如,在一个自动化部署脚本中,先通过 on_failure 检测服务器是否允许打开 session 通道。如果允许,再进入 on_confirm 继续请求 PTY 和 exec。如果 PTY 被拒绝,可以尝试无终端模式执行命令;如果 exec 被拒绝,则关闭通道并输出明确的错误信息。这样每一层失败都有对应的兜底逻辑,而不是把所有错误都塞进一个回调中。
下面是一个更完整的示例,展示如何同时处理通道打开失败、PTY 请求失败以及 exec 请求失败,并结合数据输出完成一次远程命令执行。
require 'net/ssh'
Net::SSH.start('ipipp.com', 'deploy', keys: ['~/.ssh/id_rsa']) do |ssh|
channel = ssh.open_channel do |ch|
ch.on_failure do |ch, code, desc|
puts "通道打开失败 #{code}: #{desc}"
ssh.close
end
ch.on_confirm do |ch|
ch.request_pty do |ch, success|
if success
ch.exec("uptime") do |ch, exec_success|
if exec_success
puts "命令已开始执行"
else
puts "exec 请求失败"
ch.close
end
end
else
puts "PTY 请求失败,尝试无终端执行"
ch.exec("uptime") do |ch, exec_success|
unless exec_success
puts "无终端 exec 也失败"
ch.close
end
end
end
end
end
ch.on_data do |ch, data|
puts "输出: #{data}"
end
ch.on_extended_data do |ch, type, data|
puts "错误输出: #{data}"
end
ch.on_close do |ch|
puts "通道已关闭"
end
end
ssh.loop
end
这个例子中,on_failure 只出现在通道打开阶段,而 exec 请求的失败分别在各自块中处理。即使是 PTY 请求失败的回退路径,也保持独立的 success 判断,不依赖全局状态。
四、常见误区与调试建议
第一个常见误区是把 on_failure 当作命令执行失败的回调。出现这种误解的原因通常是方法名带有 failure,且没有仔细阅读文档。实际上,命令执行失败在 SSH 协议中通常表现为退出状态码非零,而不是请求被拒绝。要获取退出状态码,需要处理 exit-status 请求或使用 on_request 回调,而不是依赖 on_failure。
第二个误区是忽略了不同请求类型的数据格式差异。以 env 请求为例,它需要变量名和变量值两个独立字符串。如果手动调用 send_channel_request 时只传了一个字符串,服务器可能拒绝请求,但错误信息并不直观。此时应该打开 Net::SSH 的调试日志,观察底层消息流。可以在初始化连接时传入 verbose: :debug,或者设置 Net::SSH::Transport::Session 的日志输出。
调试通道级问题还可以使用 channel.on_process 或 channel.on_request 来监听低层消息。例如:
channel.on_process do |ch|
puts "底层消息处理中"
end
channel.on_request do |ch, type, success, payload|
puts "请求响应: #{type} #{success ? '成功' : '失败'}"
end
通过这种观测手段,可以清楚地看到每个请求的响应顺序和状态,避免在回调嵌套中迷失方向。最终的目标是让 on_failure 只做通道打开失败的兜底,而请求类型失败则依据各自的 success 参数进行精确处理。这样代码结构更清晰,也更容易在复杂 SSH 交互场景中定位问题。
Ruby Net::SSHChannel.on_failure请求类型处理修改时间:2026-08-24 19:54:21