资讯专栏INFORMATION COLUMN

JSch-用java实现服务器远程操作

孙吉亮 / 972人阅读

摘要:主要是将一个服务集群部署到远端的服务器上,具体服务器的连接信息会通过接口传入。本来部署是人工来完成的,无非是将一些必须的文件到目标服务器上,然后远程登录,执行一些安装的操作,齐活。

介绍

前段时间接了一个比较特殊的需求,需要做一个用于部署服务的服务。主要是将一个k8s服务集群部署到远端的服务器上,具体服务器的连接信息会通过接口传入。

本来部署是人工来完成的,无非是将一些必须的文件scp到目标服务器上,然后ssh远程登录,执行一些安装的操作,齐活。安装的流程没什么问题,主要是这些步骤需要使用代码来实现,也就是需要一个支持SSH的client库来执行这些操作

最终选用了JSch(Java Secure Channel),官网介绍:

JSch is a pure Java implementation of SSH2.  
JSch allows you to connect to an sshd server and use port forwarding, X11 forwarding, file transfer, etc., and you can integrate its functionality into your own Java programs. JSch is licensed under BSD style license.
实现

为了完成部署服务的任务,需要解决几个问题:

SSH连接到远端的服务器

在服务器上执行指令

使用scp命令传输文件

编辑服务器上的文件,主要是为了修改一些配置文件

这里介绍下几个主要的工具方法

远程ssh连接

先定义一个Remote类,用于记录服务器登录信息

@Data
public class Remote {

    private String user = "root";
    private String host = "127.0.0.1";
    private int port = 22;
    private String password = "";
    private String identity = "~/.ssh/id_rsa";
    private String passphrase = "";
}

这里填充了一些默认值,平时用的时候方便一些

JSch使用Session来定义一个远程节点:

public static Session getSession(Remote remote) throws JSchException {
    JSch jSch = new JSch();
    if (Files.exists(Paths.get(remote.getIdentity()))) {
        jSch.addIdentity(remote.getIdentity(), remote.getPassphrase());
    }
    Session session = jSch.getSession(remote.getUser(), remote.getHost(),remote.getPort());
    session.setPassword(remote.getPassword());
    session.setConfig("StrictHostKeyChecking", "no");
    return session;
}

测试一下:

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        System.out.println("Host({}) connected.", remote.getHost);
    }
    session.disconnect();
}

正确的输入了服务器地址和密码后,连接成功。

这里要提一下,JSch会优先使用填入的ssh_key去尝试登录,尝试失败后才会使用password登录,这点和平时使用ssh命令的交互是一致的,好评~
远程指令

接下来就是编写一个通用的方法,用于在Session上执行命令

public static List remoteExecute(Session session, String command) throws JSchException {
    log.debug(">> {}", command);
    List resultLines = new ArrayList<>();
    ChannelExec channel = null;
    try{
        channel = (ChannelExec) session.openChannel("exec");
        channel.setCommand(command);
        InputStream input = channel.getInputStream();
        channel.connect(CONNECT_TIMEOUT);
        try {
            BufferedReader inputReader = new BufferedReader(newInputStreamReader(input));
            String inputLine = null;
            while((inputLine = inputReader.readLine()) != null) {
                log.debug("   {}", inputLine);
                resultLines.add(inputLine);
            }
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (Exception e) {
                    log.error("JSch inputStream close error:", e);
                }
            }
        }
    } catch (IOException e) {
        log.error("IOcxecption:", e);
    } finally {
        if (channel != null) {
            try {
                channel.disconnect();
            } catch (Exception e) {
                log.error("JSch channel disconnect error:", e);
            }
        }
    }
    return resultLines;
}

测试一下:

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        System.out.println("Host({}) connected.", remote.getHost());
    }
    
    remoteExecute(session, "pwd");
    remoteExecute(session, "mkdir /root/jsch-demo");
    remoteExecute(session, "ls /root/jsch-demo");
    remoteExecute(session, "touch /root/jsch-demo/test1; touch /root/jsch-demo/test2");
    remoteExecute(session, "echo "It a test file." > /root/jsch-demo/test-file");
    remoteExecute(session, "ls -all /root/jsch-demo");
    remoteExecute(session, "ls -all /root/jsch-demo | grep test");
    remoteExecute(session, "cat /root/jsch-demo/test-file");
    
    session.disconnect();
}

