基于Xterm的Webterminal实现

平台

  Web前端:Vue.js
  Web后端:tornado
  WebSocket插件:Xtermjs

环境配置

前端配置——安装xterm

1
npm install --save xterm

  xterm.js的插件,使终端的尺寸适合包含元素。

1
npm install --save xterm-addon-fit

  xterm.js的附加组件,用于附加到Web Socket

1
npm install --save xterm-addon-attach

  附:建议版本:

1
2
3
"xterm": "^3.1.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0"

  在main.js加入import 'xterm/dist/xterm.css',否则显示有问题。

后端配置——安装tornado

1
2
pip install tornado==4.5.3
pip install paramiko==2.4.0

代码实现

后端服务器实现

  后端启动代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# -*- coding: utf-8 -*-
import tornado
import tornado.websocket
import paramiko
import threading
import time

# 配置服务器信息
HOSTS = "192.168.64.131"
PORT = 22
USERNAME = "cxx"
PASSWORD = " "

class MyThread(threading.Thread):
def __init__(self, id, chan):
threading.Thread.__init__(self)
self.chan = chan

def run(self):
while not self.chan.chan.exit_status_ready():
time.sleep(0.1)
try:
data = self.chan.chan.recv(1024)
self.chan.write_message(data)
except Exception as ex:
# 注释掉print,否则会报错,原因:库版本不对
# print(str(ex))
pass
self.chan.sshclient.close()
return False


class webSSHServer(tornado.websocket.WebSocketHandler):
def open(self):
self.sshclient = paramiko.SSHClient()
self.sshclient.load_system_host_keys()
self.sshclient.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.sshclient.connect(HOSTS, PORT, USERNAME, PASSWORD)
self.chan = self.sshclient.invoke_shell(term='xterm')
self.chan.settimeout(0)
t1 = MyThread(999, self)
t1.setDaemon(True)
t1.start()

def on_message(self, message):
try:
self.chan.send(message)
except Exception as ex:
print(str(ex))

def on_close(self):
self.sshclient.close()

def check_origin(self, origin):
# 允许跨域访问
return True


if __name__ == '__main__':
# 定义路由
app = tornado.web.Application([
(r"/terminals/", webSSHServer),
],
debug=True
)

# 启动服务器
http_server = tornado.httpserver.HTTPServer(app)
# 监听的端口号
http_server.listen(3000)
tornado.ioloop.IOLoop.current().start()

前端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { Terminal } from 'xterm'
import * as fit from 'xterm/lib/addons/fit/fit'
import * as attach from 'xterm/lib/addons/attach/attach'
Terminal.applyAddon(fit)
Terminal.applyAddon(attach)

import { Message } from 'element-ui'

var webSocket = null
var term = null

// 初始化webSocket,Ip地址和后端对应
export const initWebSocket = (IpAddress) => {
webSocket = new WebSocket('ws://' + IpAddress + '/terminals/')
// 打开事件
webSocket.onopen = runRealTerminal
// 关闭事件
webSocket.onclose = closeRealTerminal
// 错误事件
webSocket.onerror = errorRealTerminal
}

// 初始化terminal
export const initTerminal = (divID, rows, cols) => {
var terminalContainer = document.getElementById(divID)
// Terminal构造函数
term = new Terminal({
fontSize: 18,
fontFamily: 'monospace',
rendererType: 'canvas', // 渲染类型
rows: rows,
cols: cols,
convertEol: true, // 启用时,光标将设置为下一行的开头
// scrollback: 50, // 终端中的回滚量
// disableStdin: false, // 是否应禁用输入
cursorBlink: true, // 光标闪烁
theme: {
foreground: '#ECECEC', // 字体颜色
background: '#000000', // 背景色
lineHeight: 20
}
})

term.open(terminalContainer)
term.attach(webSocket)
}

// webSocket发送数据,需要加换行才能生效
export const sendData = (data) => {
webSocket.send(data + '\n')
}

// 关闭webSocket
export const closeWebSocket = () => {
webSocket.close()
}

// 销毁terminal
export const destroyTerm = () => {
term.destroy()
}

// Terminal清屏
export const clearTerm = () => {
term.clear()
}

// 响应连接成功事件
function runRealTerminal() {
// console.log('webSocket is finished')
Message({
message: '连接Terminal成功!',
type: 'success'
})
}

// 响应错误连接事件
function errorRealTerminal() {
// console.log('error')
Message.error('连接Terminal失败!')
}

// 响应关闭连接事件
function closeRealTerminal() {
// console.log('close')
Message.error('关闭Terminal连接!')
}

Vue模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<div class="app-container">
<div id="exampleTerminal" class="div-example-terminal" />
</div>
</template>

<script>
import * as myXterm from '@/utils/terminal/myXterm'

export default {
data() {
return {
term: null,
webSocket: null,
terminalContainer: null,
}
},
mounted() {
// 页面加载时,初始化webSocket和Terminal
myXterm.initWebSocket('127.0.0.1:3000')
myXterm.initTerminal('exampleTerminal', 35, 76)
},
beforeDestroy() {
// 页面销毁时,关闭webSocket和Terminal
myXterm.closeWebSocket()
myXterm.destroyTerm()
}
}
</script>

<style scoped>
</style>

<style>
.div-example-terminal{
margin-top: 15px;
margin-left: 10px;
border: 2px solid #008B93;
}
</style>

附:vue-prism-editor

安装

1
2
npm install vue-prism-editor   
npm install prismjs

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<template>
<div class="app-container">
<el-tab-pane label="ex01_initialization">
<prism-editor
v-model="ex01_code"
class="ex01-code-editor"
:highlight="highlighter"
:line-numbers="lineNumbers"
:readonly="true"
/>
</div>
</template>

<script>
// 导入vue-prism-editor
import { PrismEditor } from 'vue-prism-editor'
import 'vue-prism-editor/dist/prismeditor.min.css' // import the styles somewhere

// import highlighting library (you can use any library you want just return html string)
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/themes/prism-tomorrow.css' // import syntax highlighting styles

export default {
// 注册组件
components: {
PrismEditor
},
data() {
return {
// 多行代码通过`/n`实现
ex01_code: 'cd ~/libbarrett_examples\ncmake .\nmake ex01_initialize_wam\n./ex01_initialize_wam',
lineNumbers: true
}
},
methods: {
// 代码颜色高亮
highlighter(code) {
return highlight(code, languages.js) // returns html
}
}
}
</script>

<style scoped>
</style>

<style>
/* required class */
.ex01-code-editor {
background: #2d2d2d;
color: #ccc;

font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 16px;
line-height: 1.5;
padding: 5px;

height: 120px;
width: 320px;
border: 2px solid #008B93;
}
/* optional */
.prism-editor__textarea:focus {
outline: none;
}
</style>
谢谢老板!
-------------本文结束感谢您的阅读给个五星好评吧~~-------------