Ganymed SSH-2 for Java

参考

https://github.com/sohutv/cachecloud/blob/master/cachecloud-open-web/src/main/java/com/sohu/cache/ssh/SSHTemplate.java
实例:https://www.jianshu.com/p/513c72dfee1b
https://www.iteye.com/blog/hotbain-1924146
https://www.cnblogs.com/umgsai/p/7846177.html
源码:https://github.com/hudson/ganymed-ssh-2
https://www.iteye.com/blog/8366-378867
https://www.it610.com/article/1305304455410913280.htm
多线程:https://blog.csdn.net/qq_31865983/article/details/106137777

实现原理

Ganymed SSH-2 java在整个访问过程中担当SSH的客户端,由于Linux系统自带SSH服务,所以可以直接访问Linux系统并执行相关命令,而 Windows系统则需要首先安装SSH服务。

引用

        <dependency>
            <groupId>ch.ethz.ganymed</groupId>
            <artifactId>ganymed-ssh2</artifactId>
            <version>build210</version>
        </dependency>

代码

package com.jd.orange;

import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import ch.ethz.ssh2.StreamGobbler;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@ActiveProfiles(profiles = "suqian")
@RunWith(SpringRunner.class)
@Slf4j
@SpringBootTest
public class AsTests {
    @Test
    public void a()  {
        Connection connection = null;
        try {
            connection = new Connection("10.206.66.68");
            connection.connect();
            boolean isAuthenticated = connection.authenticateWithPassword("root", "ssa");
            if (!isAuthenticated)
                throw new IOException("Authentication failed.");
            log.info("sss");
            Session session = connection.openSession();
            session.execCommand("cd / && ls");
            writeInfo(session);
            writeErrorInfo(session);
            System.out.println("ExitCode: " + session.getExitStatus());
            System.out.println("signal: " + session.getExitSignal());
            session.close();
            Session session2 = connection.openSession();
            session2.execCommand("ls");
            writeInfo(session2);
            writeErrorInfo(session2);
            System.out.println("ExitCode: " + session.getExitStatus());
            System.out.println("signal: " + session.getExitSignal());
        }catch (Exception e) {
            connection.close();
        }
    }

    // 取值
    public void writeInfo(Session sess) throws Exception{
        log.info("输出所有信息");
        // StreamGobbler已经实现了缓冲区,不需要额外的缓存区封装,如:BufferedOutputStream
        // 读取正常内容后再读错误信息
        InputStream stdout = new StreamGobbler(sess.getStdout());
        BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
        // 打印输出结果
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }

    public void writeErrorInfo(Session sess)  throws Exception{
        log.info("输出错误信息");
         // 只有在执行结果出现错误时,session.getStderr()才会返回inputstream  
        // 如果有错误流,读取错误流,需要注意的是输入流、输出流、错误流共享缓冲区,所以别不管错误流内容
        // 不读错误流,一旦缓冲区被用尽,命令就会被阻塞,不能执行了。当然了缓冲区大小:30kb,一般也不是那么容易占满
        InputStream stderr = new StreamGobbler(sess.getStderr());
        BufferedReader br = new BufferedReader(new InputStreamReader(stderr));
        // 打印输出结果
        // 打印输出结果
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    }

}

验证方式

1、boolean isAuthenticated = connection.authenticateWithPassword(“root”, “ssa”);
2、boolean isAuthenticated = conn.authenticateWithPublicKey(username, new File(Constants.PUBLIC_KEY_PEM), password);

问题一、Session session = connection.openSession(); 一个session只能执行一条命令,执行多条命令会报错

在这里插入图片描述
要执行多条命令:
方法一: 多条命令连接起来

sess.execCommand("cd test;cat 1.txt");

方法二:关闭session,再新建一个session

Session sess = conn.openSession();
sess.execCommand("cd test");//进入~/test路径
sess.close();
sess = conn.openSession();//由于是新打开的会话,此时在~路径下
sess.execCommand("cat 1.txt");

方法三:使用Session.startShell() 不能和sess.execCommand一块使用
// 建立虚拟终端 pty(伪终端,开启远程的客户端) TODO requestPTY requestDumbPTY startShell 区别是啥?
this.sess.requestPTY(“bash”);
// 打开一个Shell
this.sess.startShell();

session = connection.openSession();
            session.requestPTY("dumb");
            session.startShell();
            PrintWriter out = new PrintWriter(session.getStdin());
            // 输入待执行命令
            out.println("cd / && ls");
            out.flush();
            // 关闭输入流
            log.info("执行命令1");
            session.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS , 2000);
            System.out.println("ExitCode: " + session.getExitStatus());
            System.out.println("signal: " + session.getExitSignal());
            // 输入待执行命令
            out.println("ls");
            out.flush();
            // 关闭输入流
            out.close();
            log.info("执行命令2");
            session.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS, 2000);
            log.info("执行命令2结束");

