什么是Xterm.js

Xterm.js 是一个用 TypeScript 编写的前端组件,它允许应用程序在浏览器中将功能齐全的终端带给用户。 它被 VS Code、Hyper 和 Theia 等流行项目使用。

springboot整合websocket实现服务端

1、引入pom依赖

此处主要引入websocket依赖和其他辅助工具

  1. <!--websocket依赖-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-websocket</artifactId>
  5. </dependency>
  6. <!--ssh2依赖-->
  7. <dependency>
  8. <groupId>ch.ethz.ganymed</groupId>
  9. <artifactId>ganymed-ssh2</artifactId>
  10. <version>262</version>
  11. </dependency>
  12. <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
  13. <dependency>
  14. <groupId>com.alibaba</groupId>
  15. <artifactId>fastjson</artifactId>
  16. <version>1.2.79</version>
  17. </dependency>
  18. <!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
  19. <dependency>
  20. <groupId>com.jcraft</groupId>
  21. <artifactId>jsch</artifactId>
  22. <version>0.1.55</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>cn.hutool</groupId>
  26. <artifactId>hutool-all</artifactId>
  27. <version>5.3.7</version>
  28. </dependency>

2、新建SshHandler-websocket处理类

  1. package com.qingfeng.framework.ssh;
  2. import cn.hutool.core.io.IoUtil;
  3. import cn.hutool.core.thread.ThreadUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import cn.hutool.extra.ssh.ChannelType;
  6. import cn.hutool.extra.ssh.JschUtil;
  7. import com.jcraft.jsch.ChannelShell;
  8. import com.jcraft.jsch.JSchException;
  9. import com.jcraft.jsch.Session;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.stereotype.Component;
  13. import javax.annotation.PostConstruct;
  14. import javax.websocket.*;
  15. import javax.websocket.server.ServerEndpoint;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.OutputStream;
  19. import java.util.Arrays;
  20. import java.util.concurrent.ConcurrentHashMap;
  21. import java.util.concurrent.CopyOnWriteArraySet;
  22. import java.util.concurrent.atomic.AtomicInteger;
  23. /**
  24. * @ProjectName SshHandler
  25. * @author qingfeng
  26. * @version 1.0.0
  27. * @Description ssh 处理
  28. * @createTime 2022/5/2 0002 15:26
  29. */
  30. @ServerEndpoint(value = "/ws/ssh")
  31. @Component
  32. public class SshHandler {
  33. private static final ConcurrentHashMap<String, HandlerItem> HANDLER_ITEM_CONCURRENT_HASH_MAP = new ConcurrentHashMap<>();
  34. @PostConstruct
  35. public void init() {
  36. System.out.println("websocket 加载");
  37. }
  38. private static Logger log = LoggerFactory.getLogger(SshHandler.class);
  39. private static final AtomicInteger OnlineCount = new AtomicInteger(0);
  40. // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
  41. private static CopyOnWriteArraySet<javax.websocket.Session> SessionSet = new CopyOnWriteArraySet<javax.websocket.Session>();
  42. /**
  43. * 连接建立成功调用的方法
  44. */
  45. @OnOpen
  46. public void onOpen(javax.websocket.Session session) throws Exception {
  47. SessionSet.add(session);
  48. SshModel sshItem = new SshModel();
  49. sshItem.setHost("127.0.0.1");
  50. sshItem.setPort(22);
  51. sshItem.setUser("root");
  52. sshItem.setPassword("root");
  53. int cnt = OnlineCount.incrementAndGet(); // 在线数加1
  54. log.info("有连接加入,当前连接数为:{},sessionId={}", cnt,session.getId());
  55. SendMessage(session, "连接成功,sessionId="+session.getId());
  56. HandlerItem handlerItem = new HandlerItem(session, sshItem);
  57. handlerItem.startRead();
  58. HANDLER_ITEM_CONCURRENT_HASH_MAP.put(session.getId(), handlerItem);
  59. }
  60. /**
  61. * 连接关闭调用的方法
  62. */
  63. @OnClose
  64. public void onClose(javax.websocket.Session session) {
  65. SessionSet.remove(session);
  66. int cnt = OnlineCount.decrementAndGet();
  67. log.info("有连接关闭,当前连接数为:{}", cnt);
  68. }
  69. /**
  70. * 收到客户端消息后调用的方法
  71. * @param message
  72. * 客户端发送过来的消息
  73. */
  74. @OnMessage
  75. public void onMessage(String message, javax.websocket.Session session) throws Exception {
  76. log.info("来自客户端的消息:{}",message);
  77. // SendMessage(session, "收到消息,消息内容:"+message);
  78. HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
  79. this.sendCommand(handlerItem, message);
  80. }
  81. /**
  82. * 出现错误
  83. * @param session
  84. * @param error
  85. */
  86. @OnError
  87. public void onError(javax.websocket.Session session, Throwable error) {
  88. log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
  89. error.printStackTrace();
  90. }
  91. private void sendCommand(HandlerItem handlerItem, String data) throws Exception {
  92. if (handlerItem.checkInput(data)) {
  93. handlerItem.outputStream.write(data.getBytes());
  94. } else {
  95. handlerItem.outputStream.write("没有执行相关命令权限".getBytes());
  96. handlerItem.outputStream.flush();
  97. handlerItem.outputStream.write(new byte[]{3});
  98. }
  99. handlerItem.outputStream.flush();
  100. }
  101. /**
  102. * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
  103. * @param session
  104. * @param message
  105. */
  106. public static void SendMessage(javax.websocket.Session session, String message) {
  107. try {
  108. // session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
  109. session.getBasicRemote().sendText(message);
  110. session.getBasicRemote().sendText("anxingtao>$");
  111. } catch (IOException e) {
  112. log.error("发送消息出错:{}", e.getMessage());
  113. e.printStackTrace();
  114. }
  115. }
  116. private class HandlerItem implements Runnable {
  117. private final javax.websocket.Session session;
  118. private final InputStream inputStream;
  119. private final OutputStream outputStream;
  120. private final Session openSession;
  121. private final ChannelShell channel;
  122. private final SshModel sshItem;
  123. private final StringBuilder nowLineInput = new StringBuilder();
  124. HandlerItem(javax.websocket.Session session, SshModel sshItem) throws IOException {
  125. this.session = session;
  126. this.sshItem = sshItem;
  127. this.openSession = JschUtil.openSession(sshItem.getHost(), sshItem.getPort(), sshItem.getUser(), sshItem.getPassword());
  128. this.channel = (ChannelShell) JschUtil.createChannel(openSession, ChannelType.SHELL);
  129. this.inputStream = channel.getInputStream();
  130. this.outputStream = channel.getOutputStream();
  131. }
  132. void startRead() throws JSchException {
  133. this.channel.connect();
  134. ThreadUtil.execute(this);
  135. }
  136. /**
  137. * 添加到命令队列
  138. *
  139. * @param msg 输入
  140. * @return 当前待确认待所有命令
  141. */
  142. private String append(String msg) {
  143. char[] x = msg.toCharArray();
  144. if (x.length == 1 && x[0] == 127) {
  145. // 退格键
  146. int length = nowLineInput.length();
  147. if (length > 0) {
  148. nowLineInput.delete(length - 1, length);
  149. }
  150. } else {
  151. nowLineInput.append(msg);
  152. }
  153. return nowLineInput.toString();
  154. }
  155. public boolean checkInput(String msg) {
  156. String allCommand = this.append(msg);
  157. boolean refuse;
  158. if (StrUtil.equalsAny(msg, StrUtil.CR, StrUtil.TAB)) {
  159. String join = nowLineInput.toString();
  160. if (StrUtil.equals(msg, StrUtil.CR)) {
  161. nowLineInput.setLength(0);
  162. }
  163. refuse = SshModel.checkInputItem(sshItem, join);
  164. } else {
  165. // 复制输出
  166. refuse = SshModel.checkInputItem(sshItem, msg);
  167. }
  168. return refuse;
  169. }
  170. @Override
  171. public void run() {
  172. try {
  173. byte[] buffer = new byte[1024];
  174. int i;
  175. //如果没有数据来,线程会一直阻塞在这个地方等待数据。
  176. while ((i = inputStream.read(buffer)) != -1) {
  177. sendBinary(session, new String(Arrays.copyOfRange(buffer, 0, i), sshItem.getCharsetT()));
  178. }
  179. } catch (Exception e) {
  180. if (!this.openSession.isConnected()) {
  181. return;
  182. }
  183. SshHandler.this.destroy(this.session);
  184. }
  185. }
  186. }
  187. public void destroy(javax.websocket.Session session) {
  188. HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
  189. if (handlerItem != null) {
  190. IoUtil.close(handlerItem.inputStream);
  191. IoUtil.close(handlerItem.outputStream);
  192. JschUtil.close(handlerItem.channel);
  193. JschUtil.close(handlerItem.openSession);
  194. }
  195. IoUtil.close(session);
  196. HANDLER_ITEM_CONCURRENT_HASH_MAP.remove(session.getId());
  197. }
  198. private static void sendBinary(javax.websocket.Session session, String msg) {
  199. // if (!session.isOpen()) {
  200. // // 会话关闭不能发送消息 @author jzy 21-08-04
  201. // return;
  202. // }
  203. // synchronized (session.getId()) {
  204. // BinaryMessage byteBuffer = new BinaryMessage(msg.getBytes());
  205. try {
  206. System.out.println("#####:"+msg);
  207. session.getBasicRemote().sendText(msg);
  208. } catch (IOException e) {
  209. }
  210. // }
  211. }
  212. }

