使用 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());
});
}
}