前言
:近期有个小实验,课程作业吧,要实现一个浏览器终端连接服务器,同时也能连接docker容器,于是开始查资料,由于自己之前也没有这方面的经验,而且好多博客讲不清反而会误导自己的思路,才想起自己写一篇博客来分享一下
注:我参考了一些博客,现在链接找不到就没去刻意搜索挂上来了
**注重思路,想着直接复制运行就......,实验课作业,漏洞很多**
服务端
这里以websocket
保持状态进行前后端交互,不懂的可以自行百度了解一下它和普通http协议的区别。
1、导入Maven依赖
<!-- 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.55</version>
</dependency>
<!-- WebSocket 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.27</version>
</dependency>
<dependency>
<groupId>com.spotify</groupId>
<artifactId>docker-client</artifactId>
<version>8.16.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
2、配置config
配置拦截器和处理器
由于SocketHandler
处理器当中的处理逻辑交给spring容器管理,这里采用注入的方式引入,通过new
引入会找不到对象报错
IP:Port/stomp/websocketJS
时前端发送请求建立 Webscoket 连接的地址(是啥为所谓,后面对应上了就行)
@Configuration
@EnableWebSocket
public class WebSshConfig implements WebSocketConfigurer {
@Autowired
private SocketHandler socketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(socketHandler, "/stomp/websocketJS")
.addInterceptors(new WebSocketHandshakeInterceptor())
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
3、拦截器
会存在一些跨越看不懂,因为我这是写完了代码再来写的博客,还是那句话,这东西不难,
注重理解才能举一反三
public class WebSocketHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
private final Logger logger = LoggerFactory.getLogger(WebSocketHandshakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// websocket握手建立前调用,获取httpsession
if(request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servlet = (ServletServerHttpRequest) request;
// 这里从request中获取session,获取不到不创建,可以根据业务处理此段
HttpSession httpSession = servlet.getServletRequest().getSession();
logger.info("httpSession key:" + httpSession.getId());
httpSession.setAttribute(Constants.USER_KEY, httpSession.getId());
// 设置连接标志
attributes.put(Constants.CONTAINER_EXEC, false);
String containerId = ((ServletServerHttpRequest) request).getServletRequest().getParameter("containerId");
if (StringUtils.isNotBlank(containerId)){
attributes.put(Constants.CONTAINER_ID, containerId);
attributes.put(Constants.CONTAINER_EXEC, true);
}
}
return super.beforeHandshake(request,response,wsHandler,attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Exception e) {
// websocket握手建立后调用
logger.info("websocket连接握手成功");
}
}
上图红色框中的这一大块其实就是请求过滤,辅助性的做一些校验,实际场景比这复杂多了,引导性的写一些便于理解
4、处理器
主要是处理并引导请求,根据相关过程处理一些信息,请求过程不明朗的同学,自行实践一下
@Component
public class SocketHandler extends DefaultHandshakeHandler implements WebSocketHandler {
private final Logger logger = LoggerFactory.getLogger(SocketHandler.class);
@Autowired
private WebSocketService webSocketService;
@Autowired
private WebDockerService webDockerService;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("初始化连接");
if ((Boolean) session.getAttributes().get(Constants.CONTAINER_EXEC)) {
webDockerService.createExec(session);
} else {
webSocketService.initConnection(session);
}
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws IOException {
if (message instanceof TextMessage) {
logger.info("发送命令:{}", message.getPayload());
if ((Boolean) session.getAttributes().get(Constants.CONTAINER_EXEC)) {
webDockerService.recvHandle((String) message.getPayload(), session);
} else {
webSocketService.recvHandle((String) message.getPayload(), session);
}
} else if (message instanceof BinaryMessage) {
} else if (message instanceof PongMessage) {
} else {
System.out.println("Unexpected WebSocket message type: " + message);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception){
logger.error("数据传输错误");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws IOException {
logger.info("断开连接");
//调用service关闭连接
webSocketService.close(session);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
举个例子
下图就是我在区分与服务器建立连接,还是于docker容器建立连接,根据实际情况灵活使用
5、处理类接口
public interface WebSocketService {
/**
* 初始化ssh连接
* @param session
*/
void initConnection(WebSocketSession session);
/**
* 处理客户段发的数据
* @param buffer
* @param session
*/
void recvHandle(String buffer, WebSocketSession session);
/**
* 数据写回前端 for websocket
* @param session
* @param buffer
* @throws IOException
*/
void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;
/**
* 关闭连接
* @param session
*/
void close(WebSocketSession session);
}
6、处理类接口实现
@Service
public class WebSocketServiceImpl implements WebSocketService {
private static Map<String, Object> sshMap = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(WebSocketServiceImpl.class);
private ExecutorService executorService = Executors.newCachedThreadPool();
@Override
public void initConnection(WebSocketSession session) {
JSch jSch = new JSch();
ConnectInfo connectInfo = new ConnectInfo();
connectInfo.setJsch(jSch);
connectInfo.setWebSocketSession(session);
String uuid = String.valueOf(session.getAttributes().get(Constants.USER_KEY));
//将这个ssh连接信息放入map中
sshMap.put(uuid, connectInfo);
}
@Override
public void recvHandle(String buffer, WebSocketSession session) {
ObjectMapper objectMapper = new ObjectMapper();
WebSSHData webSSHData;
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(Constants.USER_KEY));
if (Constants.OPERATE_CONNECT.equals(webSSHData.getOperate())) {
//如果是连接请求
//找到刚才存储的ssh连接对象
ConnectInfo connectInfo = (ConnectInfo) sshMap.get(userId);
//启动线程异步处理
executorService.execute(new Runnable() {
@Override
public void run() {
try {
//连接到终端
connectToSSH(connectInfo, webSSHData, session);
} catch (JSchException | IOException e) {
logger.error("ssh连接异常");
logger.error("异常信息:{}", e.getMessage());
close(session);
}
}
});
} else if (Constants.OPERATE_COMMAND.equals(webSSHData.getOperate())) {
//如果是发送命令的请求
String command = webSSHData.getCommand();
ConnectInfo connectInfo = (ConnectInfo) sshMap.get(userId);
if (connectInfo != null) {
try {
//发送命令到终端
transToSSH(connectInfo.getChannel(), command);
} catch (IOException e) {
logger.error("ssh连接异常");
logger.error("异常信息:{}", e.getMessage());
close(session);
}
}
} else {
logger.error("不支持的操作");
close(session);
}
}
@Override
public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
session.sendMessage(new TextMessage(buffer));
}
@Override
public void close(WebSocketSession session) {
//获取随机生成的uuid
String userId = String.valueOf(session.getAttributes().get(Constants.USER_KEY));
ConnectInfo connectInfo = (ConnectInfo) sshMap.get(userId);
if (connectInfo != null) {
//断开连接
if (connectInfo.getChannel() != null) {
connectInfo.getChannel().disconnect();
}
//map中移除该ssh连接信息
sshMap.remove(userId);
}
}
/**
* 使用jsch连接终端
*/
private void connectToSSH(ConnectInfo connectInfo, WebSSHData webSSHData, WebSocketSession webSocketSession) throws JSchException, IOException {
Session session;
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
//获取jsch的会话
session = connectInfo.getJsch().getSession(webSSHData.getUsername(), webSSHData.getHost(), webSSHData.getPort());
session.setConfig(config);
//设置密码
session.setPassword(webSSHData.getPassword());
//连接 超时时间30s
session.connect(30000);
//开启shell通道
Channel channel = session.openChannel("shell");
//通道连接 超时时间3s
channel.connect(3000);
//设置channel
connectInfo.setChannel(channel);
//转发消息
transToSSH(channel, "");
//读取终端返回的信息流
InputStream inputStream = channel.getInputStream();
try {
//循环读取
byte[] buffer = new byte[1024];
int i = 0;
//如果没有数据来,线程会一直阻塞在这个地方等待数据。
while ((i = inputStream.read(buffer)) != -1) {
sendMessage(webSocketSession, Arrays.copyOfRange(buffer, 0, i));
}
} finally {
//断开连接后关闭会话
session.disconnect();
channel.disconnect();
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* 将消息转发到终端
*/
private void transToSSH(Channel channel, String command) throws IOException {
if (channel != null) {
OutputStream outputStream = channel.getOutputStream();
outputStream.write(command.getBytes());
outputStream.flush();
}
}
}
服务端到这里差不多完成80%,还有常量类,和实体类我就不一一放上来了,有心的同学知道这些该怎么写,
也可以去我的源码地址自行查看===>链接在文章最后
前端页面
1、terminal组件
<template>
<div class="window" ref="contain">
<div class="header"></div>
<div id="xterm" class="xterm" ref="xterm"/>
</div>
</template>
<script>
import 'xterm/css/xterm.css'
import {Terminal} from 'xterm'
import {FitAddon} from 'xterm-addon-fit'
import SockJS from "sockjs-client";
export default {
name: "terminal",
props: {
// 终端信息
hostInfo: {type: Object},
},
watch: {
'hostInfo.width': {
handler(value) {
this.$refs.contain.style.width = null
this.$refs.contain.style.width = value
this.fitAddon.fit()
},
},
'hostInfo.height': {
handler(value) {
this.$refs.contain.style.height = null
this.$refs.contain.style.height = value
this.fitAddon.fit()
},
},
},
mounted() {
this.$refs.contain.style.width = '200px'
this.$refs.contain.style.height = '700px'
this.initTerm();
},
beforeDestroy() {
this.socket.close();
this.term.dispose();
},
data() {
return {
term: null,
socket: null,
fitAddon: 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"
},
});
this.fitAddon = new FitAddon();
this.fitAddon.activate(term)
term.loadAddon(this.fitAddon);
term.open(document.getElementById('xterm'));
this.fitAddon.fit();
term.focus();
this.term = term;
this.initSocket();
},
//初始化websocket
initSocket() {
let url = 'http://localhost:8080/stomp/websocketJS';
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.socket.send(JSON.stringify({
operate: 'connect',
host: this.hostInfo.host,//IP
port: this.hostInfo.port,//端口号
username: this.hostInfo.username,//用户名
password: this.hostInfo.password,//密码
}));
this.term.write('Connecting...\r\n');
// 监听键盘输入
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;
}
.header {
background-color: #E0E0E0;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
padding: 20px;
}
.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;
float: left;
}
.window .xterm {
margin: 0;
padding: 0;
background-color: #F5F5F5;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
</style>
2、新建一个父组件创建terminal实例
// 主要是将以下参数传递给terminal实例对象
data() {
return {
hostInfo: {
host: '192.168.127.132',
port: '22',
username: '***',
password: '***',
// 这两个是我后续加入其他组件补上的
height: 700,
width: 900,
}
}
},