项目需求,要求在浏览器端进行远程桌面的访问,如图所示:javascript
实现远程桌面,须要依赖VNC
协议:html
VNC(Virtual Network Computing)
,为一种使用RFB
协议的屏幕画面分享及远程操做软件。此软件借由网络,可发送键盘与鼠标的动做及即时的屏幕画面。java
相关的参考比较少,去谷歌搜索出来的文章大多都是如何使用客户端进行VNC
的搭建与访问,不多有将其内嵌到web
里的,腾讯云有相关的功能,但由于业务安全性,咱也看不着人家咋实现的。node
再见,百度。用百度查了一次以后,我才知道原来VNC
是口红。nginx
因此VNC
实践之路就是以下流程:web
VNC
方案。从总体的最开始设计,到最终落地方案,大约经历了如下七个方案的迭代:spring
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
数据报在运输层转发给目标主机。typescript
若是思考过的话,其实发现不用nginx
也能实现功能,这里使用nginx
主要是减小了前台对后台架构的耦合。shell
添加网关转发全部请求,对前台只暴露一个端口,无论后台用什么技术,用什么架构,用什么微服务,在前台看来,就好像在访问单体应用同样。npm
就像目前的华软项目同样,后台用了spring-boot
、.net
、node.js
,各语言各框架发挥各自的优点,经过nginx
的转发将各模块链接起来,不管后台的架构怎么变,对前台毫无影响,这应该是微服务架构的最佳实践。
这是spring
官方推荐的微服务架构图,咱们学习并实践了api
网关,spring
推荐netflix zuul
,咱们用的nginx
,在请求转发上,两者性能不相上下。
随着业务需求的增加,咱们确定也会服务拆分,服务注册,服务发现,消息队列,RPC
调用。而后用上eureka
、zookeeper
、hystrix
、feign
等一个个优秀的开源组件,一块儿探索spring-cloud
的最佳实践。
以前一直不了解websocket
,就是知道个名,具体细节没有学习。
http
协议:请求响应,客户端请求,服务器响应,一次请求就结束。服务端没法主动向客户端推送数据。
为了解决这个问题,websocket
应运而生。若是所示,不作赘述。
官网连接: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
监听本地的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
监听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(); }); };
为了这个功能犯愁了半个月,觉也睡很差,客户都在腾讯云上看到过的功能,写不出来就特别的难受,现在终于圆满解决。
拥抱开源,互帮互助,若是过去的我能看到我这篇博客,我就不会浪费这么久的时间进行一次次的尝试了。