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

这篇博客主要介绍如何在Java服务端实现与Docker容器的连接,包括处理类接口设计、接口实现以及Docker工具类的创建,其中详细解释了使用2375端口的原因。前端部分则是对上次组件的扩展,保持了类似的功能。

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

上一篇完成基本功能,这一片来补充连接docker容器的方法

1、服务端

需要新增一个专门的处理类

1、处理类接口

public interface WebDockerService {

    /**
     * 创建与 docker 容器建立 exec 的连接
     * @return Socket
     */
    void createExec(WebSocketSession session) throws Exception;

    /**
     * 接收消息处理
     * @param buffer 消息对象
     * @param session
     */
    void recvHandle(String buffer, WebSocketSession session) throws IOException;
}

2、处理类接口实现

@Service
public class WebDockerServiceImpl implements WebDockerService {

    private final Logger logger = LoggerFactory.getLogger(WebDockerServiceImpl.class);
    private ExecutorService executorService = Executors.newCachedThreadPool();
    private Map<String,Object> map = new HashMap<>(2);
    private final static String SOCKET = "socket";
    public static final String EXEC_ID = "exec_id";

    @Value("${docker.connect.host}")
    private String ip;
    @Value("${docker.connect.port}")
    private Integer port;

    @Override
    public void createExec(WebSocketSession session) throws Exception {
        String containerId=session.getAttributes().get(Constants.CONTAINER_ID).toString();

        String execId = DockerUtil.query(ip, docker -> {
            ExecCreation execCreation = docker.execCreate(containerId, new String[]{"/bin/bash"},
                    DockerClient.ExecCreateParam.attachStdin(), DockerClient.ExecCreateParam.attachStdout(), DockerClient.ExecCreateParam.attachStderr(),
                    DockerClient.ExecCreateParam.tty(true));
            return execCreation.id();
        });
        map.put(EXEC_ID, execId);
    }

    @Override
    public void recvHandle(String buffer, WebSocketSession session) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String execId = (String) map.get(EXEC_ID);
        WebSSHData webSSHData;
        try {
            //转换前端发送的JSON
            webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
        } catch (IOException e) {
            logger.error("Json转换异常");
            logger.error("异常信息:{}", e.getMessage());
            return;
        }

        if (Constants.OPERATE_CONNECT.equals(webSSHData.getOperate())) {
            //如果是连接请求
            //启动线程异步处理
            executorService.execute(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    try {
                        //连接到终端
                        connectExec(session, execId);
                    } catch (IOException e) {
                        logger.error("container连接异常");
                        logger.error("异常信息:{}", e.getMessage());
                        session.close();
                    }
                }
            });
        } else if (Constants.OPERATE_COMMAND.equals(webSSHData.getOperate())) {
            //如果是发送命令的请求
            Socket socket = (Socket) map.get(SOCKET);
            String command = webSSHData.getCommand();
            try {
                //发送命令到终端
                transToContainer(socket, command);
            } catch (IOException e) {
                logger.error("container连接异常");
                logger.error("异常信息:{}", e.getMessage());
                socket.close();
                session.close();
            }
        } else {
            logger.error("不支持的操作");
            session.close();
        }
    }


    /**
     * 以 /bin/bash 方式建立连接
     * @param execId 命令id
     */
    private void connectExec(WebSocketSession session, String execId) throws IOException {
        Socket socket = new Socket(ip,port);
        socket.setKeepAlive(true);
        OutputStream out = socket.getOutputStream();
        StringBuffer pw = new StringBuffer();
        pw.append("POST /exec/"+execId+"/start HTTP/1.1\r\n");
        pw.append("Host: "+ip+":"+port+" \r\n");
        pw.append("Content-Type: application/json\r\n");
        pw.append("Connection: keep-alive\r\n");
        JSONObject obj = new JSONObject();
        obj.put("Detach",false);
        obj.put("Tty",false);
        String json=obj.toJSONString();
        pw.append("Content-Length: "+json.length()+"\r\n");
        pw.append("\r\n");
        pw.append(json);
        out.write(pw.toString().getBytes(StandardCharsets.UTF_8));
        out.flush();

        map.put(SOCKET, socket);

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

    /**
     * 将消息发送给客户端
     * @param session
     * @param buffer
     * @throws IOException
     */
    private void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
        session.sendMessage(new TextMessage(buffer));
    }

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

建立与docker的连接为什么是用这个请求POST /exec/"+execId+"/start HTTP/1.1
问题的答案只能去官方文档查看

3、Docker工具类

public class DockerUtil {

    public static void execute(String ip,DockerAction dockerAction)throws Exception{
        DockerClient docker = DefaultDockerClient.builder().uri("http://".concat(ip).concat(":2375")).apiVersion("v1.41").build();
        dockerAction.action(docker);
        docker.close();
    }

    public static <T> T query(String ip,DockerQuery<T> dockerQuery)throws Exception{
        DockerClient docker = DefaultDockerClient.builder().uri("http://".concat(ip).concat(":2375")).apiVersion("v1.41").build();
        T result=dockerQuery.action(docker);
        docker.close();
        return result;
    }

    public interface DockerAction {
        void action(DockerClient docker) throws Exception;
    }

    public interface DockerQuery<T> {
        T action(DockerClient docker) throws Exception;
    }
}

在这里插入图片描述

ip是docker容器服务器的ip
为什么是2375端口?
答案还是在官方文档里面,已经规定好了

2、前端部分

与上次的组件大同小异

<template>
  <div class="father">
    <vue-drag-resize :is-resizable="false">
      <div class="window">
        <div class="header"></div>
        <div id="terminal" class="terminal"/>
      </div>
    </vue-drag-resize>
  </div>
</template>
<!--
  仅供学习参考
  以 exec -it 的方式进入容器
-->

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

export default {
  name: "docker-exec",
  components: {
    VueDragResize
  },
  mounted() {
    this.initTerm();
  },
  beforeDestroy() {
    this.socket.close();
    this.term.dispose();
  },
  data() {
    return {
      containerId: this.$route.query.containerId,
      term: null,
      socket: 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"
        },

      });
      const fitAddon = new FitAddon();
      term.loadAddon(fitAddon);
      term.open(document.getElementById('terminal'));
      fitAddon.fit();
      term.focus();
      // 窗口尺寸变化时,终端尺寸自适应
      window.onresize = function () {
        fitAddon.fit()
      }
      this.term = term;
      this.initSocket();
    },
    //初始化websocket
    initSocket() {
      let url = `http://localhost:8080/stomp/websocketJS?containerId=${this.containerId}`;
      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.term.write('Connecting...\r\n');
        this.socket.send(JSON.stringify({operate: 'connect'}));
        // 监听键盘输入
        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;
}

.window {
  width: 1000px;
  margin: 0 auto;
}

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

.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;
  margin-left: -950px;
}

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

详细情况根据需要查看吧。
我的gitee项目地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值