Java实现一个web-terminal连接服务器(1)

这篇博客介绍了如何使用Java实现一个web-terminal,允许在浏览器中连接到服务器和docker容器。文章涵盖了服务端的Maven依赖配置、拦截器、处理器等关键步骤,以及前端页面的terminal组件创建。读者可以通过这个指南了解整个实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:近期有个小实验,课程作业吧,要实现一个浏览器终端连接服务器,同时也能连接docker容器,于是开始查资料,由于自己之前也没有这方面的经验,而且好多博客讲不清反而会误导自己的思路,才想起自己写一篇博客来分享一下
注:我参考了一些博客,现在链接找不到就没去刻意搜索挂上来了
**注重思路,想着直接复制运行就......,实验课作业,漏洞很多**

服务端

这里以websocket保持状态进行前后端交互,不懂的可以自行百度了解一下它和普通http协议的区别。

1、导入Maven依赖

		<!-- Web相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- jsch支持 -->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>
        <!-- WebSocket 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
            <version>2.27</version>
        </dependency>
        <dependency>
            <groupId>com.spotify</groupId>
            <artifactId>docker-client</artifactId>
            <version>8.16.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

2、配置config

配置拦截器和处理器
由于SocketHandler 处理器当中的处理逻辑交给spring容器管理,这里采用注入的方式引入,通过new引入会找不到对象报错
IP:Port/stomp/websocketJS 时前端发送请求建立 Webscoket 连接的地址(是啥为所谓,后面对应上了就行)

@Configuration
@EnableWebSocket
public class WebSshConfig implements WebSocketConfigurer {

    @Autowired
    private SocketHandler socketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(socketHandler, "/stomp/websocketJS")
                .addInterceptors(new WebSocketHandshakeInterceptor())
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
}

3、拦截器

会存在一些跨越看不懂,因为我这是写完了代码再来写的博客,还是那句话,这东西不难,注重理解才能举一反三

public class WebSocketHandshakeInterceptor extends HttpSessionHandshakeInterceptor {

    private final Logger logger = LoggerFactory.getLogger(WebSocketHandshakeInterceptor.class);

    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // websocket握手建立前调用,获取httpsession
        if(request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servlet = (ServletServerHttpRequest) request;

            // 这里从request中获取session,获取不到不创建,可以根据业务处理此段
            HttpSession httpSession = servlet.getServletRequest().getSession();
            logger.info("httpSession key:" + httpSession.getId());
            httpSession.setAttribute(Constants.USER_KEY, httpSession.getId());

            // 设置连接标志
            attributes.put(Constants.CONTAINER_EXEC, false);

            String containerId = ((ServletServerHttpRequest) request).getServletRequest().getParameter("containerId");

            if (StringUtils.isNotBlank(containerId)){
                attributes.put(Constants.CONTAINER_ID, containerId);
                attributes.put(Constants.CONTAINER_EXEC, true);
            }
        }

        return super.beforeHandshake(request,response,wsHandler,attributes);
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Exception e) {
        // websocket握手建立后调用
        logger.info("websocket连接握手成功");
    }
}

在这里插入图片描述
上图红色框中的这一大块其实就是请求过滤,辅助性的做一些校验,实际场景比这复杂多了,引导性的写一些便于理解

4、处理器

主要是处理并引导请求,根据相关过程处理一些信息,请求过程不明朗的同学,自行实践一下

@Component
public class SocketHandler extends DefaultHandshakeHandler implements WebSocketHandler {

    private final Logger logger = LoggerFactory.getLogger(SocketHandler.class);