问题二、使用sess.execCommand(commandStepDTO.getCmd());这种方式执行Shell命令,会避免环境变量读取不全而执行失败的问题
方法:
// 准备输入命令
PrintWriter out = new PrintWriter(sess.getStdin());
// 输入待执行命令
out.println(cmd);
out.flush();
// 关闭输入流
out.close();
// 等待,除非1.连接关闭;2.输出数据传送完毕;3.进程状态为退出;4.超时
sess.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS , timeout);
// todo 命令执行失败是否继续执行
if(sess.getExitStatus() < 0){
log.info(“命令执行失败”);
}

问题三:当一个Shell命令执行时间过长时,会遇到ssh连接超时的问题。
第一种方法:采用nohup后台执行;
第二种方法:把Linux主机的sshd_config的参数ClientAliveInterval设为60,同时将waitForCondition中timeout时间设置很大,来保证命令执行完毕

问题四:如果使用Sess.execCommand()得到的结果和预期不一样或根本不能执行,要注意环境变量
方法:直接指定全路径

问题五:只从stdout中读取数据时,进程有时候会挂起
底层SSH2协议为stdout和stderr定义了一个共享的接收窗,如果远程的SSH进程产生了很多stderr信息但是你从来都没有消费过它们,一段时间后,本地的接收窗将被填满,此时数据发送端将被挂起,如果这时你试图读取stdout,你的请求也将被挂起。由于接收窗已满,因此接收窗中没有任何可用的stdout信息,并且远程SSH进程也不会发送任何stdout信息(除非先读取一些stderr信息以释放部分接收窗的空间)
Ganymed SSH-2使用30KB大小的接收窗,所以上面描述的情景很少发生

方法一:使用Ganymed SSH-2自带的StreamGobbler类即可使用2个工作线程并行消费远程的stdout和stderr,StreamGobbler是一个特殊的输入流,它能使用内部工作线程读取、缓存所有另一个输入流输入的信息,示例如下:
InputStream stdout = new StreamGobbler(mysession.getStdout());
InputStream stderr = new StreamGobbler(mysession.getStderr());
然后你就可以以任何顺序访问stdout和stderr,StreamGobblers将会在后台自动消费所有远程端口传递过来的数据并存放在一个内部的buffer中

方法二:sess.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS , timeout);
Sess.waitForCondition方法可以获取流的状态信息,通过得到的状态信息来判断下一步做什么

问题六:使用Ganymed SSH-2调用服务器脚本的时候,进程经常会卡主
可能原因:执行命令的时候没有返回值的时候会卡住。
解决方法:1、多加个判断,强制退出一下循环就好了;2、使用线程控制

问题七:br.readLine() 不是null ,啥也没有,执行卡顿
原因:readLine是阻塞函数
(1)readLine()方法在进行读取一行时,只有遇到回车(\r)或者换行符(\n)才会返回读取结果
readLine()是一个阻塞函数,当没有数据读取时,就一直会阻塞在那,而不是返回null。没有数据时会阻塞,在数据流异常或断开时才会返回null
readLine()只有在数据流发生异常或者另一端被close()掉时,才会返回null值。
如果不指定buffer大小,则readLine()使用的buffer有8192个字符。在达到buffer大小之前,只有遇到"/r"、“/n”、"/r/n"才会返回。
(2)标准输出和标准错误信息要同时读取,只读其中一个 会卡顿

    // 获取命令执行结果
    private void getResult(CmdResultDTO cmdResultDTO) {
        // stdOutStream 和 stdErrStream 流 共用同一个 buffer,当 errStream 写满了 buffer 之后,outStream 就无法再写入内容,造成 outStream hang 住。
        StringBuffer sb1 = new StringBuffer();
        StringBuffer sb2 = new StringBuffer();
        Future<Boolean> infoFuture = messAsync.getMess(sess.getStdout(), sb1);
        Future<Boolean> errInfoFuture = messAsync.getMess(sess.getStderr(), sb2);
        try {
            infoFuture.get(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
        }
        ResponseModel<String> responseModel2 = null;
        try {
            errInfoFuture.get(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
        }
        cmdResultDTO.setInfo(sb1.toString());
        System.out.println(sb1.toString());
        cmdResultDTO.setErrorIndo(sb2.toString());
        System.out.println(sb2.toString());
    }

@Slf4j
@Component
public class MessAsync {

    @Async("getMessExecutor")
    public Future<Boolean> getMess(InputStream inputStream, StringBuffer sb) {
        log.info("当前线程id" + Thread.currentThread().getId() + ",当前线程名" + Thread.currentThread().getName());
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        Boolean flag = true;
        String line = null;
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line + "\n");
            }
        } catch (Exception e) {
            log.error("读流信息失败", e.getMessage());
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                log.error("关闭流失败", e.getMessage());
            }
        }
        log.info("当前线程id" + Thread.currentThread().getId() + ",当前线程名" + Thread.currentThread().getName() + "执行结束");
        return new AsyncResult<>(flag);
    }
}