3、创建SshModel实体类

  1. package com.qingfeng.framework.ssh;
  2. import cn.hutool.core.io.FileUtil;
  3. import cn.hutool.core.util.CharsetUtil;
  4. import cn.hutool.core.util.EnumUtil;
  5. import cn.hutool.core.util.StrUtil;
  6. import com.alibaba.fastjson.JSONArray;
  7. import java.nio.charset.Charset;
  8. import java.util.Arrays;
  9. import java.util.List;
  10. /**
  11. * @ProjectName SshModel
  12. * @author Administrator
  13. * @version 1.0.0
  14. * @Description SshModel实体类
  15. * @createTime 2022/5/2 0002 15:29
  16. */
  17. public class SshModel {
  18. private String name;
  19. private String host;
  20. private Integer port;
  21. private String user;
  22. private String password;
  23. /**
  24. * 编码格式
  25. */
  26. private String charset;
  27. /**
  28. * 文件目录
  29. */
  30. private String fileDirs;
  31. /**
  32. * ssh 私钥
  33. */
  34. private String privateKey;
  35. private String connectType;
  36. /**
  37. * 不允许执行的命令
  38. */
  39. private String notAllowedCommand;
  40. /**
  41. * 允许编辑的后缀文件
  42. */
  43. private String allowEditSuffix;
  44. public String getName() {
  45. return name;
  46. }
  47. public void setName(String name) {
  48. this.name = name;
  49. }
  50. public String getNotAllowedCommand() {
  51. return notAllowedCommand;
  52. }
  53. public void setNotAllowedCommand(String notAllowedCommand) {
  54. this.notAllowedCommand = notAllowedCommand;
  55. }
  56. public ConnectType connectType() {
  57. return EnumUtil.fromString(ConnectType.class, this.connectType, ConnectType.PASS);
  58. }
  59. public String getConnectType() {
  60. return connectType;
  61. }
  62. public void setConnectType(String connectType) {
  63. this.connectType = connectType;
  64. }
  65. public String getPrivateKey() {
  66. return privateKey;
  67. }
  68. public void setPrivateKey(String privateKey) {
  69. this.privateKey = privateKey;
  70. }
  71. public String getFileDirs() {
  72. return fileDirs;
  73. }
  74. public void setFileDirs(String fileDirs) {
  75. this.fileDirs = fileDirs;
  76. }
  77. public List<String> fileDirs() {
  78. return StringUtil.jsonConvertArray(this.fileDirs, String.class);
  79. }
  80. public void fileDirs(List<String> fileDirs) {
  81. if (fileDirs != null) {
  82. for (int i = fileDirs.size() - 1; i >= 0; i--) {
  83. String s = fileDirs.get(i);
  84. fileDirs.set(i, FileUtil.normalize(s));
  85. }
  86. this.fileDirs = JSONArray.toJSONString(fileDirs);
  87. } else {
  88. this.fileDirs = null;
  89. }
  90. }
  91. public String getHost() {
  92. return host;
  93. }
  94. public void setHost(String host) {
  95. this.host = host;
  96. }
  97. public Integer getPort() {
  98. return port;
  99. }
  100. public void setPort(Integer port) {
  101. this.port = port;
  102. }
  103. public String getUser() {
  104. return user;
  105. }
  106. public void setUser(String user) {
  107. this.user = user;
  108. }
  109. public String getPassword() {
  110. return password;
  111. }
  112. public void setPassword(String password) {
  113. this.password = password;
  114. }
  115. public String getCharset() {
  116. return charset;
  117. }
  118. public void setCharset(String charset) {
  119. this.charset = charset;
  120. }
  121. public Charset getCharsetT() {
  122. Charset charset;
  123. try {
  124. charset = Charset.forName(this.getCharset());
  125. } catch (Exception e) {
  126. charset = CharsetUtil.CHARSET_UTF_8;
  127. }
  128. return charset;
  129. }
  130. public List<String> allowEditSuffix() {
  131. return StringUtil.jsonConvertArray(this.allowEditSuffix, String.class);
  132. }
  133. public void allowEditSuffix(List<String> allowEditSuffix) {
  134. if (allowEditSuffix == null) {
  135. this.allowEditSuffix = null;
  136. } else {
  137. this.allowEditSuffix = JSONArray.toJSONString(allowEditSuffix);
  138. }
  139. }
  140. public String getAllowEditSuffix() {
  141. return allowEditSuffix;
  142. }
  143. public void setAllowEditSuffix(String allowEditSuffix) {
  144. this.allowEditSuffix = allowEditSuffix;
  145. }
  146. /**
  147. * 检查是否包含禁止命令
  148. *
  149. * @param sshItem 实体
  150. * @param inputItem 输入的命令
  151. * @return false 存在禁止输入的命令
  152. */
  153. public static boolean checkInputItem(SshModel sshItem, String inputItem) {
  154. // 检查禁止执行的命令
  155. String notAllowedCommand = StrUtil.emptyToDefault(sshItem.getNotAllowedCommand(), StrUtil.EMPTY).toLowerCase();
  156. if (StrUtil.isEmpty(notAllowedCommand)) {
  157. return true;
  158. }
  159. List<String> split = Arrays.asList(StrUtil.split(notAllowedCommand, StrUtil.COMMA));
  160. inputItem = inputItem.toLowerCase();
  161. List<String> commands = Arrays.asList(StrUtil.split(inputItem, StrUtil.CR));
  162. commands.addAll(Arrays.asList(StrUtil.split(inputItem, "&")));
  163. for (String s : split) {
  164. //
  165. boolean anyMatch = commands.stream().anyMatch(item -> StrUtil.startWithAny(item, s + StrUtil.SPACE, ("&" + s + StrUtil.SPACE), StrUtil.SPACE + s + StrUtil.SPACE));
  166. if (anyMatch) {
  167. return false;
  168. }
  169. //
  170. anyMatch = commands.stream().anyMatch(item -> StrUtil.equals(item, s));
  171. if (anyMatch) {
  172. return false;
  173. }
  174. }
  175. return true;
  176. }
  177. public enum ConnectType {
  178. /**
  179. * 账号密码
  180. */
  181. PASS,
  182. /**
  183. * 密钥
  184. */
  185. PUBKEY
  186. }
  187. }

