no-vnc和node.js实现web远程桌面的完整步骤


Posted in Javascript onAugust 11, 2019

引言

项目需求,要求在浏览器端进行远程桌面的访问,如图所示:

no-vnc和node.js实现web远程桌面的完整步骤

实现远程桌面,需要依赖VNC协议:

VNC(Virtual Network Computing),为一种使用RFB协议的屏幕画面分享及远程操作软件。此软件借由网络,可发送键盘与鼠标的动作及即时的屏幕画面。

no-vnc和node.js实现web远程桌面的完整步骤

相关的参考比较少,去谷歌搜索出来的文章大多都是如何使用客户端进行VNC的搭建与访问,很少有将其内嵌到web里的,腾讯云有相关的功能,但因为业务安全性,咱也看不着人家咋实现的。

再见,百度。用百度查了一次之后,我才知道原来VNC是口红。

no-vnc和node.js实现web远程桌面的完整步骤

所以VNC实践之路就是如下流程:

  1. 根据自己已有的知识与技能,设计一个VNC方案。
  2. 尝试,分析可行性。
  3. 根据可行性修改方案细节,或推翻方案重新设计。

no-vnc和node.js实现web远程桌面的完整步骤

从整体的最开始设计,到最终落地方案,大约经历了以下七个方案的迭代:

  1. SpringBoot调用REALVNC的C++类库,前后台进行数据交互。失败,因为REALVNC太贵了,客户承受不起。
  2. SpringBoot中模仿TightVNC实现JavaViewer获取数据,前后台进行数据交互。失败,因为TightVNC JavaViewer的源码没注释,看不懂。
  3. SpringBoot中手写VNC客户端,前后台数据交互。失败,因为从0实现一个协议太复杂了,时间成本太高。
  4. 浏览器端只做VNC链接,使用原生客户端,直接访问主机。失败,需要安装软件,且只能访问局域网中的主机。
  5. 原生客户端 + nginx数据转发。失败,需要安装软件,无法实现动态转发(无法动态变更nginx配置文件)。
  6. no-vnc + nginx数据转发。失败,无法实现动态转发(无法动态变更nginx配置文件)。
  7. no-vnc + node.js数据转发。成功,完美实现。

实现

思想

整体思想如下图所示:nginx转发前台的websocket连接,为了实现外网转发,添加开发的node.js服务器作为代理,将浏览器端no-vnc的websocket数据报在运输层转发给目标主机。

no-vnc和node.js实现web远程桌面的完整步骤

why nginx ?

如果思考过的话,其实发现不用nginx也能实现功能,这里使用nginx主要是减少了前台对后台架构的耦合。

添加网关转发所有请求,对前台只暴露一个端口,不管后台用什么技术,用什么架构,用什么微服务,在前台看来,就好像在访问单体应用一样。

就像目前的华软项目一样,后台用了spring-boot、.net、node.js,各语言各框架发挥各自的优势,通过nginx的转发将各模块连接起来,无论后台的架构怎么变,对前台毫无影响,这应该是微服务架构的最佳实践。

no-vnc和node.js实现web远程桌面的完整步骤

这是spring官方推荐的微服务架构图,我们学习并实践了api网关,spring推荐netflix zuul,我们用的nginx,在请求转发上,二者性能不相上下。

随着业务需求的增长,我们肯定也会服务拆分,服务注册,服务发现,消息队列,RPC调用。然后用上eureka、zookeeper、hystrix、feign等一个个优秀的开源组件,一起探索spring-cloud的最佳实践。

websocket

之前一直不了解websocket,就是知道个名,具体细节没有学习。

http协议:请求响应,客户端请求,服务器响应,一次请求就结束。服务端无法主动向客户端推送数据。

为了解决这个问题,websocket应运而生。如果所示,不做赘述。

no-vnc和node.js实现web远程桌面的完整步骤

no-vnc

官网链接:noVNC

no-vnc和node.js实现web远程桌面的完整步骤

安装依赖:

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();
  });
};

总结

