纯Java实现模拟ssh终端

使用 jsch + jediterm 实现ssh连接虚拟终端

首先引入需要的maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>jediterm-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.jetbrains.jediterm</groupId>
            <artifactId>jediterm-core</artifactId>
            <version>3.40</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.jediterm</groupId>
            <artifactId>jediterm-ui</artifactId>
            <version>3.40</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.jediterm</groupId>
            <artifactId>jediterm-pty</artifactId>
            <version>2.60</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.jediterm</groupId>
            <artifactId>jediterm-typeahead</artifactId>
            <version>2.60</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.pty4j</groupId>
            <artifactId>pty4j</artifactId>
            <version>0.12.25</version>
        </dependency>
        <!--org.jetbrains:annotations:24.0.1-->
        <dependency>
            <groupId>org.jetbrains</groupId>
            <artifactId>annotations</artifactId>
            <version>24.0.1</version>
        </dependency>
        <!--implementation("org.slf4j:slf4j-api:2.0.9")-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.9</version>
        </dependency>
        <!--implementation("org.slf4j:slf4j-jdk14:2.0.9")-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>2.0.9</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>

        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>

    </dependencies>

    <repositories>
        <repository>
            <id>jetbrains</id>
            <url>https://packages.jetbrains.team/maven/p/ij/intellij-dependencies</url>
        </repository>
    </repositories>
</project>

示例一:根据 JschTerminal 示例代码实现 

BasicJschTerminalExample.java
package org.example.good;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jediterm.terminal.TtyConnector;
import com.jediterm.terminal.ui.JediTermWidget;
import com.jediterm.terminal.ui.settings.DefaultSettingsProvider;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.SortedMap;

/**
 * @Author GuoHang
 * @Description 功能描述
 */
public class BasicJschTerminalExample {

    private static Session session = null;
    private static ChannelShell channel = null;
    private static OutputStream outputStream;
    private static InputStreamReader myInputStreamReader;
    private static final Charset encoding = StandardCharsets.UTF_8;