4、新建StringUtil工具类

  1. package com.qingfeng.framework.ssh;
  2. import cn.hutool.core.date.DateField;
  3. import cn.hutool.core.date.DateTime;
  4. import cn.hutool.core.date.DateUtil;
  5. import cn.hutool.core.io.FileUtil;
  6. import cn.hutool.core.lang.Validator;
  7. import cn.hutool.core.util.StrUtil;
  8. import cn.hutool.system.SystemUtil;
  9. import com.alibaba.fastjson.JSON;
  10. import java.io.File;
  11. import java.util.List;
  12. /**
  13. * @ProjectName StringUtil
  14. * @author qingfeng
  15. * @version 1.0.0
  16. * @Description 方法运行参数工具
  17. * @createTime 2022/5/2 0002 15:29
  18. */
  19. public class StringUtil {
  20. /**
  21. * 支持的压缩包格式
  22. */
  23. public static final String[] PACKAGE_EXT = new String[]{"tar.bz2", "tar.gz", "tar", "bz2", "zip", "gz"};
  24. /**
  25. * 获取启动参数
  26. * @param args 所有参数
  27. * @param name 参数名
  28. * @return 值
  29. */
  30. public static String getArgsValue(String[] args, String name) {
  31. if (args == null) {
  32. return null;
  33. }
  34. for (String item : args) {
  35. item = StrUtil.trim(item);
  36. if (item.startsWith("--" + name + "=")) {
  37. return item.substring(name.length() + 3);
  38. }
  39. }
  40. return null;
  41. }
  42. /**
  43. * id输入规则
  44. *
  45. * @param value 值
  46. * @param min 最短
  47. * @param max 最长
  48. * @return true
  49. */
  50. public static boolean isGeneral(CharSequence value, int min, int max) {
  51. String reg = "^[a-zA-Z0-9_-]{" + min + StrUtil.COMMA + max + "}$";
  52. return Validator.isMatchRegex(reg, value);
  53. }
  54. /**
  55. * 删除文件开始的路径
  56. *
  57. * @param file 要删除的文件
  58. * @param startPath 开始的路径
  59. * @param inName 是否返回文件名
  60. * @return /test/a.txt /test/ a.txt
  61. */
  62. public static String delStartPath(File file, String startPath, boolean inName) {
  63. String newWhitePath;
  64. if (inName) {
  65. newWhitePath = FileUtil.getAbsolutePath(file.getAbsolutePath());
  66. } else {
  67. newWhitePath = FileUtil.getAbsolutePath(file.getParentFile());
  68. }
  69. String itemAbsPath = FileUtil.getAbsolutePath(new File(startPath));
  70. itemAbsPath = FileUtil.normalize(itemAbsPath);
  71. newWhitePath = FileUtil.normalize(newWhitePath);
  72. String path = StrUtil.removePrefix(newWhitePath, itemAbsPath);
  73. //newWhitePath.substring(newWhitePath.indexOf(itemAbsPath) + itemAbsPath.length());
  74. path = FileUtil.normalize(path);
  75. if (path.startsWith(StrUtil.SLASH)) {
  76. path = path.substring(1);
  77. }
  78. return path;
  79. }
  80. /**
  81. * 获取jdk 中的tools jar文件路径
  82. *
  83. * @return file
  84. */
  85. public static File getToolsJar() {
  86. File file = new File(SystemUtil.getJavaRuntimeInfo().getHomeDir());
  87. return new File(file.getParentFile(), "lib/tools.jar");
  88. }
  89. /**
  90. * 指定时间的下一个刻度
  91. *
  92. * @return String
  93. */
  94. public static String getNextScaleTime(String time, Long millis) {
  95. DateTime dateTime = DateUtil.parse(time);
  96. if (millis == null) {
  97. millis = 30 * 1000L;
  98. }
  99. DateTime newTime = dateTime.offsetNew(DateField.SECOND, (int) (millis / 1000));
  100. return DateUtil.formatTime(newTime);
  101. }
  102. // /**
  103. // * 删除 yml 文件内容注释
  104. // *
  105. // * @param content 配置内容
  106. // * @return 移除后的内容
  107. // */
  108. // public static String deleteComment(String content) {
  109. // List<String> split = StrUtil.split(content, StrUtil.LF);
  110. // split = split.stream().filter(s -> {
  111. // if (StrUtil.isEmpty(s)) {
  112. // return false;
  113. // }
  114. // s = StrUtil.trim(s);
  115. // return !StrUtil.startWith(s, "#");
  116. // }).collect(Collectors.toList());
  117. // return CollUtil.join(split, StrUtil.LF);
  118. // }
  119. /**
  120. * json 字符串转 bean,兼容普通json和字符串包裹情况
  121. *
  122. * @param jsonStr json 字符串
  123. * @param cls 要转为bean的类
  124. * @param <T> 泛型
  125. * @return data
  126. */
  127. public static <T> T jsonConvert(String jsonStr, Class<T> cls) {
  128. if (StrUtil.isEmpty(jsonStr)) {
  129. return null;
  130. }
  131. try {
  132. return JSON.parseObject(jsonStr, cls);
  133. } catch (Exception e) {
  134. return JSON.parseObject(JSON.parse(jsonStr).toString(), cls);
  135. }
  136. }
  137. /**
  138. * json 字符串转 bean,兼容普通json和字符串包裹情况
  139. *
  140. * @param jsonStr json 字符串
  141. * @param cls 要转为bean的类
  142. * @param <T> 泛型
  143. * @return data
  144. */
  145. public static <T> List<T> jsonConvertArray(String jsonStr, Class<T> cls) {
  146. try {
  147. if (StrUtil.isEmpty(jsonStr)) {
  148. return null;
  149. }
  150. return JSON.parseArray(jsonStr, cls);
  151. } catch (Exception e) {
  152. Object parse = JSON.parse(jsonStr);
  153. return JSON.parseArray(parse.toString(), cls);
  154. }
  155. }
  156. }

