共计 4187 个字符,预计需要花费 11 分钟才能阅读完成。
引言
项目需求,要求在浏览器端进行远程桌面的访问,如图所示:
实现远程桌面,需要依赖 VNC
协议:
VNC(Virtual Network Computing)
,为一种使用 RFB
协议的屏幕画面分享及远程操作软件。此软件借由网络,可发送键盘与鼠标的动作及即时的屏幕画面。
相关的参考比较少,去谷歌搜索出来的文章大多都是如何使用客户端进行 VNC
的搭建与访问,很少有将其内嵌到 web
里的,腾讯云有相关的功能,但因为业务安全性,咱也看不着人家咋实现的。
再见,百度。用百度查了一次之后,我才知道原来 VNC
是口红。
所以 VNC
实践之路就是如下流程:
- 根据自己已有的知识与技能,设计一个
VNC
方案。 - 尝试,分析可行性。
- 根据可行性修改方案细节,或推翻方案重新设计。
从整体的最开始设计,到最终落地方案,大约经历了以下七个方案的迭代:
-
SpringBoot
调用REALVNC
的C++
类库,前后台进行数据交互。失败,因为REALVNC
太贵了,客户承受不起。 -
SpringBoot
中模仿TightVNC
实现JavaViewer
获取数据,前后台进行数据交互。失败,因为TightVNC JavaViewer
的源码没注释,看不懂。 -
SpringBoot
中手写VNC
客户端,前后台数据交互。失败,因为从0
实现一个协议太复杂了,时间成本太高。 - 浏览器端只做
VNC
链接,使用原生客户端,直接访问主机。失败,需要安装软件,且只能访问局域网中的主机。 - 原生客户端 +
nginx
数据转发。失败,需要安装软件,无法实现动态转发 (无法动态变更nginx
配置文件)。 -
no-vnc
+nginx
数据转发。失败,无法实现动态转发 (无法动态变更nginx
配置文件)。 -
no-vnc
+node.js
数据转发。成功,完美实现。
实现
思想
整体思想如下图所示:nginx
转发前台的 websocket
连接,为了实现外网转发,添加开发的 node.js
服务器作为代理,将浏览器端 no-vnc
的websocket
数据报在运输层转发给目标主机。
why nginx ?
如果思考过的话,其实发现不用 nginx
也能实现功能,这里使用 nginx
主要是减少了前台对后台架构的耦合。
添加网关转发所有请求,对前台只暴露一个端口,不管后台用什么技术,用什么架构,用什么微服务,在前台看来,就好像在访问单体应用一样。
就像目前的华软项目一样,后台用了 spring-boot
、.net
、node.js
,各语言各框架发挥各自的优势,通过nginx
的转发将各模块连接起来,无论后台的架构怎么变,对前台毫无影响,这应该是微服务架构的最佳实践。
这是 spring
官方推荐的微服务架构图,我们学习并实践了 api
网关,spring
推荐netflix zuul
,我们用的nginx
,在请求转发上,二者性能不相上下。
随着业务需求的增长,我们肯定也会服务拆分,服务注册,服务发现,消息队列,RPC
调用。然后用上 eureka
、zookeeper
、hystrix
、feign
等一个个优秀的开源组件,一起探索 spring-cloud
的最佳实践。
websocket
之前一直不了解websocket
,就是知道个名,具体细节没有学习。
http
协议:请求响应,客户端请求,服务器响应,一次请求就结束。服务端无法主动向客户端推送数据。
为了解决这个问题,websocket
应运而生。如果所示,不做赘述。
no-vnc
官网链接:noVNC
安装依赖:
npm install @novnc/novnc
前台组件
一个空div
,同时在组件中引用。
<div class="container" #container>
</div>
@ViewChild('container')
private container: ElementRef<HTMLDivElement>;
核心的代码其实就这几行,所有协议的细节都被封装在 no-vnc
中的 RFB
类中了。
所有描述以访问 192.168.0.104
主机的 5900
端口为例,websocket
地址为:ws://127.0.0.1:8013/vnc/192.168.0.104:5900
。
/**
* VNC 连接
*/
private VNCConnect(): void {
/** 访问 /vnc/ websocket */
const url = `ws://${this.host}/vnc/${this.ip}:${this.port}`;
/** 新建远程控制对象 */
this.rfb = new RFB(this.container.nativeElement, url, {
credentials: {password: this.password,},
});
/** 添加 connect 事件监听器 */
this.rfb.addEventListener('connect', () => {this.rfb.focus();
});
}
nginx 转发
nginx
监听本地的 8013
端口。
ws://127.0.0.1:8013/vnc/192.168.0.104:5900
请求发给了 nginx
,根据前缀匹配,以/vnc/
开头的转发给 8112
端口。
location /vnc/ {
proxy_pass http://127.0.0.1:8112/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
node.js 转发
node.js
监听 8112
端口,处理当前的 websocket
请求。
/** 建立基于 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);
});
转发的核心代码在方法 web_socket_handler
中,以下是完整代码:
这里说一句,之前写的注释都不规范,所有注释都应该是文档注释,单行注释使用 /** 内容 */
的格式。
/** 引入 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;
/** 截取主机地址 */
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();});
};
总结
为了这个功能犯愁了半个月,觉也睡不好,客户都在腾讯云上看到过的功能,写不出来就特别的难受,如今终于圆满解决。
拥抱开源,互帮互助,如果过去的我能看到我这篇博客,我就不会浪费这么久的时间进行一次次的尝试了。
晚上还有字节跳动的笔试,字节跳动的题特别难,小伙伴们加油呀。