问题八:line返回的是多行命令的结果
在这里插入图片描述

StartShell()使用方法

参考:https://www.tabnine.com/code/java/methods/com.trilead.ssh2.Session/startShell

LinuxClient.getSession()
linux客户端

private Session getSession() {
  try {
    Session session = conn.openSession();
    session.requestPTY("dumb");
    session.startShell();
    return session;
  } catch (Exception e) {
    String msg = "\nOpen SSH2 Session Error !";
    logger.error(msg, e);
    throw new RuntimeException(msg, e);
  }
}

CommonLinuxClient.initSession()
通用linux客户端

private void initSession() {
  try {
    session = conn.openSession();
    session.requestDumbPTY();
    session.startShell();
    stdoutReader = new BufferedReader(new InputStreamReader(
        session.getStdout()));
    stderrReader = new BufferedReader(new InputStreamReader(
        session.getStderr()));
    out = new PrintWriter(session.getStdin());
  } catch (Exception e) {
    String msg = "\nOpen SSH2 Session Error !";
    logger.error(msg, e);
    throw new RuntimeException(msg, e);
  }
}

Remote Cmd Client.execCmdWithPTY(…)
远程命令客户端

log.debug("start cmd remoteCmdClient.......");
session.requestPTY("dumb", 500, 300, 0, 0, null);
session.startShell();

CommonSshClient.executeCommand(…)
通用shell客户端
session = conn.openSession();
session.requestPTY(“dumb”);
session.startShell();
ExecutorService exec = Executors.newSingleThreadExecutor();
Future task = exec.submit(new OutputTask(session, output));

SSH Console Device.connect()
SSH控制台设备
session.startShell();

完整实现

package com.orange.service.common;

import ch.ethz.ssh2.ChannelCondition;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;

import com.orange.dto.*;
import com.orange.dto.caseDto.StepDTO;
import com.orange.dto.reportDto.CmdResultDTO;
import com.orange.dto.reportDto.StepResultDTO;
import com.orange.enums.ResponseStatus;
import com.orange.enums.StepTypeEnum;
import com.orange.util.ResponseModel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * 执行远程服务器上的shell命令
 * 连接一次执行多个命令,每次执行命令可以获取结果
 * 需指定type 和env
 */
@Slf4j
@Service
@Scope("prototype")
public class RemoteShellExecutorService {
    private Connection conn = null;
    private Session sess = null;

    @Autowired
    private MessAsync messAsync;

    public CmdResultDTO executeCmdStep(StepDTO stepDTO) {
        // 1 建连接
        login(stepDTO.getCommandStep().getMachineDTO());
        // 2 执行命令
        CmdResultDTO cmdResultDTO = exec(stepDTO);
        cmdResultDTO.setStatus(true);
        // 3 关闭连接
        closeAll();
        return cmdResultDTO;
    }

    // 建立连接
    public void login(MachineDTO machineDTO) {
        try {
            // 创建连接对象,默认端口号是22
            conn = new Connection(machineDTO.getIp());
            // 建立连接
            // verifier:验证主机秘钥的正确性
            // connectTimeout: 就是tcp连接超时,0:不超时,不能为负数
            // kexTimeout:ssh的连接超时,指的是这个方法开始调用直到密钥交换结束的时间,0:不超时,不能为负数
            conn.connect();
            // 身份认证, 当前使用用户名和密码方式,使用conn.getRemainingAuthMethods()可查看支持的验证方式
            boolean isAuthenticated = conn.authenticateWithPassword(machineDTO.getUsername(), machineDTO.getPassword());
            if (!isAuthenticated) {
                throw new IOException("Authentication failed.");
            }
            sess = conn.openSession();
            // 建立虚拟终端 pty(伪终端)
            sess.requestDumbPTY();
            // 打开一个Shell进程
            sess.startShell();
        } catch (Exception e) {
            log.error("建立shell连接失败,异常信息:" + e.toString());
            this.closeAll();
        }
    }