5、创建WebSocketConfig

给spring容器注入这个ServerEndpointExporter对象

  1. package com.qingfeng.framework.configure;
  2. import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
  3. import org.springframework.boot.web.servlet.ServletContextInitializer;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.ComponentScan;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  8. import org.springframework.web.util.WebAppRootListener;
  9. import javax.servlet.ServletContext;
  10. import javax.servlet.ServletException;
  11. /**
  12. * @author Administrator
  13. * @version 1.0.0
  14. * @ProjectName com.qingfeng
  15. * @Description WebSocketConfig
  16. * @createTime 2021年08月10日 16:51:00
  17. */
  18. @Configuration
  19. @ComponentScan
  20. @EnableAutoConfiguration
  21. public class WebSocketConfig implements ServletContextInitializer {
  22. /**
  23. * 给spring容器注入这个ServerEndpointExporter对象
  24. * 相当于xml:
  25. * <beans>
  26. * <bean id="serverEndpointExporter" class="org.springframework.web.socket.server.standard.ServerEndpointExporter"/>
  27. * </beans>
  28. * <p>
  29. * 检测所有带有@serverEndpoint注解的bean并注册他们。
  30. *
  31. * @return
  32. */
  33. @Bean
  34. public ServerEndpointExporter serverEndpointExporter() {
  35. System.out.println("我被注入了");
  36. return new ServerEndpointExporter();
  37. }
  38. @Override
  39. public void onStartup(ServletContext servletContext) throws ServletException {
  40. servletContext.addListener(WebAppRootListener.class);
  41. servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","52428800");
  42. servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize","52428800");
  43. }
  44. }

