使用JSch通过SSH隧道与远程进程通信的异常问题排查
用JSch实现SSH隧道通信的异常问题解决
我需要用JSch复现Bash中正常运行的SSH隧道功能:建立隧道后每10秒向远程发送认证字符串,并接收持续数据流。目前已成功建立SSH会话,但遇到两个异常:
异常1:本地端口转发后连接失败,抛出channel is not opened
调用session.setPortForwardingL配置本地端口转发,随后连接localhost:tunnelLocalPort时,DirectTCPIP线程抛出JSchException: channel is not opened。
相关代码
JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa") JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa") val jsch = new JSch() jsch.addIdentity(relPathPrivateKey) val session = jsch.getSession(usernameAtRemote, remoteIp, 22) session.setConfig("StrictHostKeyChecking", "no") session.connect() // 确认会话已连接并完成认证 val assignedPort: Int = session.setPortForwardingL(tunnelLocalPort, localhost, remotePort) val socket = new Socket(localhost, assignedPort) val inputStream = socket.getInputStream val outputStream = socket.getOutputStream val bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) val writer = new PrintWriter(outputStream) writer.println(auth) readLines(bufferedReader) socket.close() session.disconnect()
错误触发位置
com.jcraft.jsch.Channel类的sendChannelOpen方法:
protected void sendChannelOpen() throws Exception { // ... 第819-821行 ... if (this.open_confirmation == false) { // SSH_MSG_CHANNEL_OPEN_FAILURE throw new JSchException("channel is not opened."); // <== DirectTCPIP线程在此失败 } // ... }
异常2:使用getStreamForwarder时抛出session is down
调用session.getStreamForwarder获取通道后,执行channel.connect()时,主线程抛出JSchException: session is down。
相关代码
JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa") JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa") val jsch = new JSch() jsch.addIdentity(relPathPrivateKey) val session = jsch.getSession(usernameAtRemote, remoteIp, 22) session.setConfig("StrictHostKeyChecking", "no") session.connect() // 确认会话已连接并完成认证 val channel = session.getStreamForwarder(remoteIp, remotePort) channel.connect() // 在此失败 val inputStream = channel.getInputStream val outputStream = channel.getOutputStream val bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) val writer = new PrintWriter(outputStream) writer.println(auth) readLines(bufferedReader) channel.disconnect() session.disconnect()
错误触发位置
com.jcraft.jsch.Channel类的sendChannelOpen方法:
protected void sendChannelOpen() throws Exception { // ... 第816-818行 ... if (!_session.isConnected()) { throw new JSchException("session is down"); // <== 主线程在此失败 } // ... }
原Bash参考命令
$ ssh -l username -L LOCALPORT:127.0.0.1:REMOTEPORT REMOTEHOST $ ID='{ "id": 1234, "auth": "abcdefghijklmnopqrstuvwxyz" }' $ while true; do echo $ID; sleep 10; done | nc localhost LOCALPORT
修正后的JSch实现方案
问题核心是对JSch端口转发和通道模型的误解:
- 本地端口转发(对应
ssh -L)的正确流程是:建立会话→设置转发规则→通过本地端口创建Socket连接,而非直接操作direct-tcpip通道。 getStreamForwarder是底层流转发接口,需严格保证会话状态和通道初始化顺序。
以下是可正常运行的代码:
import com.jcraft.jsch._ import java.io._ import java.nio.charset.StandardCharsets object Main extends Log4JLogging { def main(args: Array[String]): Unit = { val remoteIpaddr = "123.45.54.321" val localhost = "127.0.0.1" val sshPort = 22 val tunnelLocalPort = LOCALPORT // 替换为实际本地端口 val tunnelRemotePort = REMOTEPORT // 替换为实际远程服务端口 val usernameAtRemoteHost = "username" val privateKeyForRemoteHost = "ssh/for-remotehost/id_rsa" val auth = "{ \"id\": 1234, \"auth\": \"abcdefghijklmnopqrstuvwxyz\" }\n" try { val currentWorkingDirectory: String = System.getProperty("user.dir") val pathPrivateKeyForRemoteHost: String = s"$currentWorkingDirectory/$privateKeyForRemoteHost" // 配置JSch支持ssh-rsa算法 JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa") JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa") val jsch = new JSch() jsch.addIdentity(pathPrivateKeyForRemoteHost) // 建立SSH会话 val session = jsch.getSession(usernameAtRemoteHost, remoteIpaddr, sshPort) session.setConfig("StrictHostKeyChecking", "no") session.connect() log.info("SSH会话已连接") // 设置本地端口转发:本地端口 -> 远程主机的127.0.0.1:REMOTEPORT val assignedPort = session.setPortForwardingL(tunnelLocalPort, localhost, tunnelRemotePort) log.info(s"本地端口转发已建立,本地端口:$assignedPort") // 连接本地转发端口,建立与远程服务的通信 val socket = new Socket(localhost, assignedPort) val outputStream = new PrintWriter(socket.getOutputStream, true) val inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8)) // 独立线程定期发送认证字符串,避免阻塞数据流读取 val senderThread = new Thread(() => { try { while (!socket.isClosed) { outputStream.println(auth) Thread.sleep(10000) // 每10秒发送一次 } } catch { case e: InterruptedException => log.info("发送线程已中断") case e: IOException => log.error("发送数据失败", e) } }) senderThread.start() // 持续读取远程返回的数据 log.info("开始接收远程数据...") var line: String = null while ({line = inputStream.readLine(); line != null}) { log.info(s"收到数据:$line") } // 清理资源 senderThread.interrupt() socket.close() session.disconnect() log.info("资源已清理,会话断开") } catch { case e: Exception => log.error("执行失败", e) System.exit(1) } } }
关键修正点
- 端口转发流程:设置
setPortForwardingL后,通过Socket连接本地端口,这是模拟ssh -L的标准方式。 - 线程分离:将认证字符串发送逻辑放在独立线程,避免阻塞主线程的数据流读取。
- 资源管理:确保Socket、流和会话在结束时正确关闭,避免资源泄漏。
内容的提问来源于stack exchange,提问作者gknauth
相关产品推荐
相关产品推荐

