共计 10797 个字符,预计需要花费 27 分钟才能阅读完成。
起源:https://blog.csdn.net/NoCortY…
前言
最近因为我的项目需要,我的项目中须要实现一个 WebSSH 连贯终端的性能,因为本人第一次做这类型性能,所以首先上了 GitHub 找了找有没有现成的轮子能够拿来间接用,过后看到了很多这方面的我的项目,例如:GateOne、webssh、shellinabox 等。
这些我的项目都能够很好地实现 webssh 的性能,然而最终并没有采纳,起因是在于这些底层大都是 python 写的,须要依赖很多文件,本人用的时候能够应用这种计划,快捷省事,然而做到我的项目中供用户应用时,总不能要求用户做到服务器中必须蕴含这些底层依赖,这显然不太正当,所以我决定本人入手写一个 WebSSH 的性能,并且作为一个独立的我的项目开源进去。(文末附我的项目开源地址)
技术选型
因为 webssh 须要实时数据交互,所以会选用长连贯的 WebSocket,为了开发的不便,框架选用 SpringBoot,另外还本人理解了 Java 用户连贯 ssh 的 jsch 和实现前端 shell 页面的 xterm.js. 所以,最终的技术选型就是 SpringBoot+Websocket+jsch+xterm.js。
导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 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.54</version>
</dependency>
<!-- WebSocket 反对 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 文件上传解析器 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
一个简略的 xterm 案例
因为 xterm 是一个冷门技术,所以很多同学并没有这方面的常识撑持,我也是为了实现这个性能所以长期学的,所以在这给大家介绍一下。
xterm.js 是一个基于 WebSocket 的容器,它能够帮忙咱们在前端实现命令行的款式。就像是咱们平时再用 SecureCRT 或者 XShell 连贯服务器时一样。上面是官网上的入门案例:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
<script src="node_modules/xterm/lib/xterm.js"></script>
</head>
<body>
<div id="terminal"></div>
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $')
</script>
</body>
</html>
最终测试,页面就是上面这个样子:能够看到页面曾经呈现了相似与 shell 的款式,那就依据这个持续深刻,实现一个 webssh。
后端实现
因为 xterm 只有只是实现了前端的款式,并不能真正地实现与服务器交互,与服务器交互次要还是靠咱们 Java 后端来进行管制的,所以咱们从后端开始,应用 jsch+websocket 实现这部分内容。
WebSocket 配置
因为音讯实时推送到前端须要用到 WebSocket,不理解 WebSocket 的同学能够先去自行理解一下,这里就不过多介绍了,咱们间接开始进行 WebSocket 的配置。
/**
* @Description: websocket 配置
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Configuration
@EnableWebSocket
public class WebSSHWebSocketConfig implements WebSocketConfigurer{
@Autowired
WebSSHWebSocketHandler webSSHWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
//socket 通道
// 指定处理器和门路,并设置跨域
webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
.addInterceptors(new WebSocketInterceptor())
.setAllowedOrigins("*");
}
}
处理器 (Handler) 和拦截器 (Interceptor) 的实现
方才咱们实现了 WebSocket 的配置,并指定了一个处理器和拦截器。所以接下来就是处理器和拦截器的实现。拦截器:
public class WebSocketInterceptor implements HandshakeInterceptor {
/**
* @Description: Handler 解决前调用
* @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
* @return: boolean
* @Author: NoCortY
* @Date: 2020/3/1
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {if (serverHttpRequest instanceof ServletServerHttpRequest) {ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
// 生成一个 UUID,这里因为是独立的我的项目,没有用户模块,所以能够用随机的 UUID
// 然而如果要集成到本人的我的项目中,须要将其改为本人辨认用户的标识
String uuid = UUID.randomUUID().toString().replace("-","");
// 将 uuid 放到 websocketsession 中
map.put(ConstantPool.USER_UUID_KEY, uuid);
return true;
} else {return false;}
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {}}
处理器:
/**
* @Description: WebSSH 的 WebSocket 处理器
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Component
public class WebSSHWebSocketHandler implements WebSocketHandler{
@Autowired
private WebSSHService webSSHService;
private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);
/**
* @Description: 用户连贯上 WebSocket 的回调
* @Param: [webSocketSession]
* @return: void
* @Author: Object
* @Date: 2020/3/8
*/
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {logger.info("用户:{}, 连贯 WebSSH", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY));
// 调用初始化连贯
webSSHService.initConnection(webSocketSession);
}
/**
* @Description: 收到音讯的回调
* @Param: [webSocketSession, webSocketMessage]
* @return: void
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {if (webSocketMessage instanceof TextMessage) {logger.info("用户:{}, 发送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
// 调用 service 接管音讯
webSSHService.recvHandle(((TextMessage) webSocketMessage).getPayload(), webSocketSession);
} else if (webSocketMessage instanceof BinaryMessage) {} else if (webSocketMessage instanceof PongMessage) { } else {System.out.println("Unexpected WebSocket message type:" + webSocketMessage);
}
}
/**
* @Description: 呈现谬误的回调
* @Param: [webSocketSession, throwable]
* @return: void
* @Author: Object
* @Date: 2020/3/8
*/
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {logger.error("数据传输谬误");
}
/**
* @Description: 连贯敞开的回调
* @Param: [webSocketSession, closeStatus]
* @return: void
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {logger.info("用户:{}断开 webssh 连贯", String.valueOf(webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY)));
// 调用 service 敞开连贯
webSSHService.close(webSocketSession);
}
@Override
public boolean supportsPartialMessages() {return false;}
}
须要留神的是,我在拦截器中退出的用户标识是应用了随机的 UUID,这是因为作为一个独立的 websocket 我的项目,没有用户模块,如果须要将这个我的项目集成到本人的我的项目中,须要批改这部分代码,将其改为本人我的项目中辨认一个用户所用的用户标识。
WebSSH 的业务逻辑实现(外围)
方才咱们实现了 websocket 的配置,都是一些死代码,实现了接口再依据本身需要即可实现,当初咱们将进行后端次要业务逻辑的实现,在实现这个逻辑之前,咱们先来想想,WebSSH,咱们次要想要出现一个什么成果。我这里做了一个总结:
1. 首先咱们得先连贯上终端(初始化连贯)2. 其次咱们的服务端须要解决来自前端的音讯(接管并解决前端音讯)3. 咱们须要将终端返回的音讯回写到前端(数据回写前端)4. 敞开连贯
依据这四个需要,咱们先定义一个接口,这样能够让需要明了起来。
/**
* @Description: WebSSH 的业务逻辑
* @Author: NoCortY
* @Date: 2020/3/7
*/
public interface WebSSHService {
/**
* @Description: 初始化 ssh 连贯
* @Param:
* @return:
* @Author: NoCortY
* @Date: 2020/3/7
*/
public void initConnection(WebSocketSession session);
/**
* @Description: 解决客户段发的数据
* @Param:
* @return:
* @Author: NoCortY
* @Date: 2020/3/7
*/
public void recvHandle(String buffer, WebSocketSession session);
/**
* @Description: 数据写回前端 for websocket
* @Param:
* @return:
* @Author: NoCortY
* @Date: 2020/3/7
*/
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;
/**
* @Description: 敞开连贯
* @Param:
* @return:
* @Author: NoCortY
* @Date: 2020/3/7
*/
public void close(WebSocketSession session);
}
当初咱们能够依据这个接口去实现咱们定义的性能了。1. 初始化连贯 因为咱们的底层是依赖 jsch 实现的,所以这里是须要应用 jsch 去建设连贯的。而所谓初始化连贯,实际上就是将咱们所须要的连贯信息,保留在一个 Map 中,这里并不进行任何的实在连贯操作。为什么这里不间接进行连贯?因为这里前端只是连贯上了 WebSocket,然而咱们还须要前端给咱们发来 linux 终端的用户名和明码,没有这些信息,咱们是无奈进行连贯的。
public void initConnection(WebSocketSession session) {JSch jSch = new JSch();
SSHConnectInfo sshConnectInfo = new SSHConnectInfo();
sshConnectInfo.setjSch(jSch);
sshConnectInfo.setWebSocketSession(session);
String uuid = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
// 将这个 ssh 连贯信息放入 map 中
sshMap.put(uuid, sshConnectInfo);
}
2. 解决客户端发送的数据 在这一步骤中,咱们会分为两个分支。第一个分支:如果客户端发来的是终端的用户名和明码等信息,那么咱们进行终端的连贯。第二个分支:如果客户端发来的是操作终端的命令,那么咱们就间接转发到终端并且获取终端的执行后果。具体代码实现:
public void recvHandle(String buffer, WebSocketSession session) {ObjectMapper objectMapper = new ObjectMapper();
WebSSHData webSSHData = null;
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(ConstantPool.USER_UUID_KEY));
if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
// 如果是连贯申请
// 找到方才存储的 ssh 连贯对象
SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
// 启动线程异步解决
WebSSHData finalWebSSHData = webSSHData;
executorService.execute(new Runnable() {
@Override
public void run() {
try {
// 连贯到终端
connectToSSH(sshConnectInfo, finalWebSSHData, session);
} catch (JSchException | IOException e) {logger.error("webssh 连贯异样");
logger.error("异样信息:{}", e.getMessage());
close(session);
}
}
});
} else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
// 如果是发送命令的申请
String command = webSSHData.getCommand();
SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
if (sshConnectInfo != null) {
try {
// 发送命令到终端
transToSSH(sshConnectInfo.getChannel(), command);
} catch (IOException e) {logger.error("webssh 连贯异样");
logger.error("异样信息:{}", e.getMessage());
close(session);
}
}
} else {logger.error("不反对的操作");
close(session);
}
}
3. 数据通过 websocket 发送到前端
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {session.sendMessage(new TextMessage(buffer));
}
4. 敞开连贯
public void close(WebSocketSession session) {
// 获取随机生成的 uuid
String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
if (sshConnectInfo != null) {
// 断开连接
if (sshConnectInfo.getChannel() != null) sshConnectInfo.getChannel().disconnect();
//map 中移除该 ssh 连贯信息
sshMap.remove(userId);
}
}
至此,咱们的整个后端实现就完结了,因为篇幅无限,这里将一些操作封装成了办法,就不做过多展现了,重点讲逻辑实现的思路吧。接下来咱们将进行前端的实现。
前端实现
前端工作次要分为这么几个步骤:
- 页面的实现
- 连贯 WebSocket 并实现数据的接管并回写
- 数据的发送
所以咱们一步一步来实现它。
页面实现
页面的实现很简略,咱们只不过须要在一整个屏幕上都显示终端那种大黑屏幕,所以咱们并不必写什么款式,只须要创立一个 div,之后将 terminal 实例通过 xterm 放到这个 div 中,就能够实现了。
<!doctype html>
<html>
<head>
<title>WebSSH</title>
<link rel="stylesheet" href="../css/xterm.css" />
</head>
<body>
<div id="terminal" style="width: 100%;height: 100%"></div>
<script src="../lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
<script src="../js/xterm.js" charset="utf-8"></script>
<script src="../js/webssh.js" charset="utf-8"></script>
<script src="../js/base64.js" charset="utf-8"></script>
</body>
</html>
连贯 WebSocket 并实现数据的发送、接管、回写
openTerminal( {
// 这里的内容能够写死,然而要整合到我的项目中时,须要通过参数的形式传入,能够动静连贯某个终端。operate:'connect',
host: 'ip 地址',
port: '端口号',
username: '用户名',
password: '明码'
});
function openTerminal(options){var client = new WSSHClient();
var term = new Terminal({
cols: 97,
rows: 37,
cursorBlink: true, // 光标闪动
cursorStyle: "block", // 光标款式 null | 'block' | 'underline' | 'bar'
scrollback: 800, // 回滚
tabStopWidth: 8, // 制表宽度
screenKeys: true
});
term.on('data', function (data) {
// 键盘输入时的回调函数
client.sendClientData(data);
});
term.open(document.getElementById('terminal'));
// 在页面上显示连贯中...
term.write('Connecting...');
// 执行连贯操作
client.connect({onError: function (error) {
// 连贯失败回调
term.write('Error:' + error + '\r\n');
},
onConnect: function () {
// 连贯胜利回调
client.sendInitData(options);
},
onClose: function () {
// 连贯敞开回调
term.write("\rconnection closed");
},
onData: function (data) {
// 收到数据时回调
term.write(data);
}
});
}
成果展现
连贯
连贯胜利
命令操作
ls 命令:
vim 编辑器:
top 命令:
结语
这样咱们就实现了一个 webssh 我的项目的实现,没有依赖其它任何的组件,后端齐全应用 Java 实现,因为用了 SpringBoot,非常容易部署。
然而,咱们还能够对这个我的项目进行扩大,比方新增上传或下载文件,就像 Xftp 一样,能够很不便地拖拽式上传下载文件。
Github 我的项目开源地址:https://github.com/NoCortY/We…
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)
2. 别在再满屏的 if/ else 了,试试策略模式,真香!!
3. 卧槽!Java 中的 xx ≠ null 是什么新语法?
4.Spring Boot 2.5 重磅公布,光明模式太炸了!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!