为了得到一个相对完善的后端websocket服务进程和前端页面功能,我也是百度并自己尝试了很长时间了,总结从百度搜索得来后缺失的要点如下:
前端要点:
1、发送数据用json格式字符串,后端方便读取后判断是需要调整屏幕大小还是发送操作命令。
2、this.fitAddon.fit()的调用要延时执行,等页面基本OK了再执行,比如
setTimeout(() => {
this.fitAddon.fit()
}, 500); // 必须延时处理
3、绑定window的resize,并注意在重连以后也要延时执行调节屏幕大小功能。
后端:ssh.shell调用的时候要传入配置{term: ‘xterm’},否则终端用vim或htop时不是彩色的,vim打开文件显示还有下划线。
后台代码如下:
/**用来实现单个webssh功能**/
const utf8 = require("utf8");
const SSHClient = require("ssh2").Client;
const Server = require("ws").Server;
const port = 92
const wss = new Server({
port: port
});
console.log("listen on " + port)
const serverInfo = {
host: 'localhost',
port: 22,
username: 'test',
password: '111111',
};
function createSocket(ws) {
const ssh = new SSHClient();
ssh.on("ready", function () {
ws.send("*** SSH CONNECTION ESTABLISHED ***\r\n");
ssh.shell({term: 'xterm'}, function (err, stream) {
if (err) {
return ws.emit("\r\n*** SSH SHELL ERROR: " + err.message + " ***\r\n");
}
ws.on("message", function (data) {
let obj = JSON.parse(data)
if (obj.Op == "resize") {
console.log("client set cols, rows: ", obj.Cols, obj.Rows);
stream.setWindow(obj.Rows, obj.Cols);
} else if (obj.Op == "stdin") {
stream.write(obj.Data);
}
});
ws.on("close", function () {
console.log("close websocket。");
ssh.end();
});
stream.on("data", function (d) {
let data = d.toString("utf-8");
ws.send(data);
}).on("close", function () {
ssh.end();
});
});
}).on("close", function () {
ws.close();
}).on("error", function (err) {
console.log("\r\n*** SSH CONNECTION ERROR: " + err.message + " ***\r\n");
ws.close();
}).connect(serverInfo);
}
wss.on("connection", function (ws) {
console.log("client connected")
createSocket(ws);
});
前端代码如下:
<template>
<div>
<div class="ssh-container" ref="terminal"></div>
</div>
</template>
<script>
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'
import { debounce } from 'lodash'
const packStdin = data =>
JSON.stringify({
Op: 'stdin',
Data: data
})
const packResize = (cols, rows) =>
JSON.stringify({
Op: 'resize',
Cols: cols,
Rows: rows
})
export default {
name: 'Terminal',
data() {
return {
initText: '连接中...\r\n',
first: true,
term: null,
fitAddon: null,
ws: null,
socketUrl: 'ws://xxx.xxx.xxx.xxx:92',
option: {
lineHeight: 1.0,
cursorBlink: true,
cursorStyle: 'block', // 光标样式 'block' | 'underline' | 'bar'
fontSize: 18,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#181d28'
},
cols: 30 // 初始化的时候不要设置fit,设置col为较小值(最小为可展示initText初始文字即可)方便屏幕缩放
}
}
},
mounted() {
this.initTerm()
this.initSocket()
this.onTerminalResize()
this.onTerminalKeyPress()
},
beforeDestroy() {
this.removeResizeListener()
this.term && this.term.dispose()
},
methods: {
isWsOpen() {
return this.ws && this.ws.readyState === 1
},
initTerm() {
this.term = new Terminal(this.option)
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
this.term.open(this.$refs.terminal)
// this.fitAddon.fit() // 初始化的时候不要使用fit
setTimeout(() => {
this.fitAddon.fit()
}, 500); // 必须延时处理
},
onTerminalKeyPress() {
this.term.onData(data => {
this.isWsOpen() && this.ws.send(packStdin(data))
})
},
// resize 相关
resizeRemoteTerminal() {
const { cols, rows } = this.term
console.log('列数、行数设置为:', cols, rows)
// 调整后端终端大小 使后端与前端终端大小一致
this.isWsOpen() && this.ws.send(packResize(cols, rows))
},
onResize: debounce(function () {
this.fitAddon.fit()
}, 500),
onTerminalResize() {
window.addEventListener('resize', this.onResize)
this.term.onResize(this.resizeRemoteTerminal)
},
removeResizeListener() {
window.removeEventListener('resize', this.onResize)
},
// socket
initSocket() {
this.term.write(this.initText)
this.ws = new WebSocket(this.socketUrl, ['OK'])
this.onOpenSocket()
this.onCloseSocket()
this.onErrorSocket()
this.onMessageSocket()
},
// 打开连接
onOpenSocket() {
this.ws.onopen = () => {
console.log('websocket 已连接')
this.term.reset()
setTimeout(() => {
this.resizeRemoteTerminal()
}, 500)
}
},
// 关闭连接
onCloseSocket() {
this.ws.onclose = () => {
console.log('关闭连接')
this.term.write("未连接, 3秒后重连...\r\n");
setTimeout(() => {
this.initSocket();
}, 3000)
}
},
// 连接错误
onErrorSocket() {
this.ws.onerror = () => {
this.$message.error('websoket连接失败,请刷新!')
}
},
// 接收消息
onMessageSocket() {
this.ws.onmessage = res => {
const data = res.data
const term = this.term
// console.log("receive: " + data)
// 第一次连接成功将 initText 清空
if (this.first) {
this.first = false
term.reset()
term.element && term.focus()
this.resizeRemoteTerminal()
}
term.write(data)
}
}
}
}
//
</script>
<style lang="scss">
body {
margin: 0;
padding: 0;
}
.ssh-container {
overflow: hidden;
height: 100vh;
border-radius: 4px;
background: rgb(24, 29, 40);
padding: 0px;
color: rgb(255, 255, 255);
.xterm-scroll-area::-webkit-scrollbar-thumb {
background-color: #b7c4d1;
/* 滚动条的背景颜色 */
}
}
</style>