关于JSch的使用,执行ssh命令,文件上传和下载以及连接方式

来源:互联网 发布:金融行业前景知乎 编辑:程序博客网 时间:2024/05/18 03:27

最近在做一个SAAS服务的项目,SAAS就是软件即服务,具体可以去问度娘,然后底层呢需要远程执行SSH命令来进行支持,最后就选择了JSch来完成这个工作。

JSch是SSH2的一个纯JAVA实现。它允许你连接到一个sshd服务器,使用端口转发,X11转发,文件传输等等。

大致需求就是能够用java代码来实现对服务器的一系列操控,其实就是执行一个业务流程的命令。

因为很多的环境配置,系统命令等都已经写好了脚本,我们用java代码要实现的就是,连上服务器,执行命令,上传、下载文件,执行脚本等一系列操作。。。

我的设想:

关于对服务器操作的对外提供三个主要方法;

1、执行命令方法;

2、文件上传方法;

3、文件下载方法。

由于文件上传和下载又涉及到进度问题,所以又提供了4个对外获取文件上传和下载情况查看的方法:

1、获取文件大小方法;

2、获取文件已传输大小方法;

3、判断文件是否已经传输完成方法;

4、获取文件传输百分比方法。

下面说下具体实现,一步步来说吧!

一、引入JSch的jar包(我是在POM添加的)

我引入的是JSch最新的包,附上:

<!-- https://mvnrepository.com/artifact/com.jcraft/jsch --><dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.53</version></dependency>
二、建工具类

public class SSHUtil{private static final Logger logger = LoggerFactory.getLogger(SSHUtil.class);}
三、定义初始化常量(后面再说具体作用)

//获取缓存对象(这个是我们项目的缓存系统,根据命名空间去取存放的数据)private BaseCache<HashMap<String, Long>> cache;//命名空间(用于从缓存中存放和取出数据)private static final String CACHE_NAMESPACE = "syncssh";//默认session通道存活时间(我这里定义的是5分钟)private static int SESSION_TIMEOUT = 300000;//默认connect通道存活时间private static int CONNECT_TIMEOUT = 1000;//默认端口号private static int DEFULT_PORT = 22;    //初始化对象private JSch jsch = null;private Session session = null;//用于读取的唯一ID(这个是用于读取某个文件上传或者下载的进度,都放在缓存空间,我肯定需要一个ID来找到是调用者是想找到哪个文件的传输进度)private String PROCESSID;
四、获取连接

这里要说明一下,获取Session连接的方式有两种,一种是直接账号、密码、IP地址、端口号就能连接,另外一种就是秘钥方式连接(免密连接);

我们的SAAS服务集群是基于一个SAAS管理服务器,所有的子服务器都是通过该服务器进行管理,所以要操控子服务器进行练级,就要在主服务器上生成一个秘钥对;

可以使用命令:     ssh-keygen -t rsa  来生成秘钥;   生成的秘钥是根据当前登录用户的账号生成的,也就是说跟当前登录用户的账号是绑定的,

然后就可以在 ssh文件夹中看到有一个  id_rsa  id_rsa.pub  两个文件(也有自定义名称方法,具体可以查一下);

id_rsa是私钥,id_rsa.pub是公钥,接下来要做的就是把这个公钥拷贝到子服务器的ssh文件夹下

然后调用命令 : cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys   

这一步就是把该公钥作为子服务器的信任列表中;

要明白这样做的意义就要先明白免密连接的具体步骤:

1.在A上生成公钥私钥。 

2.将公钥拷贝给server B,要重命名成authorized_keys(从英文名就知道含义了) 

3.Server A向Server B发送一个连接请求。 

4.Server B得到Server A的信息后,在authorized_key中查找,如果有相应的用户名和IP,则随机生成一个字符串,并用Server A的公钥加密,发送给Server A。 

5.Server A得到Server B发来的消息后,使用私钥进行解密,然后将解密后的字符串发送给Server B。Server B进行和生成的对比,如果一致,则允许免登录。 

那么现在,子服务器上就有有了主服务器上的一个公钥,而主服务器本身存了一份私钥。

下面讲一下我对这两个登录方式的定义:

该工具类的构造方法是不对外的,只提供两个获取实例的方法 ,分别是:

/**     * 根据服务器IP、账号、密码获取jschUtil实例     * @param user  服务器账号     * @param password服务器密码     * @param host服务器ip     * @param port端口号 (传null默认22端口)     * @return  账号、密码、IP不允许为null 有为null返回null     * @throws JSchException     * @author wangyijie     * @data 2017年12月7日     */    public static SSHUtil getInstance(String user, String password, String host, Integer port) throws JSchException {    if (StringUtils.isBlank(user) || StringUtils.isBlank(password) || StringUtils.isBlank(host)) {    return null;    }    if (port == null) {    port = DEFULT_PORT;//这个是上面初始化的端口号22    }        SSHInfo sshInfo = new SSHInfo(user, password, host, port);        return new SSHUtil(sshInfo);    }        /**     * 根据服务器IP、账号、秘钥地址、秘钥密码获取jschUtil实例     * @param user  服务器账号     * @param host服务器地址     * @param port端口号 (传null默认22端口)     * @param privateKey秘钥地址(本地存放的私钥地址)     * @param passphrase秘钥密码     * @return账号、IP、秘钥地址不允许为null 有为null返回null     * @throws JSchException     * @author wangyijie     * @data 2017年12月8日     */    public static SSHUtil getInstance( String user, String host, Integer port ,String privateKey ,String passphrase) throws JSchException{    if (StringUtils.isBlank(user) || StringUtils.isBlank(host) || StringUtils.isBlank(privateKey)) {    return null;    }    if (port == null) {    port = DEFULT_PORT;//这个是上面初始化的端口号22    }    SSHInfo sshInfo = new SSHInfo(user, host, port, privateKey, passphrase);    return new SSHUtil(sshInfo);    }

这里我们看到了我new了一个SSHInfo的对象

这个对象是获取Session连接的重要对象,来看一下这个对象,这个类我是直接在工具类中定义的;(后面的需要的类也都是在该工具类中定义的,因为别的地方用不到)

private static class SSHInfo{        private String user;//服务器账号        private String password;//服务器密码        private String host;//地址        private int port;//端口号        private String privateKey;  //秘钥文件路径(本地存放的私钥地址)        private String passphrase;//秘钥的密码(如果秘钥进行过加密则需要)                /**         * 账号密码方式构造         * @param user 账号         * @param password 密码         * @param host  IP地址         * @param port  端口号         */        public SSHInfo(String user, String password, String host, int port) {        this.user = user;        this.password = password;        this.host = host;        this.port = port;        }                /**         * 秘钥方式构造         * @param user  账号         * @param hostIP地址         * @param port端口号         * @param privateKey  秘钥地址(本地存放的私钥地址)         * @param passphrase  秘钥密码(如果秘钥被加密过则需要)         */        public SSHInfo(String user, String host, int port, String privateKey, String passphrase) {        this.user = user;        this.host = host;        this.port = port;        this.privateKey = privateKey;        this.passphrase = passphrase;        }                public String getPrivateKey() {return privateKey;}public void setPrivateKey(String privateKey) {this.privateKey = privateKey;}public String getPassphrase() {return passphrase;}public void setPassphrase(String passphrase) {this.passphrase = passphrase;}        public String getUser() {            return user;        }        public String getPassword() {            return password;        }        public String getHost() {            return host;        }        public int getPort() {            return port;        }    }
这个类我也提供了两个构造函数,一个账号密码方式构造,一个秘钥连接方式构造

passphrase这个字段的意思就是当初生成秘钥对的时候是否对秘钥加密过,如果加密过,就需要进行解密,这个字段就是对秘钥加密的密码是什么;

这个类看了之后我们再回到之前的获取实例方法上,那么最后这个SSHInfo对象都被传到SSHUtil的构造函数中,下面看一下SSHUtil的构造函数:

private SSHUtil(SSHInfo sshInfo) throws JSchException {    //初始化缓存    cache = SpringContext.getBean("localCache");    //设置缓存生命周期为1天    cache.set(CACHE_NAMESPACE, TimeUnit.DAYS.toSeconds(1));        //实例化工具类的时候开启Session通道        jsch =new JSch();        //秘钥方式连接        if (StringUtils.isNotBlank(sshInfo.getPrivateKey())) {            if (StringUtils.isNotBlank(sshInfo.getPassphrase())) {              //设置带口令的密钥                  jsch.addIdentity(sshInfo.getPrivateKey(), sshInfo.getPassphrase());              } else {              //设置不带口令的密钥                   jsch.addIdentity(sshInfo.getPrivateKey());              }          }        //获取session连接        session = jsch.getSession(sshInfo.getUser(),sshInfo.getHost(),sshInfo.getPort());        //连接失败        if (session == null) {        if (StringUtils.isNotBlank(sshInfo.getPrivateKey())) {        logger.error("JSCH秘钥方式开启Session通道——失败,服务器账号:{},秘钥地址:{},秘钥口令:{},IP地址:{},端口号:{}",sshInfo.getUser(),        sshInfo.getPrivateKey(), sshInfo.getPassphrase(), sshInfo.getHost(), sshInfo.getPort());        } else {        logger.error("JSCH账号密码方式开启Session通道——失败,服务器账号:{},秘钥:{},IP地址:{},端口号:{}",sshInfo.getUser(),                sshInfo.getPassword(),sshInfo.getHost(),sshInfo.getPort());        }        }        //如果密码方式连接  session传入密码        if (StringUtils.isNotBlank(sshInfo.getPassword())) {        session.setPassword(sshInfo.getPassword());        }        session.setUserInfo(new MyUserInfo());        //设置session通道最大开启时间  默认5分钟  可调用close()方法关闭该通道        session.connect(SESSION_TIMEOUT);        if (StringUtils.isNotBlank(sshInfo.getPrivateKey())) {        logger.info("JSCH秘钥方式开启Session通道——成功,服务器账号:{},秘钥地址:{},秘钥口令:{},IP地址:{},端口号:{}",sshInfo.getUser(),    sshInfo.getPrivateKey(), sshInfo.getPassphrase(), sshInfo.getHost(), sshInfo.getPort());        } else {         logger.info("JSCH开启Session通道——成功,服务器账号:{},秘钥:{},IP地址:{},端口号:{}",sshInfo.getUser(),             sshInfo.getPassword(),sshInfo.getHost(),sshInfo.getPort());        }    }

记住!这个方法要的私钥地址是本地存放的地址!本地!   如果要测试的话,比如windows系统,只需要把那个生成的私钥下载到你的电脑上,然后把路径指向这个私钥就行了!!!   (我就被这玩意坑了一下~~~)

那么看到这里大致这个Session连接就有了

我上面定义Session对象的实例的时候是没有定义成静态的,所以每个人调用这个方法的时候获取Session是不共用的,线程是不共享的。

另外,在获取Session的时候我new了一个MyUserInfo对象对吧,先来看一下这个内部类:

/*     * 自定义UserInfo     */     private static class MyUserInfo implements UserInfo{        @Override         public String getPassphrase() {        return null;        }        @Override         public String getPassword() {        return null;        }        @Override         public boolean promptPassword(String s) {        return false;        }        @Override         public boolean promptPassphrase(String s) {          return false;         }        @Override        public boolean promptYesNo(String s) {            return true;        }        @Override         public void showMessage(String s) { }     }     
说实话,我当时也没具体研究这个类的左右,我只知道在promptYesNo方法中return true;就不会在连接的时候询问是否确定要连接,还有一种方法可以直接确认这个询问,我这里就不多说了;

Session连接已经有了,下面就是具体的执行方法实现了。

五、命令执行方法

话不多说,先贴代码:

 /**     * 执行命令     * @param cmd  要执行的命令     * <ol>     * 比如:     * <li>ls</li>     * <li>cd opt/</li>     * </ol>     * <ol>     * 多个连续命令可用 && 连接     * <li>cd /opt/softinstaller && chmod u+x *.sh && ./installArg.sh java</li>     * </ol>     * @return  成功执行返回true   连接因为错误异常断开返回false     * @throws IOException     * @throws JSchException     * @throws InterruptedException     * @author wangyijie     * @data 2017年12月7日     */    public boolean exec(String cmd) throws IOException, JSchException, InterruptedException {    logger.warn("JSCH执行系统命令:{}",cmd);    //开启exec通道        ChannelExec channelExec = (ChannelExec)session.openChannel( "exec" );        if (channelExec == null) {        logger.error("JSCH打开exec通道失败,需要执行的系统命令:{}",cmd);        }        channelExec.setCommand( cmd );        channelExec.setInputStream( null );        channelExec.setErrStream( System.err );        //获取服务器输出流        InputStream in = channelExec.getInputStream();        channelExec.connect();         int res = -1;        StringBuffer buf = new StringBuffer( 1024 );        byte[] tmp = new byte[ 1024 ];        while ( true ) {            while ( in.available() > 0 ) {                int i = in.read( tmp, 0, 1024 );                if ( i < 0 ) {                break;                }                buf.append( new String( tmp, 0, i ) );            }            if ( channelExec.isClosed() ) {                res = channelExec.getExitStatus();                break;            }            TimeUnit.MILLISECONDS.sleep(100);        }        logger.warn("系统命令:{},执行结果:{}", cmd, buf);        //关闭通道        channelExec.disconnect();        if (res == IConstant.TRUE) {        return true;        }        return false;    }
这里用到的就是JSch的exec通道,通过之前我们获取的session打开这个通道,然后把命令放进去,通过getInputStream()方法,获取一个输入流,这个输入流是用来读取该命令执行后,服务器的执行结果,比如:执行ls,那么服务器本身肯定会有反馈的,这里就是把这个反馈读出来。

我这里只是把反馈写在了日志中,而返回结果只是给了调用成功或者失败。

六、文件上传和下载方法

代码开路:

/**     * 上传文件到服务器(上传传到服务器后的文件名与上传的文件同名)     * @param uploadPath  要上传到服务器的路径     * @param filePath  本地文件的存储路径     * @param processid 唯一ID(用于查看上传的进度,多个地方调用请勿重复)     * @param 例如:.sftpUpload("/opt", "F:\\softinstaller.zip");     * @throws Exception     * @author wangyijie     * @data 2017年12月8日     */    public void sftpUpload(String uploadPath, String filePath, String processid){          Channel channel = null;          try {          logger.warn("JSCH开启sftp通道上传到服务器文件————————上传到服务器位置uploadPath={},文件所在路径filePath={}",        uploadPath, filePath);            //创建sftp通信通道              channel = (Channel) session.openChannel("sftp");            if (channel == null) {            logger.error("JSCH开启sftp通道上传到服务器文件失败————————上传到服务器位置uploadPath={},文件所在路径filePath={}",            uploadPath, filePath);            return;            }            //指定通道存活时间            channel.connect(CONNECT_TIMEOUT);            ChannelSftp sftp = (ChannelSftp) channel;              //设置查看进度的ID(只对该线程有效)            PROCESSID = processid;            cache.put(CACHE_NAMESPACE, processid, new HashMap<String, Long>());            //这个对象是为了查看进度            Monitor monitor = new Monitor();            //开始复制文件            sftp.put(filePath, uploadPath, monitor);            logger.warn("JSCH关闭sftp通道上传到服务器文件—————————上传到服务器位置uploadPath={},文件所在路径filePath={}",        uploadPath, filePath);        } catch (Exception e) {           logger.warn("sftp通道上传到服务器文件错误—————————上传到服务器位置uploadPath={},文件所在路径filePath={}",         uploadPath, filePath);        }    }
这里文件上传用的是sftp,代码注释应该能看懂了吧~~~~

这里我要说明的一点就是关于查看进度,sftp复制文件有很多种方法,我这里只是其中的一种,具体可百度查询,但是想要查看进度,就必须要涉及到一个对象Monitor ;

这个对象我下面再细说,这里我将调用该方法的人传入的唯一ID跟该文件上传的进度绑定在了一起。

下面直接贴上下载的方法:

 /**     * 下载服务器指定路径的指定文件     * @param fileName   服务器上的文件名     * @param downloadPath要下载的文件在服务器上的路径     * @param filePath要存在本地的位置     * @param processid  唯一ID(用于查看下载的进度,多个地方调用请勿重复)     * @throws Exception     * @author wangyijie     * @data 2017年12月11日     */    public void sftpDownload(String fileName, String downloadPath, String filePath, String processid) {     Channel channel = null;           try {           logger.warn("JSCH开启sftp通道下载服务器文件————————文件名fileName={},下载的文件位于服务器位置downloadPath={},文件下载到本地的路径filePath={}",         fileName, downloadPath, filePath);             //创建sftp通信通道               channel = (Channel) session.openChannel("sftp");             if (channel == null) {             logger.error("JSCH开启sftp通道下载服务器文件失败————————文件名fileName={},下载的文件位于服务器位置downloadPath={},文件下载到本地的路径filePath={}",              fileName, downloadPath, filePath);             return;             }             //指定通道存活时间             channel.connect(CONNECT_TIMEOUT);             ChannelSftp sftp = (ChannelSftp) channel;               //进入服务器指定的文件夹               sftp.cd(downloadPath);               //该对象用于查看进度             Monitor monitor = new Monitor();             sftp.get(fileName, filePath, monitor);             logger.warn("JSCH关闭sftp通道下载服务器文件————————文件名fileName={},下载的文件位于服务器位置downloadPath={},文件下载到本地的路径filePath={}",             fileName, downloadPath, filePath);         } catch (Exception e) {           logger.error("sftp通道下载服务器文件错误————————文件名fileName={},下载的文件位于服务器位置downloadPath={},文件下载到本地的路径filePath={}",             fileName, downloadPath, filePath);         }    }    
看完这个其实跟上传差不多,只不过一个是get,一个是put,方法底层都写好了,我们只需要调用传入参数就行了。
好了,然后就直接开始说这个进度问题吧!

七、进度查看

先贴上代码:

/**  * 用于文件上传或者下载的进度查看  * @author wangyijie  * @date 2017年12月8日  * @version 1.0  */    private class Monitor implements SftpProgressMonitor {    private long COUNT = 0;    /**     * 文件开始上传执行方法     */    @Override    public void init(int op, String src, String dest, long max) {    HashMap<String, Long> map = new HashMap<String, Long>();  //根据命名空间和唯一ID去系统缓存模块中取得缓存对象  下面一样的道理    if (map != null) {    map.put("maxsize", max); //文件大小   单位/B    map.put("count", COUNT); //已经传输的大小(目前是0)单位/B   下面一样的    map.put("isend", 0L); //是否传输完成   下面一样的    cache.put(CACHE_NAMESPACE, PROCESSID, map);    }    }    /**     * 文件每传送一个数据包执行方法     */    @Override    public boolean count(long count) {    COUNT = COUNT + count; //没传输完成一个数据包就加到已经传输的大小上     HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);    if (map != null) {    map.put("count", COUNT);    map.put("isend", 0L);    cache.put(CACHE_NAMESPACE, PROCESSID, map);    }    return true;    }    /**     * 文件传输完成执行方法     */    @Override    public void end() {       HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);    if (map != null) {    map.put("isend", 1L);    cache.put(CACHE_NAMESPACE, PROCESSID, map);    }    }     }

这个类就是我上面说的用于查看进度的类,当我们调用sftp的get、put方法时可以传入该类,然后该类实现了SftpProgressMonitor 接口,实现了接口的三个方法,

我注释上已经标出这三个方法的执行时间。

我在SSHUtil中定义了一个用于查看进度的唯一ID,然后每个调用者获取实例的时候,调用上传或者下载方法都会给这个ID,当上传或下载执行的时候,那么缓存中就动态的存储了目前文件传输的情况,然后我对外提供了获取进度的方法,这样调用者就可以获取进度,也就是说文件传输的线程来更新进度,另外的线程用来获取进度。

然后我们来看一下提供的方法:

/**     * 获取文件大小     * @param processid  文件上传或下载方法传入的唯一ID     * @return     * @author wangyijie     * @data 2017年12月8日     */    public Long getFileSize (String processid) {    HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID); //根据命名空间和唯一ID获取缓存对象    下面几个方法一样的  把另一个线程put的值取出来    if (map == null) {    return null;    }    return map.get("maxsize");    }        /**     * 获取已经传输的文件大小     * @param processid 文件上传或下载方法传入的唯一ID     * @return     * @author wangyijie     * @data 2017年12月8日     */    public Long getCount (String processid) {    HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);    if (map == null) {    return null;    }    return map.get("count");    }        /**     * 文件是传输完成     * @param processid 文件上传或下载方法传入的唯一ID     * @return 完成返回1 、  未完成返回0     * @author wangyijie     * @data 2017年12月8日     */    public Integer isEnd (String processid) {    HashMap<String, Long> map = cache.get(CACHE_NAMESPACE, PROCESSID);    if (map == null) {    return null;    }return map.get("isend").intValue();    }        /**     * 获取文件传输的百分比     * @param processid 文件上传或下载方法传入的唯一ID     * @param 格式  比如   :  #.## 表示精确到小数点后2位   (为空默认为小数点后2位)     * @return  比如:  12.28%     * @author wangyijie     * @data 2017年12月11日     */    public String getPercentage(String processid, String formate) {    Long max = getFileSize(processid);    Long count = getCount(processid);        double d = ((double)count * 100)/(double)max;    DecimalFormat df = null;    if (StringUtils.isBlank(formate)) {    df = new DecimalFormat("#.##");    } else {    df = new DecimalFormat(formate);    }        return df.format(d) + "%";    }

八、关闭方法

Session获取后,记得关掉它

/**     * 关闭session通道     *      * @author wangyijie     * @data 2017年12月8日     */    public void close(){    session.disconnect();        logger.warn("Session通道已关闭");    }

没看懂的可以留言问我~~

所学尚浅,见笑~,但有所知,言无不尽,见谅!


阅读全文
1 0
原创粉丝点击