龙空技术网

springboot+websocket实现基于xterm.js的终端terminal(一)

青锋爱编程 415

前言:

当前大家对“apacheabwebsocket”都比较注重,大家都需要了解一些“apacheabwebsocket”的相关文章。那么小编也在网络上搜集了一些对于“apacheabwebsocket””的相关文章,希望朋友们能喜欢,咱们快快来了解一下吧!

文章总共分为三篇,分别是:

springboot+websocket实现基于xterm.js的终端terminal(一)springboot+websocket实现Html端整合Xterm.js实现客户端(二)springboot+websocket实现vue整合Xtermjs实现客户端(三)

请查看本篇文章上下文!!!

什么是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><!--  --><dependency>  <groupId>com.alibaba</groupId>  <artifactId>fastjson</artifactId>  <version>1.2.79</version></dependency><!--  --><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")@Componentpublic 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@EnableAutoConfigurationpublic 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");

至此后端整合完毕。

标签: #apacheabwebsocket