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 相关文章推荐
javascript获得CheckBoxList选中的数量
Oct 27 Javascript
跟着JQuery API学Jquery 之三 筛选
Apr 09 Javascript
jQuery 关于伪类选择符的使用说明
Apr 24 Javascript
js带按钮的提示框可供选择示例代码
Sep 17 Javascript
javascript 中__proto__和prototype详解
Nov 25 Javascript
浅谈Javascript中的Function与Object
Jan 26 Javascript
纯javascript代码实现计算器功能(三种方法)
Sep 07 Javascript
浅析Bootstrap表格的使用
Jun 23 Javascript
基于Three.js插件制作360度全景图
Nov 29 Javascript
基于javascript实现的购物商城商品倒计时实例
Dec 11 Javascript
Bootstrap3 模态框使用实例
Feb 22 Javascript
js数组常用最重要的方法
Feb 04 Javascript
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
Yii结合CKEditor实现图片上传功能
2014/06/13 PHP
全面了解PHP中的全局变量
2016/06/17 PHP
Yii实现Command任务处理的方法详解
2016/07/14 PHP
Thinkphp和Bootstrap结合打造个性的分页样式(推荐)
2016/08/01 PHP
tp5修改(实现即点即改)
2019/10/18 PHP
JAVASCRIPT 对象的创建与使用
2021/03/09 Javascript
javascript 定义初始化数组函数
2009/09/07 Javascript
JS Range HTML文档/文字内容选中、库及应用介绍
2011/05/12 Javascript
关于jQuery UI 使用心得及技巧
2012/10/10 Javascript
一个页面元素appendchild追加到另一个页面元素的问题
2013/01/27 Javascript
JavaScript—window对象使用示例
2013/12/09 Javascript
利用Javascript实现BMI计算器
2016/08/16 Javascript
Vuejs第六篇之Vuejs与form元素实例解析
2016/09/05 Javascript
KnockoutJS 3.X API 第四章之数据控制流if绑定和ifnot绑定
2016/10/10 Javascript
jQuery焦点图左右转换效果
2016/12/12 Javascript
微信JS-SDK选取手机照片上传功能
2017/04/21 Javascript
使用Vue组件实现一个简单弹窗效果
2018/04/23 Javascript
Vue项目引进ElementUI组件的方法
2018/11/11 Javascript
layer弹出子iframe层父子页面传值的实现方法
2018/11/22 Javascript
jquery实现上传文件进度条
2020/03/26 jQuery
vue 使用async写数字动态加载效果案例
2020/07/18 Javascript
Numpy数据类型转换astype,dtype的方法
2018/06/09 Python
对python3 一组数值的归一化处理方法详解
2018/07/11 Python
Python Pillow Image Invert
2019/01/22 Python
在Python中调用Ping命令,批量IP的方法
2019/01/26 Python
python 爬虫百度地图的信息界面的实现方法
2019/10/27 Python
Python如何将函数值赋给变量
2020/04/28 Python
详解python UDP 编程
2020/08/24 Python
意大利咖啡、浓缩咖啡和浓缩咖啡机:illy caffe
2019/03/20 全球购物
外企办公室竞聘演讲稿
2013/12/29 职场文书
八年级英语教学反思
2014/01/09 职场文书
关键在于落实心得体会
2014/09/03 职场文书
委托证明书
2014/09/17 职场文书
主持人开幕词
2015/01/29 职场文书
一级电子管军用接收机测评
2022/04/05 无线电
python如何利用cv2.rectangle()绘制矩形框
2022/12/24 Python