关于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通道已关闭"); }
没看懂的可以留言问我~~
所学尚浅,见笑~,但有所知,言无不尽,见谅!
- 关于JSch的使用,执行ssh命令,文件上传和下载以及连接方式
- JAVA使用JSch进行SSH连接Linux并执行命令
- JSch连接SSH远程执行命令
- JSch连接SSH远程执行命令
- 使用JSch来上传或者下载linux上的文件
- Python SSH 的远程连接并执行命令和下载文件
- JSCH 使用代理方式(HTTP或SOCKET)通过SFTP上传或下载文件
- ssh上传和下载linux文件命令
- 使用JSCH连接Linux服务器-执行linux命令
- java 通过SSH方式连接远程主机并上传和下载文件
- java 通过SSH方式连接远程主机并上传和下载文件
- JSch:纯JAVA实现SFTP文件上传和下载
- JSch:纯JAVA实现SFTP文件上传和下载
- JSch:纯JAVA实现SFTP文件上传和下载
- JSch:纯JAVA实现SFTP文件上传和下载
- JSch:纯JAVA实现SFTP文件上传和下载
- 使用jsch实现文件上传
- 使用FTP命令自动完成文件的上传和下载
- 时间的账单
- Linux 内核学习经验总结
- 面向对象:我笑,便面如春花,定能感动人,任他是谁
- 程序员秒懂,但会不会误导小朋友?
- C#下操作USB设备的方法
- 关于JSch的使用,执行ssh命令,文件上传和下载以及连接方式
- 《Effective Java》笔记
- 如何理解与快速构建python编程程环境,eclipse+pydev插件+python虚拟平台
- 趣图:新出了一个库,程序员想要跟上潮流的时候
- Java:单词倒排
- ssh_day01_01-今天内容介绍_02-svn概述和体系结构
- python并发之concurrent.futures
- ajax(1)---ajax概述
- [大数据]连载No3之Hadoop完全分布式环境搭建