最近遇到一个需求:虚拟机运行成功后,需要在前端界面上弹出虚拟机的远程桌面(类似
VNC客户端
),在此做个记录~
1. 实现流程
连接地址:
ws://localhost:8081/vnc/192.168.18.57:5900
,使用nginx代理vnc
至node服务,在转发至目标主机,也就是连接的虚拟机地址192.168.18.57:5900
2. 前端
安装 npm install @novnc/novnc
<template>
<div class="full">
<div id="screen" class="full"></div>
</div>
</template>
<script>
import RFB from '@novnc/novnc/core/rfb';
export default {
name: 'Novnc',
data() {
return {
url: '',
rfb: null
}
},
methods: {
getUrl(host) {
let protocol = '';
if (window.location.protocol === 'https:') {
protocol = 'wss://';
} else {
protocol = 'ws://';
}
// 加window.location.host可以走vue.config.js的代理,ws://localhost:8081/vnc/192.168.18.57:5900
const wsUrl = `${protocol}${window.location.host}/vnc/${host}`;
console.log(wsUrl);
return wsUrl;
},
// vnc连接断开的回调函数
disconnectedFromServer(msg) {
console.log('断开连接', msg);
// clean是boolean指示终止是否干净。在发生意外终止或错误时 clean将设置为 false。
if(msg.detail.clean){
// 根据 断开信息的msg.detail.clean 来判断是否可以重新连接
} else {
// 这里做不可重新连接的一些操作
console.log('连接不可用(可能需要密码)')
}
this.rfb = null;
this.connectVnc();
},
// 连接成功的回调函数
connectedToServer() {
console.log('连接成功');
},
//连接vnc的函数
connectVnc() {
const PASSWORD = '';
let rfb = new RFB(document.getElementById('screen'), this.url, {
// 向vnc 传递的一些参数,比如说虚拟机的开机密码等
credentials: {password: PASSWORD}
});
rfb.addEventListener('connect', this.connectedToServer);
rfb.addEventListener('disconnect', this.disconnectedFromServer);
// scaleViewport指示是否应在本地扩展远程会话以使其适合其容器。禁用时,如果远程会话小于其容器,则它将居中,或者根据clipViewport它是否更大来处理。默认情况下禁用。
rfb.scaleViewport = true;
// 是一个boolean指示是否每当容器改变尺寸应被发送到调整远程会话的请求。默认情况下禁用
rfb.resizeSession = true;
this.rfb = rfb;
}
},
mounted() {
this.url = this.getUrl(this.$route.params.host);
this.connectVnc();
},
beforeDestroy() {
this.rfb && this.rfb.disconnect();
}
}
</script>
2. node
/** 引入 http 包 */
const http = require('http');
/** 引入 net 包 */
const net = require('net');
/** 引入 websocket 类 */
const WebSocketServer = require('ws').Server;
/** 本机 ip 地址 */
const localhost = '127.0.0.1';
/** 开放的 vnc websocket 转发端口 */
const vnc_port = 8112;
/** 打印提示信息 */
console.log(`成功创建 WebSocket 代理 : ${localhost} : ${vnc_port}`);
/** 建立基于 vnc_port 的 websocket 服务器 */
const vnc_server = http.createServer();
vnc_server.listen(vnc_port, function () {
const web_socket_server = new WebSocketServer({server: vnc_server});
web_socket_server.on('connection', web_socket_handler);
});
/** websocket 处理器 */
const web_socket_handler = function (client, req) {
/** 获取请求url */
const url = req.url;
console.log("====", url);
/** 截取主机地址 */
const host = url.substring(url.indexOf('/') + 1, url.indexOf(':'));
/** 截取端口号 */
const port = Number(url.substring(url.indexOf(':') + 1));
/** 打印日志 */
console.log(`WebSocket 连接 : 版本 ${client.protocolVersion}, 协议 ${client.protocol}`);
/** 连接到 VNC Server */
const target = net.createConnection(port, host, function () {
console.log('连接至目标主机');
});
/** 数据事件 */
target.on('data', function (data) {
try {
client.send(data);
} catch (error) {
console.log('客户端已关闭,清理到目标主机的连接');
target.end();
}
});
/** 结束事件 */
target.on('end', function () {
console.log('目标主机已关闭');
client.close();
});
/** 错误事件 */
target.on('error', function () {
console.log('目标主机连接错误');
target.end();
client.close();
});
/** 消息事件 */
client.on('message', function (msg) {
target.write(msg);
});
/** 关闭事件 */
client.on('close', function (code, reason) {
console.log(`WebSocket 客户端断开连接:${code} [${reason}]`);
target.end();
});
/** 错误事件 */
client.on('error', function (error) {
console.log(`WebSocket 客户端出错:${error}`);
target.end();
});
};
3. nginx
location /vnc/ {
# rewrite ^.+iot/?(.*)$ /$1 break;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers "Accept, X-Token, Content-Type";
add_header Access-Control-Allow-Methods "GET, POST, DELETE, PATCH, PUT, OPTIONS";
proxy_pass http://127.0.0.1:8112/;
# (以下2句)配置允许创建websocket
proxy_set_header Upgrade websocket;
proxy_set_header Connection Upgrade;
}