执行后,日志输出如下内容:

Host(192.168.124.20) connected.
>> pwd
   /root
>> mkdir /root/jsch-demo
>> ls /root/jsch-demo
>> touch /root/jsch-demo/test1; touch /root/jsch-demo/test2
>> echo "It a test file." > /root/jsch-demo/test-file
>> ls -all /root/jsch-demo
   total 12
   drwxr-xr-x 2 root root 4096 Jul 30 03:05 .
   drwx------ 6 root root 4096 Jul 30 03:05 ..
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test1
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test2
   -rw-r--r-- 1 root root   16 Jul 30 03:05 test-file
>> ls -all /root/jsch-demo | grep test
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test1
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test2
   -rw-r--r-- 1 root root   16 Jul 30 03:05 test-file
>> cat /root/jsch-demo/test-file
   It a test file.

执行结果令人满意,这些常见的命令都成功了
再次好评~

scp操作

scp操作官方给了很详细的示例scpTo+scpFrom,再次好评~
scpTo:

public static long scpTo(String source, Session session, String destination) {
    FileInputStream fileInputStream = null;
    try {
        ChannelExec channel = (ChannelExec) session.openChannel("exec");
        OutputStream out = channel.getOutputStream();
        InputStream in = channel.getInputStream();
        boolean ptimestamp = false;
        String command = "scp";
        if (ptimestamp) {
            command += " -p";
        }
        command += " -t " + destination;
        channel.setCommand(command);
        channel.connect(CONNECT_TIMEOUT);
        if (checkAck(in) != 0) {
            return -1;
        }
        File _lfile = new File(source);
        if (ptimestamp) {
            command = "T " + (_lfile.lastModified() / 1000) + " 0";
            // The access time should be sent here,
            // but it is not accessible with JavaAPI ;-<
            command += (" " + (_lfile.lastModified() / 1000) + " 0
");
            out.write(command.getBytes());
            out.flush();
            if (checkAck(in) != 0) {
                return -1;
            }
        }
        //send "C0644 filesize filename", where filename should not include "/"
        long fileSize = _lfile.length();
        command = "C0644 " + fileSize + " ";
        if (source.lastIndexOf("/") > 0) {
            command += source.substring(source.lastIndexOf("/") + 1);
        } else {
            command += source;
        }
        command += "
";
        out.write(command.getBytes());
        out.flush();
        if (checkAck(in) != 0) {
            return -1;
        }
        //send content of file
        fileInputStream = new FileInputStream(source);
        byte[] buf = new byte[1024];
        long sum = 0;
        while (true) {
            int len = fileInputStream.read(buf, 0, buf.length);
            if (len <= 0) {
                break;
            }
            out.write(buf, 0, len);
            sum += len;
        }
        //send ""
        buf[0] = 0;
        out.write(buf, 0, 1);
        out.flush();
        if (checkAck(in) != 0) {
            return -1;
        }
        return sum;
    } catch(JSchException e) {
        log.error("scp to catched jsch exception, ", e);
    } catch(IOException e) {
        log.error("scp to catched io exception, ", e);
    } catch(Exception e) {
        log.error("scp to error, ", e);
    } finally {
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (Exception e) {
                log.error("File input stream close error, ", e);
            }
        }
    }
    return -1;
}

scpFrom:

