什么是Xterm.js
Xterm.js 是一个用 TypeScript 编写的前端组件,它允许应用程序在浏览器中将功能齐全的终端带给用户。 它被 VS Code、Hyper 和 Theia 等流行项目使用。
springboot整合websocket实现服务端
1、引入pom依赖
此处主要引入websocket依赖和其他辅助工具
<!--websocket依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--ssh2依赖-->
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>262</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.7</version>
</dependency>
2、新建SshHandler-websocket处理类
package com.qingfeng.framework.ssh;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ssh.ChannelType;
import cn.hutool.extra.ssh.JschUtil;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ProjectName SshHandler
* @author qingfeng
* @version 1.0.0
* @Description ssh 处理
* @createTime 2022/5/2 0002 15:26
*/
@ServerEndpoint(value = "/ws/ssh")
@Component
public class SshHandler {
private static final ConcurrentHashMap<String, HandlerItem> HANDLER_ITEM_CONCURRENT_HASH_MAP = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
System.out.println("websocket 加载");
}
private static Logger log = LoggerFactory.getLogger(SshHandler.class);
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
// concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
private static CopyOnWriteArraySet<javax.websocket.Session> SessionSet = new CopyOnWriteArraySet<javax.websocket.Session>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(javax.websocket.Session session) throws Exception {
SessionSet.add(session);
SshModel sshItem = new SshModel();
sshItem.setHost("127.0.0.1");
sshItem.setPort(22);
sshItem.setUser("root");
sshItem.setPassword("root");
int cnt = OnlineCount.incrementAndGet(); // 在线数加1
log.info("有连接加入,当前连接数为:{},sessionId={}", cnt,session.getId());
SendMessage(session, "连接成功,sessionId="+session.getId());
HandlerItem handlerItem = new HandlerItem(session, sshItem);
handlerItem.startRead();
HANDLER_ITEM_CONCURRENT_HASH_MAP.put(session.getId(), handlerItem);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(javax.websocket.Session session) {
SessionSet.remove(session);
int cnt = OnlineCount.decrementAndGet();
log.info("有连接关闭,当前连接数为:{}", cnt);
}
/**
* 收到客户端消息后调用的方法
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, javax.websocket.Session session) throws Exception {
log.info("来自客户端的消息:{}",message);
// SendMessage(session, "收到消息,消息内容:"+message);
HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
this.sendCommand(handlerItem, message);
}
/**
* 出现错误
* @param session
* @param error
*/
@OnError
public void onError(javax.websocket.Session session, Throwable error) {
log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
error.printStackTrace();
}
private void sendCommand(HandlerItem handlerItem, String data) throws Exception {
if (handlerItem.checkInput(data)) {
handlerItem.outputStream.write(data.getBytes());
} else {
handlerItem.outputStream.write("没有执行相关命令权限".getBytes());
handlerItem.outputStream.flush();
handlerItem.outputStream.write(new byte[]{3});
}
handlerItem.outputStream.flush();
}
/**
* 发送消息,实践表明,每次浏览器刷新,session会发生变化。
* @param session
* @param message
*/
public static void SendMessage(javax.websocket.Session session, String message) {
try {
// session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
session.getBasicRemote().sendText(message);
session.getBasicRemote().sendText("anxingtao>$");
} catch (IOException e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
private class HandlerItem implements Runnable {
private final javax.websocket.Session session;
private final InputStream inputStream;
private final OutputStream outputStream;
private final Session openSession;
private final ChannelShell channel;
private final SshModel sshItem;
private final StringBuilder nowLineInput = new StringBuilder();
HandlerItem(javax.websocket.Session session, SshModel sshItem) throws IOException {
this.session = session;
this.sshItem = sshItem;
this.openSession = JschUtil.openSession(sshItem.getHost(), sshItem.getPort(), sshItem.getUser(), sshItem.getPassword());
this.channel = (ChannelShell) JschUtil.createChannel(openSession, ChannelType.SHELL);
this.inputStream = channel.getInputStream();
this.outputStream = channel.getOutputStream();
}
void startRead() throws JSchException {
this.channel.connect();
ThreadUtil.execute(this);
}
/**
* 添加到命令队列
*
* @param msg 输入
* @return 当前待确认待所有命令
*/
private String append(String msg) {
char[] x = msg.toCharArray();
if (x.length == 1 && x[0] == 127) {
// 退格键
int length = nowLineInput.length();
if (length > 0) {
nowLineInput.delete(length - 1, length);
}
} else {
nowLineInput.append(msg);
}
return nowLineInput.toString();
}
public boolean checkInput(String msg) {
String allCommand = this.append(msg);
boolean refuse;
if (StrUtil.equalsAny(msg, StrUtil.CR, StrUtil.TAB)) {
String join = nowLineInput.toString();
if (StrUtil.equals(msg, StrUtil.CR)) {
nowLineInput.setLength(0);
}
refuse = SshModel.checkInputItem(sshItem, join);
} else {
// 复制输出
refuse = SshModel.checkInputItem(sshItem, msg);
}
return refuse;
}
@Override
public void run() {
try {
byte[] buffer = new byte[1024];
int i;
//如果没有数据来,线程会一直阻塞在这个地方等待数据。
while ((i = inputStream.read(buffer)) != -1) {
sendBinary(session, new String(Arrays.copyOfRange(buffer, 0, i), sshItem.getCharsetT()));
}
} catch (Exception e) {
if (!this.openSession.isConnected()) {
return;
}
SshHandler.this.destroy(this.session);
}
}
}
public void destroy(javax.websocket.Session session) {
HandlerItem handlerItem = HANDLER_ITEM_CONCURRENT_HASH_MAP.get(session.getId());
if (handlerItem != null) {
IoUtil.close(handlerItem.inputStream);
IoUtil.close(handlerItem.outputStream);
JschUtil.close(handlerItem.channel);
JschUtil.close(handlerItem.openSession);
}
IoUtil.close(session);
HANDLER_ITEM_CONCURRENT_HASH_MAP.remove(session.getId());
}
private static void sendBinary(javax.websocket.Session session, String msg) {
// if (!session.isOpen()) {
// // 会话关闭不能发送消息 @author jzy 21-08-04
// return;
// }
// synchronized (session.getId()) {
// BinaryMessage byteBuffer = new BinaryMessage(msg.getBytes());
try {
System.out.println("#####:"+msg);
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
}
// }
}
}
3、创建SshModel实体类
package com.qingfeng.framework.ssh;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
/**
* @ProjectName SshModel
* @author Administrator
* @version 1.0.0
* @Description SshModel实体类
* @createTime 2022/5/2 0002 15:29
*/
public class SshModel {
private String name;
private String host;
private Integer port;
private String user;
private String password;
/**
* 编码格式
*/
private String charset;
/**
* 文件目录
*/
private String fileDirs;
/**
* ssh 私钥
*/
private String privateKey;
private String connectType;
/**
* 不允许执行的命令
*/
private String notAllowedCommand;
/**
* 允许编辑的后缀文件
*/
private String allowEditSuffix;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNotAllowedCommand() {
return notAllowedCommand;
}
public void setNotAllowedCommand(String notAllowedCommand) {
this.notAllowedCommand = notAllowedCommand;
}
public ConnectType connectType() {
return EnumUtil.fromString(ConnectType.class, this.connectType, ConnectType.PASS);
}
public String getConnectType() {
return connectType;
}
public void setConnectType(String connectType) {
this.connectType = connectType;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getFileDirs() {
return fileDirs;
}
public void setFileDirs(String fileDirs) {
this.fileDirs = fileDirs;
}
public List<String> fileDirs() {
return StringUtil.jsonConvertArray(this.fileDirs, String.class);
}
public void fileDirs(List<String> fileDirs) {
if (fileDirs != null) {
for (int i = fileDirs.size() - 1; i >= 0; i--) {
String s = fileDirs.get(i);
fileDirs.set(i, FileUtil.normalize(s));
}
this.fileDirs = JSONArray.toJSONString(fileDirs);
} else {
this.fileDirs = null;
}
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCharset() {
return charset;
}
public void setCharset(String charset) {
this.charset = charset;
}
public Charset getCharsetT() {
Charset charset;
try {
charset = Charset.forName(this.getCharset());
} catch (Exception e) {
charset = CharsetUtil.CHARSET_UTF_8;
}
return charset;
}
public List<String> allowEditSuffix() {
return StringUtil.jsonConvertArray(this.allowEditSuffix, String.class);
}
public void allowEditSuffix(List<String> allowEditSuffix) {
if (allowEditSuffix == null) {
this.allowEditSuffix = null;
} else {
this.allowEditSuffix = JSONArray.toJSONString(allowEditSuffix);
}
}
public String getAllowEditSuffix() {
return allowEditSuffix;
}
public void setAllowEditSuffix(String allowEditSuffix) {
this.allowEditSuffix = allowEditSuffix;
}
/**
* 检查是否包含禁止命令
*
* @param sshItem 实体
* @param inputItem 输入的命令
* @return false 存在禁止输入的命令
*/
public static boolean checkInputItem(SshModel sshItem, String inputItem) {
// 检查禁止执行的命令
String notAllowedCommand = StrUtil.emptyToDefault(sshItem.getNotAllowedCommand(), StrUtil.EMPTY).toLowerCase();
if (StrUtil.isEmpty(notAllowedCommand)) {
return true;
}
List<String> split = Arrays.asList(StrUtil.split(notAllowedCommand, StrUtil.COMMA));
inputItem = inputItem.toLowerCase();
List<String> commands = Arrays.asList(StrUtil.split(inputItem, StrUtil.CR));
commands.addAll(Arrays.asList(StrUtil.split(inputItem, "&")));
for (String s : split) {
//
boolean anyMatch = commands.stream().anyMatch(item -> StrUtil.startWithAny(item, s + StrUtil.SPACE, ("&" + s + StrUtil.SPACE), StrUtil.SPACE + s + StrUtil.SPACE));
if (anyMatch) {
return false;
}
//
anyMatch = commands.stream().anyMatch(item -> StrUtil.equals(item, s));
if (anyMatch) {
return false;
}
}
return true;
}
public enum ConnectType {
/**
* 账号密码
*/
PASS,
/**
* 密钥
*/
PUBKEY
}
}
4、新建StringUtil工具类
package com.qingfeng.framework.ssh;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import com.alibaba.fastjson.JSON;
import java.io.File;
import java.util.List;
/**
* @ProjectName StringUtil
* @author qingfeng
* @version 1.0.0
* @Description 方法运行参数工具
* @createTime 2022/5/2 0002 15:29
*/
public class StringUtil {
/**
* 支持的压缩包格式
*/
public static final String[] PACKAGE_EXT = new String[]{"tar.bz2", "tar.gz", "tar", "bz2", "zip", "gz"};
/**
* 获取启动参数
* @param args 所有参数
* @param name 参数名
* @return 值
*/
public static String getArgsValue(String[] args, String name) {
if (args == null) {
return null;
}
for (String item : args) {
item = StrUtil.trim(item);
if (item.startsWith("--" + name + "=")) {
return item.substring(name.length() + 3);
}
}
return null;
}
/**
* id输入规则
*
* @param value 值
* @param min 最短
* @param max 最长
* @return true
*/
public static boolean isGeneral(CharSequence value, int min, int max) {
String reg = "^[a-zA-Z0-9_-]{" + min + StrUtil.COMMA + max + "}$";
return Validator.isMatchRegex(reg, value);
}
/**
* 删除文件开始的路径
*
* @param file 要删除的文件
* @param startPath 开始的路径
* @param inName 是否返回文件名
* @return /test/a.txt /test/ a.txt
*/
public static String delStartPath(File file, String startPath, boolean inName) {
String newWhitePath;
if (inName) {
newWhitePath = FileUtil.getAbsolutePath(file.getAbsolutePath());
} else {
newWhitePath = FileUtil.getAbsolutePath(file.getParentFile());
}
String itemAbsPath = FileUtil.getAbsolutePath(new File(startPath));
itemAbsPath = FileUtil.normalize(itemAbsPath);
newWhitePath = FileUtil.normalize(newWhitePath);
String path = StrUtil.removePrefix(newWhitePath, itemAbsPath);
//newWhitePath.substring(newWhitePath.indexOf(itemAbsPath) + itemAbsPath.length());
path = FileUtil.normalize(path);
if (path.startsWith(StrUtil.SLASH)) {
path = path.substring(1);
}
return path;
}
/**
* 获取jdk 中的tools jar文件路径
*
* @return file
*/
public static File getToolsJar() {
File file = new File(SystemUtil.getJavaRuntimeInfo().getHomeDir());
return new File(file.getParentFile(), "lib/tools.jar");
}
/**
* 指定时间的下一个刻度
*
* @return String
*/
public static String getNextScaleTime(String time, Long millis) {
DateTime dateTime = DateUtil.parse(time);
if (millis == null) {
millis = 30 * 1000L;
}
DateTime newTime = dateTime.offsetNew(DateField.SECOND, (int) (millis / 1000));
return DateUtil.formatTime(newTime);
}
// /**
// * 删除 yml 文件内容注释
// *
// * @param content 配置内容
// * @return 移除后的内容
// */
// public static String deleteComment(String content) {
// List<String> split = StrUtil.split(content, StrUtil.LF);
// split = split.stream().filter(s -> {
// if (StrUtil.isEmpty(s)) {
// return false;
// }
// s = StrUtil.trim(s);
// return !StrUtil.startWith(s, "#");
// }).collect(Collectors.toList());
// return CollUtil.join(split, StrUtil.LF);
// }
/**
* json 字符串转 bean,兼容普通json和字符串包裹情况
*
* @param jsonStr json 字符串
* @param cls 要转为bean的类
* @param <T> 泛型
* @return data
*/
public static <T> T jsonConvert(String jsonStr, Class<T> cls) {
if (StrUtil.isEmpty(jsonStr)) {
return null;
}
try {
return JSON.parseObject(jsonStr, cls);
} catch (Exception e) {
return JSON.parseObject(JSON.parse(jsonStr).toString(), cls);
}
}
/**
* json 字符串转 bean,兼容普通json和字符串包裹情况
*
* @param jsonStr json 字符串
* @param cls 要转为bean的类
* @param <T> 泛型
* @return data
*/
public static <T> List<T> jsonConvertArray(String jsonStr, Class<T> cls) {
try {
if (StrUtil.isEmpty(jsonStr)) {
return null;
}
return JSON.parseArray(jsonStr, cls);
} catch (Exception e) {
Object parse = JSON.parse(jsonStr);
return JSON.parseArray(parse.toString(), cls);
}
}
}
5、创建WebSocketConfig
给spring容器注入这个ServerEndpointExporter对象
package com.qingfeng.framework.configure;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.util.WebAppRootListener;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
/**
* @author Administrator
* @version 1.0.0
* @ProjectName com.qingfeng
* @Description WebSocketConfig
* @createTime 2021年08月10日 16:51:00
*/
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class WebSocketConfig implements ServletContextInitializer {
/**
* 给spring容器注入这个ServerEndpointExporter对象
* 相当于xml:
* <beans>
* <bean id="serverEndpointExporter" class="org.springframework.web.socket.server.standard.ServerEndpointExporter"/>
* </beans>
* <p>
* 检测所有带有@serverEndpoint注解的bean并注册他们。
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
System.out.println("我被注入了");
return new ServerEndpointExporter();
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addListener(WebAppRootListener.class);
servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize","52428800");
servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize","52428800");
}
}
6、修改shiro拦截控制
如果是其他的拦截器,也需要设置请求过滤。
filterChainDefinitionMap.put("/ws/**", "anon");
至此后端整合完毕。
Html端整合Xterm.js实现客户端
1、首先下载Xterm.js
2、创建html页面index
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="/web/system/admin/include :: top"></th:block>
<link rel="stylesheet" th:href="@{/static/plugins/xterm/css/xterm.css}">
<script type="text/javascript" th:src="@{/static/plugins/xterm/lib/xterm.js}"></script>
<script type="text/javascript" th:src="@{/static/plugins/xterm-addon/lib/xterm-addon-attach.js}"></script>
</head>
<body>
<!--<button th:onclick="closeA()">关闭</button>-->
<div id="xterm"></div>
<script>
var socket = new WebSocket("ws://127.0.0.1:8989/ws/ssh1");
//连接打开事件
socket.onopen = function() {
console.log("Socket 已打开");
};
//收到消息事件
socket.onmessage = function(msg) {
console.log(msg);
term.write(msg.data);//把接收的数据写到这个插件的屏幕上
if(msg.data.indexOf('连接成功,sessionId=')!=-1){
var sessionId = msg.data.slice(15);
console.log(sessionId)
}else{
console.log("常规数据:"+msg.data);
}
};
//连接关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
var commandKey = [];
var term = new Terminal({
rendererType: "canvas", //渲染类型
rows: parseInt(24), //行数
cols: parseInt(100), // 不指定行数,自动回车后光标从下一行开始
convertEol: true, //启用时,光标将设置为下一行的开头
scrollback: 10, //终端中的回滚量
disableStdin: false, //是否应禁用输入
cursorStyle: "underline", //光标样式
cursorBlink: true, //光标闪烁
theme: {
foreground: "yellow", //字体
background: "#060101", //背景色
cursor: "help" //设置光标
}
});
// const attachAddon = new AttachAddon(socket);
// term.loadAddon(attachAddon);
term.open(document.getElementById("xterm"));
// 支持输入与粘贴方法
term.onData(function(key) {
// console.log("|"+key+"|");
// commandKey.push(key);
// console.log(commandKey);
// term.write(key);
socket.send(key); //转换为字符串
});
term.onLineFeed(function(){
console.log("执行换行"+JSON.stringify(commandKey))
});
term.onTitleChange(function(key){
console.log("onTitleChange:"+key);
});
function closeA(){
socket.close();
}
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
term.prompt = () => {
term.write('\r\n~$ ');
};
term.writeln('Welcome to xterm.js');
prompt(term);
term.onKey(e => {
const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
if (e.domEvent.keyCode === 13) {
prompt(term);
} else if (e.domEvent.keyCode === 8) {
// Do not delete the prompt
if (term._core.buffer.x > 2) {
// term.write('\b \b')
}
} else if (printable) {
// term.write(e.key);
}
console.log(commandKey);
console.log("key::"+e.domEvent.keyCode);
});
}
function prompt(term) {
// term.write('\r\n~$ ');
// term.focus();
}
runFakeTerminal();
</script>
<th:block th:include="/web/system/admin/include :: bottom" />
</body>
</html>
websocket相关事件
var socket = new WebSocket("ws://127.0.0.1:8989/ws/ssh");
//连接打开事件
socket.onopen = function() {
console.log("Socket 已打开");
};
//收到消息事件
socket.onmessage = function(msg) {
console.log(msg);
term.write(msg.data);//把接收的数据写到这个插件的屏幕上
if(msg.data.indexOf('连接成功,sessionId=')!=-1){
var sessionId = msg.data.slice(15);
console.log(sessionId)
}else{
console.log("常规数据:"+msg.data);
}
};
//连接关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
核心方法
//发送信息到服务
socket.send(key); //转换为字符串
//收到消息事件
socket.onmessage = function(msg) {
console.log(msg);
term.write(msg.data);//把接收的数据写到这个插件的屏幕上
if(msg.data.indexOf('连接成功,sessionId=')!=-1){
var sessionId = msg.data.slice(15);
console.log(sessionId)
}else{
console.log("常规数据:"+msg.data);
}
};
term相关事件
var commandKey = [];
var term = new Terminal({
rendererType: "canvas", //渲染类型
rows: parseInt(24), //行数
cols: parseInt(100), // 不指定行数,自动回车后光标从下一行开始
convertEol: true, //启用时,光标将设置为下一行的开头
scrollback: 10, //终端中的回滚量
disableStdin: false, //是否应禁用输入
cursorStyle: "underline", //光标样式
cursorBlink: true, //光标闪烁
theme: {
foreground: "yellow", //字体
background: "#060101", //背景色
cursor: "help" //设置光标
}
});
// const attachAddon = new AttachAddon(socket);
// term.loadAddon(attachAddon);
term.open(document.getElementById("xterm"));
// 支持输入与粘贴方法
term.onData(function(key) {
// console.log("|"+key+"|");
// commandKey.push(key);
// console.log(commandKey);
// term.write(key);
socket.send(key); //转换为字符串
});
term.onLineFeed(function(){
console.log("执行换行"+JSON.stringify(commandKey))
});
term.onTitleChange(function(key){
console.log("onTitleChange:"+key);
});
function closeA(){
socket.close();
}
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
term.prompt = () => {
term.write('\r\n~$ ');
};
term.writeln('Welcome to xterm.js');
prompt(term);
term.onKey(e => {
const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
if (e.domEvent.keyCode === 13) {
prompt(term);
} else if (e.domEvent.keyCode === 8) {
// Do not delete the prompt
if (term._core.buffer.x > 2) {
// term.write('\b \b')
}
} else if (printable) {
// term.write(e.key);
}
console.log(commandKey);
console.log("key::"+e.domEvent.keyCode);
});
}
function prompt(term) {
// term.write('\r\n~$ ');
// term.focus();
}
runFakeTerminal();
3、运行效果
vue整合Xtermjs实现客户端
安装Xtermjs
安装xterm
npm install --save xterm
安装xterm-addon-fit
xterm.js的插件,使终端的尺寸适合包含元素。
//xterm.js的插件,使终端的尺寸适合包含元素。
npm install --save xterm-addon-fit
安装xterm-addon-attach
xterm.js的附加组件,用于附加到Web Socket
//xterm.js的附加组件,用于附加到Web Socket
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
<template>
<section>
<div id="log" style="margin:10px auto;">
<div class="console" id="terminal"></div>
</div>
</section>
</template>
<script>
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { AttachAddon } from "xterm-addon-attach";
export default {
name: "Xterm",
props: {
socketURI: {
type: String,
default: ""
}
},
data () {
return {
term: null,
socket: null,
rows: 28,
cols: 20,
SetOut: false,
isKey: false
};
},
mounted () {
this.initSocket();
},
beforeDestroy () {
this.socket.close();
// this.term.dispose();
},
methods: {
//Xterm主题
initTerm () {
const term = new Terminal({
rendererType: "canvas", //渲染类型
rows: this.rows, //行数
// cols: this.cols,// 设置之后会输入多行之后覆盖现象
convertEol: true, //启用时,光标将设置为下一行的开头
// scrollback: 10,//终端中的回滚量
fontSize: 14, //字体大小
disableStdin: false, //是否应禁用输入。
cursorStyle: "block", //光标样式
// cursorBlink: true, //光标闪烁
scrollback: 30,
tabStopWidth: 4,
theme: {
foreground: "yellow", //字体
background: "#060101", //背景色
cursor: "help" //设置光标
}
});
const attachAddon = new AttachAddon(this.socket);
const fitAddon = new FitAddon();
term.loadAddon(attachAddon);
term.loadAddon(fitAddon);
term.open(document.getElementById("terminal"));
// fitAddon.fit();
term.focus();
let _this = this;
//限制和后端交互,只有输入回车键才显示结果
term.prompt = () => {
term.write("\r\n$ ");
};
term.prompt();
function runFakeTerminal (_this) {
if (term._initialized) {
return;
}
// 初始化
term._initialized = true;
term.writeln();//控制台初始化报错处
term.prompt();
// / **
// *添加事件监听器,用于按下键时的事件。事件值包含
// *将在data事件以及DOM事件中发送的字符串
// *触发了它。
// * @返回一个IDisposable停止监听。
// * /
// / ** 更新:xterm 4.x(新增)
// *为数据事件触发时添加事件侦听器。发生这种情况
// *用户输入或粘贴到终端时的示例。事件值
// *是`string`结果的结果,在典型的设置中,应该通过
// *到支持pty。
// * @返回一个IDisposable停止监听。
// * /
// 支持输入与粘贴方法
term.onData(function (key) {
let order = {
Data: key,
Op: "stdin"
};
_this.onSend(order);
});
_this.term = term;
}
runFakeTerminal(_this);
},
//webShell主题
initSocket () {
const WebSocketUrl = "ws://127.0.0.1:8989/ws/ssh"
this.socket = new WebSocket(
WebSocketUrl
);
this.socketOnClose(); //关闭
this.socketOnOpen(); //
this.socketOnError();
},
//webshell链接成功之后操作
socketOnOpen () {
this.socket.onopen = () => {
// 链接成功后
this.initTerm();
};
},
//webshell关闭之后操作
socketOnClose () {
this.socket.onclose = () => {
console.log("close socket");
};
},
//webshell错误信息
socketOnError () {
this.socket.onerror = () => {
console.log("socket 链接失败");
};
},
//特殊处理
onSend (data) {
data = this.base.isObject(data) ? JSON.stringify(data) : data;
data = this.base.isArray(data) ? data.toString() : data;
data = data.replace(/\\\\/, "\\");
this.shellWs.onSend(data);
},
//删除左右两端的空格
trim (str) {
return str.replace(/(^\s*)|(\s*$)/g, "");
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
运行测试
先知先安装:npm install
进入项目根目录下面,执行:npm run dev
npm run dev
在浏览器输入:http://127.0.0.1:3000