Xterm + Jsch + Websocket + Spring Boot打造网页终端(SSH)

Xterm 官方文档
Xterm.js (xtermjs.org)

Jsch 官方文档
JSch - Java Secure Channel (jcraft.com)

对于Websocket不熟悉的可以先看一下这篇文章👉spring中利用websocket打造最简易的双向通讯-CSDN博客

👇最终实现的效果如下图所示👇

在这里插入图片描述

后端部分

这里使用spring boot框架演示

建立好框架之后,引入pom

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

新建JschUtil.java

首先连接服务器必须要的配置先写到属性当中

@Slf4j
public class JschUtil {
    // 主机ip
    static String host = "IP地址";

    // 主机端口号
    static int port = 22;

    // 主机账号
    static String username = "root";

    // 主机密码
    static String password = "123456";

    static JSch jSch = new JSch();

    // session对象
    static Session session;

    // JAVA与主机的连接通道
    static Channel channel;

    static ChannelShell shell;

    // 线程池,后续需要新开线程,去持续监听模拟连接的输出,否则会造成主线程堵塞
    private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
}

开启一个连接

此处WebSocketServer用到文章一开始提到的建立Websocket的java类,这里假设用户ID为1的连接,始终给ID为1的用户响应,作为此DEMO演示。

@Slf4j
public class JschUtil {
    // ......
    
    public static void getConnectedSession() throws JSchException, SftpException {
        // 根据主机账号、ip、端口获取一个Session对象
        session = jSch.getSession(username, host, port);
        // 存放主机密码
        session.setPassword(password);

        // 首次连接,去掉公钥确认
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        // 超时连接时间为3秒
        session.setTimeout(3000);
        session.connect();

        shell = (ChannelShell) session.openChannel("shell");
        shell.connect();
        //开新线程持续监听
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                InputStream inputStream = null;
                try {
                    inputStream = shell.getInputStream();
                    byte[] tmp = new byte[1024];
                    int i = 0;
                    // 持续监听
                    while ((i = inputStream.read(tmp, 0, 1024)) != -1) {
                        // 始终给ID为1的用户发送。
                        if (WebSocketServer.onlineSessionClientMap.containsKey("1")) {
                            WebSocketServer.onlineSessionClientMap.get("1").getBasicRemote().sendText(new String(tmp, 0, i));
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } finally {
                    close();
                }
            }
        });
    }
}

连接成功之后响应其他的Linux命令