    // 与远程终端建立连接
    private static void buildConnection(String host, int port, String username, String password) {
        JSch jsch = new JSch();
        try {
            session = jsch.getSession(username, host, port);
            session.setPassword(password);
            session.setConfig("StrictHostKeyChecking", "no");
            session.setTimeout(60 * 1000 * 1000);
            session.connect();

            channel = openChannel(session);
            configureChannelShell(channel);
            channel.connect();

            InputStream inputStream = channel.getInputStream();
            myInputStreamReader = new InputStreamReader(inputStream, encoding);
            outputStream = channel.getOutputStream();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected static ChannelShell openChannel(Session session) throws JSchException {
        return (ChannelShell) session.openChannel("shell");
    }

    protected static void configureChannelShell(ChannelShell channel) {
        String lang = (String) System.getenv().get("LANG");
        channel.setEnv("LANG", lang != null ? lang : "zh_CN.UTF-8");
        channel.setPtyType("xterm");
    }

//    private void resizeImmediately() {
//        if (this.myPendingTermSize != null && this.myPendingPixelSize != null) {
//            this.setPtySize(channel, this.myPendingTermSize.width, this.myPendingTermSize.height, this.myPendingPixelSize.width, this.myPendingPixelSize.height);
//            this.myPendingTermSize = null;
//            this.myPendingPixelSize = null;
//        }
//    }

    protected void setPtySize(ChannelShell channel, int col, int row, int wp, int hp) {
        channel.setPtySize(col, row, wp, hp);
    }

    private static @NotNull JediTermWidget createTerminalWidget() {
        DefaultSettingsProvider defaultSettingsProvider = new DefaultSettingsProvider() {
            @Override
            public Font getTerminalFont() {
                // 设置字体,确保选择一个支持中文字符的字体
                String font_yahei_ui = "Microsoft YaHei UI";
                String font_yahei = "Microsoft YaHei";
                String font_nsongti = "NSimSun";
                String font_songti = "SimSun";
                return getFont(font_songti);
                // 使用默认的英文字体
//                return new Font("Consolas", Font.PLAIN, 14);
            }
        };

        JediTermWidget widget = new JediTermWidget(80, 24, defaultSettingsProvider);
        ExampleTtyConnector exampleTtyConnector = new ExampleTtyConnector();
        widget.setTtyConnector(exampleTtyConnector);
        widget.requestFocusInWindow();
        widget.start();
        return widget;
    }

    private static void createAndShowGUI() {
        JFrame frame = new JFrame("Basic Terminal Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(createTerminalWidget());
        frame.pack();
        frame.setVisible(true);
    }

    // 设置字符编码 默认为UTF-8
    private static void setCharset() {
        // 支持的编码
        SortedMap<String, Charset> charsets = Charset.availableCharsets();
        Set<String> names = charsets.keySet();

        for (String name : names) {
            Charset charset = charsets.get(name);
            System.out.println(charset.name());
        }
    }

    // 按键序列(解决退格/删除键失效,乱码问题)
    private static void setKeySequence() {
        // Backspace退格键 "VT220 - Delete";"ASCII - Delete";"ASCII - Backspace";

        // Delete删除键
    }

    // 字体
    public static Font getFont(String name) {
        return getFont(name, 0, 12);
    }

    public static Font getFont(String name, int style, int size) {
        Font font = null;
        if (System.getProperty("os.name").toLowerCase().contains("win")) {
            File file = null;
            boolean loadFromFile = false;
            String path;
            if (name.equals("Microsoft YaHei")) {
                path = System.getenv("WINDIR") + "\\Fonts\\\\msyh1.ttc";
                file = new File(path);
                if (file.exists()) {
                    loadFromFile = true;
                }
            } else if (name.equals("Microsoft YaHei UI")) {
                path = System.getenv("WINDIR") + "\\Fonts\\\\msyh.ttc";
                if (style == 1) {
                    path = System.getenv("WINDIR") + "\\Fonts\\\\msyhbd.ttc";
                }

                file = new File(path);
                if (file.exists()) {
                    loadFromFile = true;
                }
            }

            if (loadFromFile) {
                try {
                    font = Font.createFont(0, new FileInputStream(file)).deriveFont(style, (float) size);
                } catch (IOException | FontFormatException var7) {
                    Exception e = var7;
                    ((Exception) e).printStackTrace();
                }
            } else {
                font = new Font(name, style, size);
            }
        } else {
            font = new Font(name, style, size);
        }
        return font;
    }

    public static void main(String[] args) {
        // Create and show this application's GUI in the event-dispatching thread.
        buildConnection("192.168.168.201", 22, "jy", "root");

        SwingUtilities.invokeLater(BasicJschTerminalExample::createAndShowGUI);
    }

    private static class ExampleTtyConnector implements TtyConnector {

        public ExampleTtyConnector() {
        }

        @Override
        public void close() {
        }

        @Override
        public String getName() {
            return null;
        }

        @Override
        public int read(char[] buf, int offset, int length) throws IOException {
            return myInputStreamReader.read(buf, offset, length);
        }

        @Override
        public void write(byte[] bytes) throws IOException {
            if (outputStream != null) {
                outputStream.write(bytes);
                outputStream.flush();
            }
        }

        @Override
        public boolean isConnected() {
            return true;
        }

        @Override
        public void write(String string) throws IOException {
            this.write(string.getBytes(encoding));
        }

        @Override
        public int waitFor() throws InterruptedException {
            while (channel != null && channel.getExitStatus() < 0 && channel.isConnected()) {
                Thread.sleep(100L);
            }
            return 0;
        }

        @Override
        public boolean ready() throws IOException {
            return myInputStreamReader.ready();
        }

    }

}

以上代码还存在的一些问题:

1.输入回显慢,问题可能是在 JschTerminal 框架 write 时。

2.中文乱码。

示例二: jsch + swing实现输入和回显同框

回显处理可能会出现一些问题

package org.example.jsch;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @Author GuoHang
 * @Description 功能描述
 */
public class JschDemo {
    private static Session session = null;
    private static ChannelShell channel = null;
    private static PrintWriter printWriter = null;
    private static BufferedInputStream bufferedInputStream = null;
    private static JTextArea area = null;

    private static boolean flag = true;
    private static boolean mark = true;
    private static int index = 0;
    private static int old_len = 0;

    public static void main(String[] args) {
        buildConnection(new DestHost("192.168.168.201", "jy", "root"));
        // 启动线程开始读取
        read();
        buildMain();
    }

    // 与远程终端建立连接
    private static void buildConnection(DestHost destHost) {
        JSch jsch = new JSch();
        try {
            session = jsch.getSession(destHost.getUsername(), destHost.getHost(), destHost.getPort());
            session.setPassword(destHost.getPassword());
            session.setConfig("StrictHostKeyChecking", "no");
            session.setTimeout(destHost.getTimeout());
            session.connect();

            channel = (ChannelShell) session.openChannel("shell");
            channel.setPty(true);
            channel.connect();

            bufferedInputStream = new BufferedInputStream(channel.getInputStream());
            printWriter = new PrintWriter(channel.getOutputStream(), true);
        } catch (JSchException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 刷新文本框内容
    private static void refreshJTextArea(String content) {
        area.setText("");
        area.append(content);
        area.paintImmediately(area.getBounds());// 让写入到文本框的内容立即显示
        old_len = area.getText().length();
    }

    // 实时读取远程终端执行完命令后的输入流并更新到文本框里
    private static void read() {
        new Thread(() -> {
            while (true) { // 让该线程永久运行下去,定时更新文本框内容
                StringBuilder sb = new StringBuilder();
                byte[] bytes = new byte[1024];
                try {
                    int count = 0;
                    int len = bufferedInputStream.available();// 避免阻塞
                    int now = 0;
                    // 使用mark和reset方法,从而可以多次读取同一输入流
                    bufferedInputStream.mark(0);
                    while (now < len) {
                        count = bufferedInputStream.read(bytes);
                        now += count;
                        String s = new String(bytes, 0, count);
                        // 用正则表达式过滤掉表示颜色的字符
                        String reg = "\\[\\d.*?m";
                        Pattern pattern = Pattern.compile(reg);
                        Matcher matcher = pattern.matcher(s);
                        s = matcher.replaceAll("");
                        sb.append(s);
                    }
                    if (mark && sb.length() > 0) {
                        refreshJTextArea(sb.toString());
                    }
                    bufferedInputStream.reset();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    // 构建GUI
    private static void buildMain() {
        JFrame frame = new JFrame("terminal");
        area = new JTextArea() {
            @Override
            public void append(String str) {
                super.append(str);
                this.setCaretPosition(getDocument().getLength());
            }

            @Override
            protected void processComponentKeyEvent(KeyEvent e) {
                if (e.getID() == KeyEvent.KEY_TYPED) { // 只允许输入字符
                    super.processComponentKeyEvent(e);
                } else if (e.getID() == KeyEvent.KEY_PRESSED) { // 阻止删除和修改操作
                    int keyCode = e.getKeyCode();
                    if (!(keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_DELETE)) {
                        super.processComponentKeyEvent(e);
                    }
                }
            }
        };
        area.setFont(new Font("宋体", Font.BOLD, 14));

        // 设置JTextArea为不可编辑
//        area.setEditable(false);

        // 把文本框加入到滚动面板中,内容溢出时可拉取
        JScrollPane scrollPane = new JScrollPane(area);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setBounds(15, 0, 750, 540);
        JPanel panel = new JPanel(null);
        panel.add(scrollPane);

        // 为文本框添加监听键盘事件
        area.addKeyListener(new KeyListener() {
            @Override
            public void keyTyped(KeyEvent e) {
            }

            @Override
            public void keyPressed(KeyEvent e) {
            }

            @Override
            public void keyReleased(KeyEvent e) {
                mark = false;
                if (flag) {
                    index = Math.min(area.getText().length(), old_len);
                    flag = false;
                }
                // 监听回车键
                if (e.getKeyChar() == '\n') {
                    area.paintImmediately(area.getBounds());
                    String content = area.getText();
                    // 获取输入的命令
                    content = content.substring(index);
                    // 发送给终端
                    printWriter.print(content);
                    printWriter.flush();
                    flag = true;
                    mark = true;
                }
            }
        });
        frame.add(panel);
        frame.setSize(800, 600);
        frame.setLocationRelativeTo(null);//设置居中显示
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

连接信息实体类: 

package org.example.jsch;

/**
 * @Author GuoHang
 * @Description 功能描述
 */
public class DestHost {
    private String host;
    private String username;
    private String password;
    private int port = 22;
    private int timeout = 60 * 60 * 1000;

    public DestHost(String host, String username, String password) {
        this.host = host;
        this.username = username;
        this.password = password;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
}

示例三: jsch+swing实现输入框和回显框分离(相对于示例二比较稳定)

package org.example;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Author GuoHang
 * @Description 功能描述
 */
public class JschTerminalExample {

    private static Session session = null;
    private static ChannelShell channel = null;
    private static BufferedInputStream bufferedInputStream = null;
    private static OutputStream outputStream;

    private static JTextArea outputArea;
    private static JScrollPane scrollPane;

    // 与远程终端建立连接
    private static void buildConnection(String host, int port, String username, String password) {
        JSch jsch = new JSch();
        try {
            session = jsch.getSession(username, host, port);
            session.setPassword(password);
            session.setConfig("StrictHostKeyChecking", "no");
            session.setTimeout(60 * 60 * 1000);
            session.connect();

            channel = (ChannelShell) session.openChannel("shell");
            //解决终端高亮显示时颜色乱码问题
            channel.setPtyType("dumb");
            channel.setPty(true);
            channel.connect();

            bufferedInputStream = new BufferedInputStream(channel.getInputStream());
            outputStream = channel.getOutputStream();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 实时读取远程终端执行完命令后的输入流并更新到文本框里
    private static void read() {
        final AtomicBoolean running = new AtomicBoolean(true); // 添加一个标志控制循环是否继续

        new Thread(() -> {
            try { // 确保资源被正确关闭
                while (running.get()) { // 让该线程根据标志运行
                    byte[] buffer = new byte[1024];
                    do {
                        int i = bufferedInputStream.read(buffer, 0, 1024); // 直接读取,无需检查 available()
                        if (i < 0) {
                            break;
                        }
                        String s = new String(buffer, 0, i);
                        outputArea.append(s);
                        scrollToBottom();
                    } while (running.get()); // 根据标志决定是否继续循环
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                running.set(false); // 确保循环退出
            }
        }).start();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            // 显示输入弹窗
            JTextField ipField = new JTextField();
            JTextField portField = new JTextField("22"); // 默认端口 22
            JTextField userField = new JTextField();
            JPasswordField passwordField = new JPasswordField();

            JPanel panel = new JPanel(new GridLayout(0, 1));
            panel.add(new JLabel("IP Address:"));
            panel.add(ipField);
            panel.add(new JLabel("Port:"));
            panel.add(portField);
            panel.add(new JLabel("Username:"));
            panel.add(userField);
            panel.add(new JLabel("Password:"));
            panel.add(passwordField);

            int result = JOptionPane.showConfirmDialog(null, panel,
                    "Enter SSH Connection Details", JOptionPane.OK_CANCEL_OPTION,
                    JOptionPane.PLAIN_MESSAGE);

            if (result == JOptionPane.OK_OPTION) {
                String ip = ipField.getText();
                int port = Integer.parseInt(portField.getText());
                String username = userField.getText();
                String password = new String(passwordField.getPassword());

                // Connect to SSH server
                connect(ip, port, username, password);
            }
        });

    }

    private static void connect(String ip, int port, String username, String password) {
        JFrame frame = new JFrame("Shell Window Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(600, 400);

        // 创建一个文本区域用于显示回显
        outputArea = new JTextArea();
        outputArea.setEditable(false);
        outputArea.setLineWrap(true);
        outputArea.setWrapStyleWord(true);
        scrollPane = new JScrollPane(outputArea);
        scrollPane.setPreferredSize(new Dimension(600, 300));

        // 创建一个 JPanel
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(scrollPane, BorderLayout.CENTER);

        // 创建隐藏的输入区域
        JTextField hiddenInput = new JTextField();
        hiddenInput.setPreferredSize(new Dimension(600, 30));
        hiddenInput.setFocusable(true);
        panel.add(hiddenInput, BorderLayout.SOUTH);

        frame.add(panel);
        frame.setVisible(true);

        // 启动后台进程或 shell
        buildConnection(ip, port, username, password);
        // 启动线程开始读取
        read();
        try {
            // 处理输入
            hiddenInput.addActionListener(e -> {
                String command = hiddenInput.getText();
                hiddenInput.setText("");
                try {
                    outputStream.write((command + "\n").getBytes());
                    outputStream.flush();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 滚动到底部
     */
    private static void scrollToBottom() {
        SwingUtilities.invokeLater(() -> {
            JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
            verticalScrollBar.setValue(verticalScrollBar.getMaximum());
        });
    }

}

示例四:参考jsch 和前端 xterm.js 通过 websocket 实现web版终端

GitHub - NoCortY/WebSSH: 纯Java实现的WebSSH

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今晚哒老虎

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值