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 相关文章推荐
jQuery Div中加载其他页面的实现代码
Feb 27 Javascript
关于JQuery($.load)事件的用法和分析
Apr 09 Javascript
纯文字版返回顶端的js代码
Aug 01 Javascript
JavaScript插件化开发教程 (四)
Jan 27 Javascript
JavaScript DOM操作表格及样式
Apr 13 Javascript
JS图片放大效果简单实现代码
Sep 08 Javascript
jQuery+CSS3实现四种应用广泛的导航条制作实例详解
Sep 17 Javascript
前端开发必知的15个jQuery小技巧
Jan 22 Javascript
easy ui datagrid 从编辑框中获取值的方法
Feb 22 Javascript
自适应布局meta标签中viewport、content、width、initial-scale、minimum-scale、maximum-scale总结
Aug 18 Javascript
浅谈ECMAScript 中的Array类型
Jun 10 Javascript
layer.confirm点击第一个按钮关闭弹出框的方法
Sep 09 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
Ajax PHP 边学边练 之三 数据库
2009/11/26 PHP
php 获取一个月第一天与最后一天的代码
2010/05/16 PHP
PHP读取mssql json数据中文乱码的解决办法
2016/04/11 PHP
PHP面向对象程序设计之类与反射API详解
2016/12/02 PHP
php版微信自定义回复功能示例
2016/12/05 PHP
PHP底层运行机制与工作原理详解
2020/07/31 PHP
jQuery)扩展jQuery系列之一 模拟alert,confirm(一)
2010/12/04 Javascript
js检查页面上有无重复id的实现代码
2013/07/17 Javascript
js 本地预览的简单实现方法
2014/02/18 Javascript
javascript数据类型验证方法
2015/12/31 Javascript
微信公众号-获取用户信息(网页授权获取)实现步骤
2016/10/21 Javascript
js基础之DOM中document对象的常用属性方法详解
2016/10/28 Javascript
微信小程序实现手势图案锁屏功能
2018/01/30 Javascript
微信小程序实现上传图片功能
2018/05/28 Javascript
vue 框架下自定义滚动条(easyscroll)实现方法
2019/08/29 Javascript
mui js控制开关状态、修改switch开关的值方法
2019/09/03 Javascript
JS实现进度条动态加载特效
2020/03/25 Javascript
jQuery实现鼠标拖拽登录框移动效果
2020/09/13 jQuery
uni-app使用countdown插件实现倒计时
2020/11/01 Javascript
[06:53]2018DOTA2国际邀请赛寻真——为复仇而来的Newbee
2018/08/15 DOTA
python实现ftp客户端示例分享
2014/02/17 Python
Python 正则表达式入门(中级篇)
2016/12/07 Python
python matplotlib 注释文本箭头简单代码示例
2018/01/08 Python
Pandas 数据处理,数据清洗详解
2018/07/10 Python
Python使用pymysql从MySQL数据库中读出数据的方法
2018/07/25 Python
详解python破解zip文件密码的方法
2020/01/13 Python
CSS3制作炫酷带方向感应的鼠标滑过图片3D动画
2016/03/16 HTML / CSS
Lookfantastic德国官网:英国知名美妆购物网站
2017/06/11 全球购物
static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?
2015/02/22 面试题
【魔兽争霸3重制版】原版画面与淬火MOD画面对比
2021/03/26 魔兽争霸
初一英语教学反思
2014/01/11 职场文书
工作时间上网检讨书
2014/02/03 职场文书
《最后的姿势》教学反思
2014/02/27 职场文书
怎么写工作检讨书
2014/11/16 职场文书
通知范文怎么写
2015/04/16 职场文书
MySQL中日期型单行函数代码详解
2021/06/21 MySQL