6、修改shiro拦截控制

如果是其他的拦截器,也需要设置请求过滤。

  1. filterChainDefinitionMap.put("/ws/**", "anon");

至此后端整合完毕。

Html端整合Xterm.js实现客户端

1、首先下载Xterm.js

下载xtermjs和xterm-addonjsimage.png

2、创建html页面index

  1. <!DOCTYPE html>
  2. <html lang="zh" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <th:block th:include="/web/system/admin/include :: top"></th:block>
  5. <link rel="stylesheet" th:href="@{/static/plugins/xterm/css/xterm.css}">
  6. <script type="text/javascript" th:src="@{/static/plugins/xterm/lib/xterm.js}"></script>
  7. <script type="text/javascript" th:src="@{/static/plugins/xterm-addon/lib/xterm-addon-attach.js}"></script>
  8. </head>
  9. <body>
  10. <!--<button th:onclick="closeA()">关闭</button>-->
  11. <div id="xterm"></div>
  12. <script>
  13. var socket = new WebSocket("ws://127.0.0.1:8989/ws/ssh1");
  14. //连接打开事件
  15. socket.onopen = function() {
  16. console.log("Socket 已打开");
  17. };
  18. //收到消息事件
  19. socket.onmessage = function(msg) {
  20. console.log(msg);
  21. term.write(msg.data);//把接收的数据写到这个插件的屏幕上
  22. if(msg.data.indexOf('连接成功,sessionId=')!=-1){
  23. var sessionId = msg.data.slice(15);
  24. console.log(sessionId)
  25. }else{
  26. console.log("常规数据:"+msg.data);
  27. }
  28. };
  29. //连接关闭事件
  30. socket.onclose = function() {
  31. console.log("Socket已关闭");
  32. };
  33. //发生了错误事件
  34. socket.onerror = function() {
  35. alert("Socket发生了错误");
  36. }
  37. var commandKey = [];
  38. var term = new Terminal({
  39. rendererType: "canvas", //渲染类型
  40. rows: parseInt(24), //行数
  41. cols: parseInt(100), // 不指定行数,自动回车后光标从下一行开始
  42. convertEol: true, //启用时,光标将设置为下一行的开头
  43. scrollback: 10, //终端中的回滚量
  44. disableStdin: false, //是否应禁用输入
  45. cursorStyle: "underline", //光标样式
  46. cursorBlink: true, //光标闪烁
  47. theme: {
  48. foreground: "yellow", //字体
  49. background: "#060101", //背景色
  50. cursor: "help" //设置光标
  51. }
  52. });
  53. // const attachAddon = new AttachAddon(socket);
  54. // term.loadAddon(attachAddon);
  55. term.open(document.getElementById("xterm"));
  56. // 支持输入与粘贴方法
  57. term.onData(function(key) {
  58. // console.log("|"+key+"|");
  59. // commandKey.push(key);
  60. // console.log(commandKey);
  61. // term.write(key);
  62. socket.send(key); //转换为字符串
  63. });
  64. term.onLineFeed(function(){
  65. console.log("执行换行"+JSON.stringify(commandKey))
  66. });
  67. term.onTitleChange(function(key){
  68. console.log("onTitleChange:"+key);
  69. });
  70. function closeA(){
  71. socket.close();
  72. }
  73. function runFakeTerminal() {
  74. if (term._initialized) {
  75. return;
  76. }
  77. term._initialized = true;
  78. term.prompt = () => {
  79. term.write('\r\n~$ ');
  80. };
  81. term.writeln('Welcome to xterm.js');
  82. prompt(term);
  83. term.onKey(e => {
  84. const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
  85. if (e.domEvent.keyCode === 13) {
  86. prompt(term);
  87. } else if (e.domEvent.keyCode === 8) {
  88. // Do not delete the prompt
  89. if (term._core.buffer.x > 2) {
  90. // term.write('\b \b')
  91. }
  92. } else if (printable) {
  93. // term.write(e.key);
  94. }
  95. console.log(commandKey);
  96. console.log("key::"+e.domEvent.keyCode);
  97. });
  98. }
  99. function prompt(term) {
  100. // term.write('\r\n~$ ');
  101. // term.focus();
  102. }
  103. runFakeTerminal();
  104. </script>
  105. <th:block th:include="/web/system/admin/include :: bottom" />
  106. </body>
  107. </html>