public static long scpFrom(Session session, String source, String destination) {
    FileOutputStream fileOutputStream = null;
    try {
        ChannelExec channel = (ChannelExec) session.openChannel("exec");
        channel.setCommand("scp -f " + source);
        OutputStream out = channel.getOutputStream();
        InputStream in = channel.getInputStream();
        channel.connect();
        byte[] buf = new byte[1024];
        //send ""
        buf[0] = 0;
        out.write(buf, 0, 1);
        out.flush();
        while(true) {
            if (checkAck(in) != "C") {
                break;
            }
        }
        //read "644 "
        in.read(buf, 0, 4);
        long fileSize = 0;
        while (true) {
            if (in.read(buf, 0, 1) < 0) {
                break;
            }
            if (buf[0] == " ") {
                break;
            }
            fileSize = fileSize * 10L + (long)(buf[0] - "0");
        }
        String file = null;
        for (int i = 0; ; i++) {
            in.read(buf, i, 1);
            if (buf[i] == (byte) 0x0a) {
                file = new String(buf, 0, i);
                break;
            }
        }
        // send ""
        buf[0] = 0;
        out.write(buf, 0, 1);
        out.flush();
        // read a content of lfile
        if (Files.isDirectory(Paths.get(destination))) {
            fileOutputStream = new FileOutputStream(destination + File.separator +file);
        } else {
            fileOutputStream = new FileOutputStream(destination);
        }
        long sum = 0;
        while (true) {
            int len = in.read(buf, 0 , buf.length);
            if (len <= 0) {
                break;
            }
            sum += len;
            if (len >= fileSize) {
                fileOutputStream.write(buf, 0, (int)fileSize);
                break;
            }
            fileOutputStream.write(buf, 0, len);
            fileSize -= len;
        }
        return sum;
    } catch(JSchException e) {
        log.error("scp to catched jsch exception, ", e);
    } catch(IOException e) {
        log.error("scp to catched io exception, ", e);
    } catch(Exception e) {
        log.error("scp to error, ", e);
    } finally {
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (Exception e) {
                log.error("File output stream close error, ", e);
            }
        }
    }
    return -1;
}

另外还有一个公用的方法checkAck:

private static int checkAck(InputStream in) throws IOException {
    int b=in.read();
    // b may be 0 for success,
    //          1 for error,
    //          2 for fatal error,
    //          -1
    if(b==0) return b;
    if(b==-1) return b;
    if(b==1 || b==2){
        StringBuffer sb=new StringBuffer();
        int c;
        do {
            c=in.read();
            sb.append((char)c);
        }
        while(c!="
");
        if(b==1){ // error
            log.debug(sb.toString());
        }
        if(b==2){ // fatal error
            log.debug(sb.toString());
        }
    }
    return b;
}

测试一下:
我们在项目根目录下新建一个文件test.txt

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        log.debug("Host({}) connected.", remote.getHost());
    }
    
    remoteExecute(session, "ls /root/jsch-demo/");
    scpTo("test.txt", session, "/root/jsch-demo/");
    remoteExecute(session, "ls /root/jsch-demo/");
    remoteExecute(session, "echo " append text." >> /root/jsch-demo/test.txt");
    scpFrom(session, "/root/jsch-demo/test.txt", "file-from-remote.txt");
    
    session.disconnect();
}

日志输出如下:
而且可以看到项目目录下出现了一个文件file-from-remote.txt。里面的内容比原先的test.txt多了 append text

Host(192.168.124.20) connected.
>> ls /root/jsch-demo/
   test1
   test2
   test-file
>> ls /root/jsch-demo/
   test1
   test2
   test-file
   test.txt
>> echo " append text." >> /root/jsch-demo/test.txt
远程编辑

我们平时在服务器上编辑文件一般使用vi,非常方便,但是在这里操作vi就有点复杂了
最后采用的方案是,先将源文件备份,然后scp拉到本地,编辑完后scp回原位置
remoteEdit方法:

private static boolean remoteEdit(Session session, String source, Function, List> process) {
    InputStream in = null;
    OutputStream out = null;
    try {
        String fileName = source;
        int index = source.lastIndexOf("/");
        if (index >= 0) {
            fileName = source.substring(index + 1);
        }
        //backup source
        remoteExecute(session, String.format("cp %s %s", source, source + ".bak." +System.currentTimeMillis()));
        //scp from remote
        String tmpSource = System.getProperty("java.io.tmpdir") + session.getHost() +"-" + fileName;
        scpFrom(session, source, tmpSource);
        in = new FileInputStream(tmpSource);
        //edit file according function process
        String tmpDestination = tmpSource + ".des";
        out = new FileOutputStream(tmpDestination);
        List inputLines = new ArrayList<>();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String inputLine = null;
        while ((inputLine = reader.readLine()) != null) {
            inputLines.add(inputLine);
        }
        List outputLines = process.apply(inputLines);
        for (String outputLine : outputLines) {
            out.write((outputLine + "
").getBytes());
            out.flush();
        }
        //scp to remote
        scpTo(tmpDestination, session, source);
        return true;
    } catch (Exception e) {
        log.error("remote edit error, ", e);
        return false;
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (Exception e) {
                log.error("input stream close error", e);
            }
        }
        if (out != null) {
            try {
                out.close();
            } catch (Exception e) {
                log.error("output stream close error", e);
            }
        }
    }
}

测试一下:

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        log.debug("Host({}) connected.", remote.getHost());
    }
    
    remoteExecute(session, "echo "It a test file." > /root/jsch-demo/test");
    remoteExecute(session, "cat /root/jsch-demo/test");
    remoteEdit(session, "/root/jsch-demo/test", (inputLines) -> {
        List outputLines = new ArrayList<>();
        for (String inputLine : inputLines) {
            outputLines.add(inputLine.toUpperCase());
        }
        return outputLines;
    });
    remoteExecute(session, "cat /root/jsch-demo/test");
    
    session.disconnect();
}

执行后日志输出:

Host(192.168.124.20) connected.
>> echo "It a test file." > /root/jsch-demo/test
>> cat /root/jsch-demo/test
   It a test file.
>> cp /root/jsch-demo/test /root/jsch-demo/test.bak.1564556060191
>> cat /root/jsch-demo/test
   IT A TEST FILE.

可以看到字母已经都是大写了

总结

上面这些方法,基本上覆盖了我们日常在服务器上进行操作的场景了,那么不管部署服务,还是运维服务器都不成问题了

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75783.html

相关文章

  • JSch-java实现务器远程操作

    摘要:主要是将一个服务集群部署到远端的服务器上,具体服务器的连接信息会通过接口传入。本来部署是人工来完成的,无非是将一些必须的文件到目标服务器上,然后远程登录,执行一些安装的操作,齐活。 介绍 前段时间接了一个比较特殊的需求,需要做一个用于部署服务的服务。主要是将一个k8s服务集群部署到远端的服务器上,具体服务器的连接信息会通过接口传入。 本来部署是人工来完成的,无非是将一些必须的文件s...

    scwang90 评论0 收藏0
  • 《大型网站系统与Java中间件》读书笔记 (中)

    摘要:文本已收录至我的仓库,欢迎回顾上一篇大型网站系统与中间件读书笔记一这周周末读了第四章,现在过来做做笔记,希望能帮助到大家。没错,我们通过肯定是可以完成两个系统之间的通信的问题的。 前言 只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 回顾上一篇: 《大型网站系统与Java中间件》读书笔记(一)...

    fredshare 评论0 收藏0
  • Java 远程通讯技术及原理分析

    摘要:对于与而言,则可以看做是消息传递技术的一种衍生或封装。在生产者通知消费者时,传递的往往是消息或事件,而非生产者自身。通过消息路由,我们可以配置路由规则指定消息传递的路径,以及指定具体的消费者消费对应的生产者。采用和来进行远程对象的通讯。 消息模式 归根结底,企业应用系统就是对数据的处理,而对于一个拥有多个子系统的企业应用系统而言,它的基础支撑无疑就是对消息的处理。与对象不同,消息本质上...

    rozbo 评论0 收藏0
  • 分布式服务框架之远程通讯技术及原理分析

    摘要:微软的虽然引入了事件机制,可以在队列收到消息时触发事件,通知订阅者。由微软作为主要贡献者的,则对以及做了进一层包装,并能够很好地实现这一模式。 在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java领域中有很多可实现远程通讯的技术,例如:RMI、MINA、ESB、Burlap、Hessian、SOAP、EJB和JMS等,这些名词之间到底是些什么关系呢,它们背后到底是基...

    sorra 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<