    @Autowired
    private WebSocketService webSocketService;
    @Autowired
    private WebDockerService webDockerService;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        logger.info("初始化连接");
        if ((Boolean) session.getAttributes().get(Constants.CONTAINER_EXEC)) {
            webDockerService.createExec(session);
        } else {
            webSocketService.initConnection(session);
        }
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws IOException {

        if (message instanceof TextMessage) {
            logger.info("发送命令:{}", message.getPayload());
            if ((Boolean) session.getAttributes().get(Constants.CONTAINER_EXEC)) {
                webDockerService.recvHandle((String) message.getPayload(), session);
            } else {
                webSocketService.recvHandle((String) message.getPayload(), session);
            }

        } else if (message instanceof BinaryMessage) {

        } else if (message instanceof PongMessage) {

        } else {
            System.out.println("Unexpected WebSocket message type: " + message);
        }
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception){
        logger.error("数据传输错误");
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws IOException {
        logger.info("断开连接");
        //调用service关闭连接
        webSocketService.close(session);
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
}

举个例子

下图就是我在区分与服务器建立连接,还是于docker容器建立连接,根据实际情况灵活使用
在这里插入图片描述

5、处理类接口

public interface WebSocketService {

    /**
     * 初始化ssh连接
     * @param session
     */
    void initConnection(WebSocketSession session);

    /**
     * 处理客户段发的数据
     * @param buffer
     * @param session
     */
    void recvHandle(String buffer, WebSocketSession session);

    /**
     * 数据写回前端 for websocket
     * @param session
     * @param buffer
     * @throws IOException
     */
    void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;

    /**
     * 关闭连接
     * @param session
     */
    void close(WebSocketSession session);
}

6、处理类接口实现

@Service
public class WebSocketServiceImpl implements WebSocketService {

    private static Map<String, Object> sshMap = new ConcurrentHashMap<>();

    private final Logger logger = LoggerFactory.getLogger(WebSocketServiceImpl.class);

    private ExecutorService executorService = Executors.newCachedThreadPool();

    @Override
    public void initConnection(WebSocketSession session) {
        JSch jSch = new JSch();
        ConnectInfo connectInfo = new ConnectInfo();
        connectInfo.setJsch(jSch);
        connectInfo.setWebSocketSession(session);
        String uuid = String.valueOf(session.getAttributes().get(Constants.USER_KEY));
        //将这个ssh连接信息放入map中
        sshMap.put(uuid, connectInfo);
    }

    @Override
    public void recvHandle(String buffer, WebSocketSession session) {
        ObjectMapper objectMapper = new ObjectMapper();
        WebSSHData webSSHData;
        try {
            //转换前端发送的JSON
            webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
        } catch (IOException e) {
            logger.error("Json转换异常");
            logger.error("异常信息:{}", e.getMessage());
            return;
        }
        //获取刚才设置的随机的uuid
        String userId = String.valueOf(session.getAttributes().get(Constants.USER_KEY));
        if (Constants.OPERATE_CONNECT.equals(webSSHData.getOperate())) {
            //如果是连接请求
            //找到刚才存储的ssh连接对象
            ConnectInfo connectInfo = (ConnectInfo) sshMap.get(userId);
            //启动线程异步处理
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        //连接到终端
                        connectToSSH(connectInfo, webSSHData, session);
                    } catch (JSchException | IOException e) {
                        logger.error("ssh连接异常");
                        logger.error("异常信息:{}", e.getMessage());
                        close(session);
                    }
                }
            });
        } else if (Constants.OPERATE_COMMAND.equals(webSSHData.getOperate())) {
            //如果是发送命令的请求
            String command = webSSHData.getCommand();
            ConnectInfo connectInfo = (ConnectInfo) sshMap.get(userId);
            if (connectInfo != null) {
                try {
                    //发送命令到终端
                    transToSSH(connectInfo.getChannel(), command);
                } catch (IOException e) {
                    logger.error("ssh连接异常");
                    logger.error("异常信息:{}", e.getMessage());
                    close(session);
                }
            }
        } else {
            logger.error("不支持的操作");
            close(session);
        }
    }

    @Override
    public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
        session.sendMessage(new TextMessage(buffer));
    }

    @Override
    public void close(WebSocketSession session) {
        //获取随机生成的uuid
        String userId = String.valueOf(session.getAttributes().get(Constants.USER_KEY));
        ConnectInfo connectInfo = (ConnectInfo) sshMap.get(userId);
        if (connectInfo != null) {
            //断开连接
            if (connectInfo.getChannel() != null) {
                connectInfo.getChannel().disconnect();
            }
            //map中移除该ssh连接信息
            sshMap.remove(userId);
        }
    }

    /**
     * 使用jsch连接终端
     */
    private void connectToSSH(ConnectInfo connectInfo, WebSSHData webSSHData, WebSocketSession webSocketSession) throws JSchException, IOException {
        Session session;
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        //获取jsch的会话
        session = connectInfo.getJsch().getSession(webSSHData.getUsername(), webSSHData.getHost(), webSSHData.getPort());
        session.setConfig(config);
        //设置密码
        session.setPassword(webSSHData.getPassword());
        //连接  超时时间30s
        session.connect(30000);

        //开启shell通道
        Channel channel = session.openChannel("shell");

        //通道连接 超时时间3s
        channel.connect(3000);

        //设置channel
        connectInfo.setChannel(channel);

        //转发消息
        transToSSH(channel, "");

        //读取终端返回的信息流
        InputStream inputStream = channel.getInputStream();
        try {
            //循环读取
            byte[] buffer = new byte[1024];
            int i = 0;
            //如果没有数据来,线程会一直阻塞在这个地方等待数据。
            while ((i = inputStream.read(buffer)) != -1) {
                sendMessage(webSocketSession, Arrays.copyOfRange(buffer, 0, i));
            }

        } finally {
            //断开连接后关闭会话
            session.disconnect();
            channel.disconnect();
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

    /**
     * 将消息转发到终端
     */
    private void transToSSH(Channel channel, String command) throws IOException {
        if (channel != null) {
            OutputStream outputStream = channel.getOutputStream();
            outputStream.write(command.getBytes());
            outputStream.flush();
        }
    }
}

服务端到这里差不多完成80%,还有常量类,和实体类我就不一一放上来了,有心的同学知道这些该怎么写,
也可以去我的源码地址自行查看===>链接在文章最后

前端页面

1、terminal组件

<template>
  <div class="window" ref="contain">
    <div class="header"></div>
    <div id="xterm" class="xterm" ref="xterm"/>
  </div>
</template>

<script>
import 'xterm/css/xterm.css'
import {Terminal} from 'xterm'
import {FitAddon} from 'xterm-addon-fit'
import SockJS from "sockjs-client";

export default {
  name: "terminal",
  props: {
    // 终端信息
    hostInfo: {type: Object},
  },
  watch: {
    'hostInfo.width': {
      handler(value) {
        this.$refs.contain.style.width = null
        this.$refs.contain.style.width = value
        this.fitAddon.fit()
      },
    },
    'hostInfo.height': {
      handler(value) {
        this.$refs.contain.style.height = null
        this.$refs.contain.style.height = value
        this.fitAddon.fit()
      },
    },
  },
  mounted() {
    this.$refs.contain.style.width = '200px'
    this.$refs.contain.style.height = '700px'
    this.initTerm();
  },
  beforeDestroy() {
    this.socket.close();
    this.term.dispose();
  },
  data() {
    return {
      term: null,
      socket: null,
      fitAddon: null,
    }
  },
  methods: {
    //初始化Xterm
    initTerm() {
      const term = new Terminal({
        cursorBlink: true, // 光标闪烁
        cursorStyle: "block", // 光标样式 null | 'block' | 'underline' | 'bar'
        scrollback: 800, //回滚
        tabStopWidth: 8, //制表宽度
        screenKeys: true,
        fontFamily: "Consolas",
        fontSize: 22,
        fontWeightBold: "bold",
        theme: {
          foreground: "#24cc3d"
        },

      });
      this.fitAddon = new FitAddon();
      this.fitAddon.activate(term)
      term.loadAddon(this.fitAddon);
      term.open(document.getElementById('xterm'));
      this.fitAddon.fit();
      term.focus();
      this.term = term;
      this.initSocket();
    },
    //初始化websocket
    initSocket() {
      let url = 'http://localhost:8080/stomp/websocketJS';
      if (window.WebSocket) {
        // 创建WebSocket对象
        this.socket = new SockJS(url)
      } else {
        this.term.write('Error: WebSocket Not Supported\r\n');//否则报错
        return;
      }
      this.socketOnOpen();
      this.socketOnMessage();
      this.socketOnClose();
      this.socketOnError();
    },
    socketOnOpen() {
      this.socket.onopen = () => {

        this.socket.send(JSON.stringify({
          operate: 'connect',
          host: this.hostInfo.host,//IP
          port: this.hostInfo.port,//端口号
          username: this.hostInfo.username,//用户名
          password: this.hostInfo.password,//密码
        }));
        this.term.write('Connecting...\r\n');
        // 监听键盘输入
        this.term.onData((data) => {
          //发送指令
          this.socket.send(JSON.stringify({"operate": "command", "command": data}));
        });
      }
    },
    // 回显字符
    socketOnMessage() {
      let _this = this;
      this.socket.onmessage = (evt) => {
        let data = evt.data.toString();
        _this.term.write(data);
      }
    },
    socketOnClose() {
      this.socket.onclose = () => {
        // console.log('close socket')
      }
    },
    socketOnError() {
      let _this = this;
      this.socket.onerror = (error) => {
        //连接失败回调
        _this.term.write('Error: ' + error + '\r\n');
      }
    },
  }
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}

.header {
  background-color: #E0E0E0;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;
  padding: 20px;
}

.header::before {
  content: '';
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background-color: #fd6458;
  box-shadow: 20px 0 0 #ffbf2b, 40px 0 0 #24cc3d;
  float: left;
}

.window .xterm {
  margin: 0;
  padding: 0;
  background-color: #F5F5F5;
  border-bottom-left-radius: 6px;
  border-bottom-right-radius: 6px;
}
</style>

2、新建一个父组件创建terminal实例

// 主要是将以下参数传递给terminal实例对象
data() {
    return {
      hostInfo: {
        host: '192.168.127.132',
        port: '22',
        username: '***',
        password: '***',
        // 这两个是我后续加入其他组件补上的
        height: 700,
        width: 900,
      }
    }
  },

我的gitee项目地址

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值