    // 发送命令
    public CmdResultDTO exec(StepDTO stepDTO) {
        CmdResultDTO cmdResult = new CmdResultDTO();
        BeanUtils.copyProperties(stepDTO.getCommandStep(), cmdResult);
        try {
            // session.execcommand在远程机器上执行会出现环境变量丢失或者错误的问题,用获取pty(虚拟终端)和启动sell命令来代替
            // 输入待执行命令
            PrintWriter pw = new PrintWriter(sess.getStdin());
            // 初始化环境
            List<String> cmds = initEnv(stepDTO);
            cmds.addAll(stepDTO.getCommandStep().getCmds());
            for (String cmd : cmds) {
                pw.println(cmd + "\n");
                pw.flush();
                // 等待,除非1.连接关闭;2.输出数据传送完毕;3.进程状态为退出;4.超时
                sess.waitForCondition(ChannelCondition.CLOSED | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS, stepDTO.getTimeout());
            }
            pw.close();
            getResult(cmdResult);
        } catch (Exception e) {
            log.error("命令执行失败" + e.toString());
            cmdResult.setStatus(false);
        }
        return cmdResult;
    }

    private List<String> initEnv(StepDTO stepDTO) {
        List<String> envs = new ArrayList<>();
        if (stepDTO.getStepType().equals(StepTypeEnum.HDFS.getValue()) || StringUtils.isNotBlank(stepDTO.getCommandStep().getNnSign())) {
            NNDTO nndto = stepDTO.getCommandStep().getNndto();
            if (nndto != null) {
                envs.add("su - " + nndto.getAccount());
            }
        } else if (stepDTO.getStepType().equals(StepTypeEnum.HIVE.getValue()) || StringUtils.isNotBlank(stepDTO.getCommandStep().getExportSign())) {
            ExportDTO exportDTO = stepDTO.getCommandStep().getExportDTO();
            if (exportDTO != null) {
                if (StringUtils.isNotBlank(exportDTO.getAccount())) {
                    envs.add("su - " + exportDTO.getAccount());
                }
                if (StringUtils.isNotBlank(exportDTO.getCluster())) {
                    envs.add("export JDHXXXXX_CLUSTER_NAME=" + exportDTO.getCluster());
                }
                if (StringUtils.isNotBlank(exportDTO.getUser())) {
                    envs.add("export JDHXXXXX_USER=" + exportDTO.getUser());
                }
                if (StringUtils.isNotBlank(exportDTO.getTeamUser())) {
                    envs.add("export TEAM_USER=" + exportDTO.getTeamUser());
                }
                if (StringUtils.isNotBlank(exportDTO.getQueue())) {
                    envs.add("export JDHXXXXX_QUEUE=" + exportDTO.getQueue());
                }
                if (StringUtils.isNotBlank(exportDTO.getSource())) {
                    envs.add("source " + exportDTO.getSource());
                }
            }
        }
        return envs;
    }

    // 获取命令执行结果
    private void getResult(CmdResultDTO cmdResultDTO) {
        // stdOutStream 和 stdErrStream 流 共用同一个 buffer,当 errStream 写满了 buffer 之后,outStream 就无法再写入内容,造成 outStream hang 住。
        StringBuffer sb1 = new StringBuffer();
        StringBuffer sb2 = new StringBuffer();
        Future<Boolean> infoFuture = messAsync.getMess(sess.getStdout(), sb1);
        Future<Boolean> errInfoFuture = messAsync.getMess(sess.getStderr(), sb2);
        try {
            infoFuture.get(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
        }
        ResponseModel<String> responseModel2 = null;
        try {
            errInfoFuture.get(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
        }
        cmdResultDTO.setInfo(sb1.toString());
        log.info("info:" + sb1.toString());
        cmdResultDTO.setErrorInfo(sb2.toString());
        log.info("errorInfo:" + sb2.toString());
    }

    // 关闭连接
    public void closeAll() {
        if (sess != null) {
            sess.close();
        }
        if (conn != null) {
            conn.close();
        }
    }
}




@Slf4j
@Component
public class MessAsync {

    @Async("getMessExecutor")
    public Future<Boolean> getMess(InputStream inputStream, StringBuffer sb) {
        log.info("当前线程id" + Thread.currentThread().getId() + ",当前线程名" + Thread.currentThread().getName());
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        Boolean flag = true;
        String line = null;
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line+"\n");
            }
        } catch (Exception e) {
            log.error("读流信息失败", e.getMessage());
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                log.error("关闭流失败", e.getMessage());
            }
        }
        log.info("当前线程id" + Thread.currentThread().getId() + ",当前线程名" + Thread.currentThread().getName() + "执行结束");
        return new AsyncResult<>(flag);
    }
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值