websocket相关事件

  1. var socket = new WebSocket("ws://127.0.0.1:8989/ws/ssh");
  2. //连接打开事件
  3. socket.onopen = function() {
  4. console.log("Socket 已打开");
  5. };
  6. //收到消息事件
  7. socket.onmessage = function(msg) {
  8. console.log(msg);
  9. term.write(msg.data);//把接收的数据写到这个插件的屏幕上
  10. if(msg.data.indexOf('连接成功,sessionId=')!=-1){
  11. var sessionId = msg.data.slice(15);
  12. console.log(sessionId)
  13. }else{
  14. console.log("常规数据:"+msg.data);
  15. }
  16. };
  17. //连接关闭事件
  18. socket.onclose = function() {
  19. console.log("Socket已关闭");
  20. };
  21. //发生了错误事件
  22. socket.onerror = function() {
  23. alert("Socket发生了错误");
  24. }

核心方法

  1. //发送信息到服务
  2. socket.send(key); //转换为字符串
  3. //收到消息事件
  4. socket.onmessage = function(msg) {
  5. console.log(msg);
  6. term.write(msg.data);//把接收的数据写到这个插件的屏幕上
  7. if(msg.data.indexOf('连接成功,sessionId=')!=-1){
  8. var sessionId = msg.data.slice(15);
  9. console.log(sessionId)
  10. }else{
  11. console.log("常规数据:"+msg.data);
  12. }
  13. };