@Slf4j
public class JschUtil {
    // ......
    public static void execCommand(String command) {
        try {
            OutputStream os = shell.getOutputStream();
            os.write(command.getBytes());
            os.flush();
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

关闭响应的方法👇

@Slf4j
public class JschUtil {
    // ......
    public static void close() {
        if (channel != null && channel.isConnected()) {
            channel.disconnect();
        }
        if (shell != null && shell.isConnected()) {
            shell.disconnect();
        }
        if (session != null && session.isConnected()) {
            session.disconnect();
        }
    }
}

此处使用的WebSocketServer.java和文章开头提到的另一篇文章使用类似,如下

@ServerEndpoint(value = "/test/{id}")
@Slf4j
@Component
public class WebSocketServer {

    //在线客户端集合
    public static final Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();

    /**
     * 连接创建成功
     *
     * @param id
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session) {
        System.out.println("开启连接" + id);
        onlineSessionClientMap.put(id, session);
        try {
            // 一开始建立的时候连接服务器
            JschUtil.getConnectedSession();
        } catch (JSchException | SftpException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 连接关闭回调
     *
     * @param id
     * @param session
     */
    @OnClose
    public void onClose(@PathParam("id") String id, Session session) {
        //从map集合中移除
        System.out.println("断开连接" + id);
        onlineSessionClientMap.remove(id);
        JschUtil.close();
    }

    /**
     * 收到消息后的回调
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        Message msg = JSONObject.parseObject(message, Message.class);
        if (msg != null && msg.getTo() != null) {
            JschUtil.execCommand(msg.getContent());
        }
    }

    /**
     * 发生错误时的回调
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
    }

}

前端部分

下载依赖

使用yarn

yarn add @xterm/xterm @xterm/addon-web-links @xterm/addon-fit

使用npm

npm i @xterm/xterm @xterm/addon-web-links @xterm/addon-fit

本篇使用vue3演示

<!-- App.vue -->
<script setup lang="ts">
import { Terminal } from '@xterm/xterm'
import { WebLinksAddon } from '@xterm/addon-web-links';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css'

let term: Terminal
const ws = new WebSocket('ws://localhost:8080/test/1');

ws.onopen = () => {
  console.log('连接成功');
};

ws.onclose = (e) => {
  console.log(e, '连接关闭');
};

ws.onmessage = (e) => {
  term.write(e.data);
};

onMounted(() => {
  term = new Terminal({
    rows: 60,
    cols: 160,
    convertEol: true,
    cursorBlink: true,
    cursorStyle: "bar", // 光标样式  'block' | 'underline' | 'bar' | null
  });
  term.open(document.getElementById('terminal') as HTMLElement);
  term.loadAddon(new WebLinksAddon());
  term.loadAddon(new FitAddon());
  term.focus() //自动聚焦
  term.write('connecting...\n') //一开始显示连接中
  // 每一次输入都实时传输到后端和服务器通信
  term.onData(data => {
    ws.send(JSON.stringify({
      from: '1',
      to: '1',
      content: data
    }))
  })
})

</script>

<template>
  <div id="terminal">
  </div>
</template>

<style scoped></style>

完成!

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要在 Vue 中使用 xterm.js 和 WebSocket 实现终端,你需要将用户输入的命令发送给后端,然后将后端返回的结果输出到 xterm.js 终端中。以下是一个简单的示例: ```html <template> <div id="terminal"></div> </template> <script> import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; export default { data() { return { socket: null, // WebSocket 实例 term: null, // Terminal 实例 }; }, mounted() { // 创建 WebSocket 实例 this.socket = new WebSocket('ws://localhost:8080'); // 创建 Terminal 实例 this.term = new Terminal(); const fitAddon = new FitAddon(); this.term.loadAddon(fitAddon); this.term.open(document.getElementById('terminal')); // 处理 WebSocket 消息 this.socket.onmessage = (event) => { this.term.write(event.data); }; // 处理输入事件 this.term.onData(data => { this.socket.send(data); }); // 调整终端大小 this.term.onResize(size => { const cols = size.cols; const rows = size.rows; this.socket.send(JSON.stringify({ type: 'resize', cols, rows })); }); // 发送 resize 消息 const cols = this.term.cols; const rows = this.term.rows; this.socket.send(JSON.stringify({ type: 'resize', cols, rows })); }, beforeDestroy() { // 关闭 WebSocket 连接 this.socket.close(); } } </script> ``` 以上代码中,我们首先在 `mounted` 钩子函数中创建了一个 WebSocket 实例和一个 Terminal 实例。然后我们为 WebSocket 实例添加了一个 `onmessage` 事件监听器,该监听器会在接收到服务器返回的消息时触发,我们在该事件处理函数中将消息输出到终端中。 接着,我们为 Terminal 实例添加了一个 `onData` 事件监听器,该监听器会在用户输入时触发,我们在该事件处理函数中向服务器发送用户输入的命令。同时,我们还为 Terminal 实例添加了一个 `onResize` 事件监听器,该监听器会在终端大小调整时触发,我们在该事件处理函数中向服务器发送终端大小变化的消息。 最后,我们在 `beforeDestroy` 钩子函数中关闭了 WebSocket 连接。 需要注意的是,以上代码中的 WebSocket 连接是通过 `ws://localhost:8080` 连接本地服务器的,你需要根据实际情况修改 WebSocket 连接地址。另外,代码中的消息格式和处理逻辑也需要根据实际情况进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你熬夜了吗?

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

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

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

打赏作者

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

抵扣说明:

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

余额充值