为了这个功能犯愁了半个月,觉也睡不好,客户都在腾讯云上看到过的功能,写不出来就特别的难受,如今终于圆满解决。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
ExtJS4如何自动生成控制grid的列显示、隐藏的checkbox
May 02 Javascript
EasyUI中combobox默认值注意事项
Mar 01 Javascript
JavaScript检测鼠标移动方向的方法
May 22 Javascript
纯javascript响应式树形菜单效果
Nov 10 Javascript
Javascript 实现简单计算器实例代码
Oct 23 Javascript
jquery 删除节点 添加节点 找兄弟节点的简单实现
Dec 07 Javascript
vue.js 初体验之Chrome 插件开发实录
May 13 Javascript
搭建element-ui的Vue前端工程操作实例
Feb 23 Javascript
React Native日期时间选择组件的示例代码
Apr 27 Javascript
微信小程序bindinput与bindsubmit的区别实例分析
Apr 17 Javascript
layui+jquery支持IE8的表格分页方法
Sep 28 jQuery
vue项目两种方式实现竖向表格的思路分析
Apr 28 Vue.js
Angular8基础应用之表单及其验证
Aug 11 #Javascript
浅谈javascript错误处理
Aug 11 #Javascript
axios异步提交表单数据的几种方法
Aug 11 #Javascript
node.js实现带进度条的多文件上传
Mar 27 #Javascript
基于Express框架使用POST传递Form数据
Aug 10 #Javascript
Vue实现点击显示不同图片的效果
Aug 10 #Javascript
vue+eslint+vscode配置教程
Aug 09 #Javascript
You might like
php5 non-thread-safe和thread-safe这两个版本的区别分析
2010/03/13 PHP
PHP创建word文档的方法(平台无关)
2016/03/29 PHP
php强制下载文件函数
2016/08/24 PHP
用js怎么把&amp;字符换成&quot;&amp;amp:&quot;
2006/10/19 Javascript
stream.js 一个很小、完全独立的Javascript类库
2011/10/28 Javascript
鼠标滚轴控制文本框值的JS代码
2013/11/19 Javascript
jQuery绑定事件不执行但alert后可以正常执行
2014/06/03 Javascript
js,jquery滚动/跳转页面到指定位置的实现思路
2014/06/03 Javascript
Java遍历集合方法分析(实现原理、算法性能、适用场合)
2016/04/25 Javascript
Ajax与服务器(JSON)通信实例代码
2016/11/05 Javascript
Angular2 组件间通过@Input @Output通讯示例
2017/08/24 Javascript
JS实现颜色的10进制转化成rgba格式的方法
2017/09/04 Javascript
Vue组件为什么data必须是一个函数
2020/06/11 Javascript
python 运算符 供重载参考
2009/06/11 Python
python连接mysql数据库示例(做增删改操作)
2013/12/31 Python
python生成随机验证码(中文验证码)示例
2014/04/03 Python
Python基于有道实现英汉字典功能
2015/07/25 Python
Python程序退出方式小结
2017/12/09 Python
利用Django-environ如何区分不同环境
2018/08/26 Python
解决Python2.7中IDLE启动没有反应的问题
2018/11/30 Python
Python中字符串List按照长度排序
2019/07/01 Python
Python Multiprocessing多进程 使用tqdm显示进度条的实现
2019/08/13 Python
python datetime中strptime用法详解
2019/08/29 Python
如何基于python实现画不同品种的樱花树
2020/01/03 Python
python GUI库图形界面开发之PyQt5控件数据拖曳Drag与Drop详细使用方法与实例
2020/02/27 Python
django 数据库 get_or_create函数返回值是tuple的问题
2020/05/15 Python
完美解决keras 读取多个hdf5文件进行训练的问题
2020/07/01 Python
Python 随机按键模拟2小时
2020/12/30 Python
css3图片边框border-image的用法
2017/06/30 HTML / CSS
澳大利亚最大的百货公司:Myer
2018/12/21 全球购物
2015年房地产销售工作总结
2015/04/20 职场文书
2015大学生入党个人自传
2015/06/26 职场文书
房地产置业顾问工作总结
2015/10/23 职场文书
500字作文之关于爸爸
2019/11/14 职场文书
html+css实现赛博朋克风格按钮
2021/05/26 HTML / CSS
laravel添加角色和模糊搜索功能的实现代码
2021/06/22 PHP