term相关事件

  1. var commandKey = [];
  2. var term = new Terminal({
  3. rendererType: "canvas", //渲染类型
  4. rows: parseInt(24), //行数
  5. cols: parseInt(100), // 不指定行数,自动回车后光标从下一行开始
  6. convertEol: true, //启用时,光标将设置为下一行的开头
  7. scrollback: 10, //终端中的回滚量
  8. disableStdin: false, //是否应禁用输入
  9. cursorStyle: "underline", //光标样式
  10. cursorBlink: true, //光标闪烁
  11. theme: {
  12. foreground: "yellow", //字体
  13. background: "#060101", //背景色
  14. cursor: "help" //设置光标
  15. }
  16. });
  17. // const attachAddon = new AttachAddon(socket);
  18. // term.loadAddon(attachAddon);
  19. term.open(document.getElementById("xterm"));
  20. // 支持输入与粘贴方法
  21. term.onData(function(key) {
  22. // console.log("|"+key+"|");
  23. // commandKey.push(key);
  24. // console.log(commandKey);
  25. // term.write(key);
  26. socket.send(key); //转换为字符串
  27. });
  28. term.onLineFeed(function(){
  29. console.log("执行换行"+JSON.stringify(commandKey))
  30. });
  31. term.onTitleChange(function(key){
  32. console.log("onTitleChange:"+key);
  33. });
  34. function closeA(){
  35. socket.close();
  36. }
  37. function runFakeTerminal() {
  38. if (term._initialized) {
  39. return;
  40. }
  41. term._initialized = true;
  42. term.prompt = () => {
  43. term.write('\r\n~$ ');
  44. };
  45. term.writeln('Welcome to xterm.js');
  46. prompt(term);
  47. term.onKey(e => {
  48. const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
  49. if (e.domEvent.keyCode === 13) {
  50. prompt(term);
  51. } else if (e.domEvent.keyCode === 8) {
  52. // Do not delete the prompt
  53. if (term._core.buffer.x > 2) {
  54. // term.write('\b \b')
  55. }
  56. } else if (printable) {
  57. // term.write(e.key);
  58. }
  59. console.log(commandKey);
  60. console.log("key::"+e.domEvent.keyCode);
  61. });
  62. }
  63. function prompt(term) {
  64. // term.write('\r\n~$ ');
  65. // term.focus();
  66. }
  67. runFakeTerminal();

3、运行效果

image.png

vue整合Xtermjs实现客户端

安装Xtermjs

安装xterm

  1. npm install --save xterm

安装xterm-addon-fit

xterm.js的插件,使终端的尺寸适合包含元素。

  1. //xterm.js的插件,使终端的尺寸适合包含元素。
  2. npm install --save xterm-addon-fit

安装xterm-addon-attach

xterm.js的附加组件,用于附加到Web Socket

  1. //xterm.js的附加组件,用于附加到Web Socket
  2. npm install --save xterm-addon-attach

创建测试view页面

终端组件说明:
功能:用于用户操作容器
原理说明:组件接收一个参数containerId,用于连接指定的容器
前端终端功能使用xterm.js实现,文档地址https://xtermjs.org/
FitAddon和AttachAddon是与xterm相关的两个插件,
FitAddon用于终端尺寸的自适应,撑满父容器;AttachAddon用于使用websocket与容器进行交互
接口文档可参照https://docs.docker.com/engine/api/v1.24/#31-containers
docker的daemon需要进行一些网络上的设置以允许请求从2375端口接入,还可能会遇到cors跨域问题,
具体设置方法可参照https://docs.docker.com/engine/reference/commandline/dockerd

  1. <template>
  2. <section>
  3. <div id="log" style="margin:10px auto;">
  4. <div class="console" id="terminal"></div>
  5. </div>
  6. </section>
  7. </template>
  8. <script>
  9. import "xterm/css/xterm.css";
  10. import { Terminal } from "xterm";
  11. import { FitAddon } from "xterm-addon-fit";
  12. import { AttachAddon } from "xterm-addon-attach";
  13. export default {
  14. name: "Xterm",
  15. props: {
  16. socketURI: {
  17. type: String,
  18. default: ""
  19. }
  20. },
  21. data () {
  22. return {
  23. term: null,
  24. socket: null,
  25. rows: 28,
  26. cols: 20,
  27. SetOut: false,
  28. isKey: false
  29. };
  30. },
  31. mounted () {
  32. this.initSocket();
  33. },
  34. beforeDestroy () {
  35. this.socket.close();
  36. // this.term.dispose();
  37. },
  38. methods: {
  39. //Xterm主题
  40. initTerm () {
  41. const term = new Terminal({
  42. rendererType: "canvas", //渲染类型
  43. rows: this.rows, //行数
  44. // cols: this.cols,// 设置之后会输入多行之后覆盖现象
  45. convertEol: true, //启用时,光标将设置为下一行的开头
  46. // scrollback: 10,//终端中的回滚量
  47. fontSize: 14, //字体大小
  48. disableStdin: false, //是否应禁用输入。
  49. cursorStyle: "block", //光标样式
  50. // cursorBlink: true, //光标闪烁
  51. scrollback: 30,
  52. tabStopWidth: 4,
  53. theme: {
  54. foreground: "yellow", //字体
  55. background: "#060101", //背景色
  56. cursor: "help" //设置光标
  57. }
  58. });
  59. const attachAddon = new AttachAddon(this.socket);
  60. const fitAddon = new FitAddon();
  61. term.loadAddon(attachAddon);
  62. term.loadAddon(fitAddon);
  63. term.open(document.getElementById("terminal"));
  64. // fitAddon.fit();
  65. term.focus();
  66. let _this = this;
  67. //限制和后端交互,只有输入回车键才显示结果
  68. term.prompt = () => {
  69. term.write("\r\n$ ");
  70. };
  71. term.prompt();
  72. function runFakeTerminal (_this) {
  73. if (term._initialized) {
  74. return;
  75. }
  76. // 初始化
  77. term._initialized = true;
  78. term.writeln();//控制台初始化报错处
  79. term.prompt();
  80. // / **
  81. // *添加事件监听器,用于按下键时的事件。事件值包含
  82. // *将在data事件以及DOM事件中发送的字符串
  83. // *触发了它。
  84. // * @返回一个IDisposable停止监听。
  85. // * /
  86. // / ** 更新:xterm 4.x(新增)
  87. // *为数据事件触发时添加事件侦听器。发生这种情况
  88. // *用户输入或粘贴到终端时的示例。事件值
  89. // *是`string`结果的结果,在典型的设置中,应该通过
  90. // *到支持pty。
  91. // * @返回一个IDisposable停止监听。
  92. // * /
  93. // 支持输入与粘贴方法
  94. term.onData(function (key) {
  95. let order = {
  96. Data: key,
  97. Op: "stdin"
  98. };
  99. _this.onSend(order);
  100. });
  101. _this.term = term;
  102. }
  103. runFakeTerminal(_this);
  104. },
  105. //webShell主题
  106. initSocket () {
  107. const WebSocketUrl = "ws://127.0.0.1:8989/ws/ssh"
  108. this.socket = new WebSocket(
  109. WebSocketUrl
  110. );
  111. this.socketOnClose(); //关闭
  112. this.socketOnOpen(); //
  113. this.socketOnError();
  114. },
  115. //webshell链接成功之后操作
  116. socketOnOpen () {
  117. this.socket.onopen = () => {
  118. // 链接成功后
  119. this.initTerm();
  120. };
  121. },
  122. //webshell关闭之后操作
  123. socketOnClose () {
  124. this.socket.onclose = () => {
  125. console.log("close socket");
  126. };
  127. },
  128. //webshell错误信息
  129. socketOnError () {
  130. this.socket.onerror = () => {
  131. console.log("socket 链接失败");
  132. };
  133. },
  134. //特殊处理
  135. onSend (data) {
  136. data = this.base.isObject(data) ? JSON.stringify(data) : data;
  137. data = this.base.isArray(data) ? data.toString() : data;
  138. data = data.replace(/\\\\/, "\\");
  139. this.shellWs.onSend(data);
  140. },
  141. //删除左右两端的空格
  142. trim (str) {
  143. return str.replace(/(^\s*)|(\s*$)/g, "");
  144. }
  145. }
  146. };
  147. </script>
  148. <!-- Add "scoped" attribute to limit CSS to this component only -->
  149. <style scoped>
  150. h1, h2 {
  151. font-weight: normal;
  152. }
  153. ul {
  154. list-style-type: none;
  155. padding: 0;
  156. }
  157. li {
  158. display: inline-block;
  159. margin: 0 10px;
  160. }
  161. a {
  162. color: #42b983;
  163. }
  164. </style>

运行测试

先知先安装:npm install
进入项目根目录下面,执行:npm run dev

  1. npm run dev

image.png
在浏览器输入:http://127.0.0.1:3000
image.png