Node.js 实现远程桌面监控的方法步骤


Posted in Javascript onJuly 02, 2019

描述

最近使用node实现了一个远程桌面监控的应用,分为服务端和客户端,客户端可以实时监控服务端的桌面,并且可以通过鼠标和键盘来控制服务端的桌面。

Node.js 实现远程桌面监控的方法步骤

Node.js 实现远程桌面监控的方法步骤

这里因为我是用的同一台电脑,所以监控画面是这样的,当然使用两台电脑一个跑客户端,一个跑服务端才有意义。

原理

其实这个应用的功能主要分为两部分,一是实现监控,即在客户端可以看到服务端的桌面,这部分功能是通过定时截图来实现的,比如服务端一秒截几次图,然后通过socketio发送到客户端,客户端通过改变img的src来实现一帧帧的显示最新的图片,这样就能看到动态的桌面了。监控就是这样实现的。

另一个功能是控制,即客户端对监控画面的操作,包括鼠标和键盘的操作都可以在服务端的桌面真正的生效,这部分功能的实现是在electron的应用中监听了所有的鼠标和键盘事件,比如keydown、keyup、keypress,mousedown、mouseup、mousemove、click等,然后通过socketio把事件传递到服务端,服务端通过 robot-js来执行不同的事件,这样就能使得客户端的事件在服务端触发了。

实现

原理讲完,我们来具体实现一下(源码链接在这)。

实现socket通信

首先,服务端和客户端分别引入socket.io和socket.io-client, 分别初始化

服务端:

const app = new Koa();
const server = http.createServer(app.callback());
createSocketIO(server);

app.use((ctx): void => {
  ctx.body = 'please connect use socket';
});

server.listen(port, (): void => {
  console.log('server started at http://localhost:' + port);
});

//createSocketIO
const io = socketIO(server, {
    pingInterval: 10000,
    pingTimeout: 5000,
    cookie: false
  });

io.on('connect', (socket): void => {
  socket.emit('msg', 'connected');
}

客户端:

var socket = this.socket = io('http://' + this.ip + ':3000')
socket.on('msg', (msg) => {
 console.log(msg)
})
socket.on('error', (err) => {
 alert('出错了' + err)
})

这样,服务端和客户端就通过socketio建立了链接。

实现桌面监控

之后我们首先要在服务端来截图,使用screenshot-desktop这个包

const screenshot = require('screenshot-desktop')

const SCREENSHOT_INTERVAL = 500;

export const createScreenshot = (): Promise<[string, Buffer]> => {
  return screenshot({format: 'png'}).then((img): [string, Buffer] => {
    return [ img.toString('base64'), img];
  }).catch((err): {} => {
    console.log('截图失败', err);
    return err;
  })
}

export const startScreenshotTimer = (callback): {} => {
  return setInterval((): void => {
    createScreenshot().then(([imgStr, img]): void => {
      callback(['data:image/png;base64,' + imgStr, img]);
    })
  }, SCREENSHOT_INTERVAL)
}

然后通过socketio的emit来传到客户端:

startScreenshotTimer(([imgStr, img]): void => {
  io.sockets.emit('screenshot', imgStr);
});

客户端收到图片后,设置到img的src上(这里是base64的图片url):

<img 
  class="screenshot" 
  :src="screenshot"
/>
data () {
 return {
  screenshot: ''
 }
}
socket.on('screenshot', (data) => {
 this.screenshot = data
})

其实这样就已经实现了桌面监控了,有兴趣的同学可以照着这个思路实现看看,并不是很麻烦。

当然这样的方案是有问题的,因为我们需要知道服务端桌面尺寸的大小,然后根据这个来调整客户端显示的图片尺寸。

实现这个细节是使用的get-pixels这个库,可以读取本地图片文件的宽度高度等信息,所以我先把图片写入本地,然后又读取出来,这样获取到的屏幕尺寸。

interface ScreenSize {
  width: number;
  height: number;
}

function getScreenSize(img): Promise<ScreenSize> {
  const imgPath = path.resolve(process.cwd(), './tmp.png');
  fs.writeFileSync(imgPath, img);
  return new Promise((resolve): void => {
    getPixels(imgPath, function(err, pixels): void {
      if(err) {
        console.log("Bad image path")
        return
      }
      resolve({
        width: pixels.shape[0],
        height: pixels.shape[1]
      });
    });
  })
}

然后通过socektio传递给客户端

getScreenSize(img).then(({ width, height}) => {
  io.sockets.emit('screensize', {
    width,
    height
  })
});

客户端收到之后调整图片大小就可以了

<img 
  class="screenshot" 
  :src="screenshot"
  :style="screenshotStyle"
/>
data () {
 return {
  screenshot: '',
  screenshotStyle: '',
 }
}
socket.on('screensize', (screensize) => {
 this.screenshotStyle = {'width': screensize.width + 'px', 'height': screensize.height + 'px'}
})

至此已经实现了桌面监控,并且图片尺寸和服务端屏幕的尺寸是一致的。

这里还有一个细节,就是获取到的图片大小是物理像素,而客户端设置的px是设备无关像素,也就是要除以dpr才是px的值。这里需要获取dpr,因为目前只是在mac下用,所以直接除以2了。

实现远程控制

代码写到这里,客户端的electron应用中已经可以实时显示服务端的桌面了。(当然像输入ip的弹框,以及electron-vue和typescript等和主要逻辑无关的细节就不展开了。)

接下来我们要实现远程控制,也就是监听事件,传递事件,执行事件这几部分。

首先我们定义一下传递的事件的格式:

interface MouseEvent {
  type: string;
  buttonType: string;
  x: number;
  y: number;
}

interface KeyboardEvent {
  type: string;
  keyCode: number;
  keyName: string;
}

鼠标事件MouseEvent,type为鼠标事件的类型,具体的值包括mousedown、mouseup、mousemove、click、dblclick,buttonType指的是鼠标的左键还是右键,值为 left 或 right,x和y是具体的坐标。

键盘事件KeyboardEvent,type为键盘事件的类型,具体的值包括keydown、keyup、keypress,keyCode为键盘码,keyName为键的名字。

接下来我们要在客户端监听事件:

<img 
  class="screenshot" 
  :src="screenshot"
  :style="screenshotStyle"
  @mousedown="handleMouseEvent"
  @mousemove="handleMouseEvent" 
  @mouseup="handleMouseEvent"
  @click="handleMouseEvent"
  @dblclick="handleMouseEvent"  
/>
window.onkeypress = window.onkeyup = window.onkeydown = this.handleKeyboardEvent

通过socekt把事件传递到服务端

handleKeyboardEvent (e) {
  this.socket && this.socket.emit('userevent', {
   type: 'keyboard',
   event: {
    type: e.type,
    keyName: e.key,
    keyCode: e.keyCode
   }
  })
 },
 handleMouseEvent (e) {
  this.socket && this.socket.emit('userevent', {
   type: 'mouse',
   event: {
    type: e.type,
    buttonType: e.buttons === 2 ? 'right' : 'left',
    x: e.clientX,
    y: e.clientY
   }
  })
 },

然后在服务端把事件取出来执行,执行事件使用的是robot-js:

const { Mouse, Point, Keyboard } = require('robot-js');

interface MouseEvent {
  type: string;
  buttonType: string;
  x: number;
  y: number;
}

interface KeyboardEvent {
  type: string;
  keyCode: number;
  keyName: string;
}

export default class EventExecuter {
  public mouse;
  public keyboard;
  public constructor(){
    this.mouse = new Mouse();
    this.keyboard = new Keyboard();
  }

  public executeKeyboardEvent(event: KeyboardEvent): void {
    switch(event.type) {
      case 'keydown':
        this.keyboard.press(event.keyCode);
        break;
      case 'keyup':
        this.keyboard.release(event.keyCode);
        break;
      case 'keypress':
        this.keyboard.click(event.keyCode);
        break;
      default: break;
    }
  }

  public executeMouseEvent(event): void {
    Mouse.setPos(new Point(event.x, event.y));
    const button = event.buttonType === 'left' ? 0 : 2
    switch(event.type) {
      case 'mousedown':
        this.mouse.press(button);
        break;
      case 'mousemove':
        break;
      case 'mouseup': 
        this.mouse.release(button);
        break;
      case 'click': 
        this.mouse.click(button);
        break;
      case 'dblclick': 
        this.mouse.click(button);
        this.mouse.click(button);
        break;
      default: break;
    }
  }

  public exectue(eventInfo): void {
    console.log(eventInfo);
    switch (eventInfo.type) {
      case 'keyboard':
        this.executeKeyboardEvent(eventInfo.event);
        break;
      case 'mouse':
        this.executeMouseEvent(eventInfo.event);
        break;
      default: break;
    }
  }
}

至此,桌面监控和远程控制的客户端还有服务端的部分,以及两端的通信都已经实现了。思路其实并不麻烦,但细节还是很多的。有兴趣的同学可以把代码下下来跑跑试试,或者按着这个思路自己实现一遍,还是挺好玩的。

源码链接

remote-monitor-server

remote-monitor-client

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
显示、隐藏密码
Jul 01 Javascript
javascript 隔行换色函数代码
Oct 24 Javascript
jquery中focus()函数实现当对象获得焦点后自动把光标移到内容最后
Sep 29 Javascript
jquery 页眉单行信息滚动显示实现思路及代码
Jun 26 Javascript
Jquery全选与反选点击执行一次的解决方案
Aug 14 Javascript
jquery拖拽效果完整实例(附demo源码下载)
Jan 14 Javascript
教你如何终止JQUERY的$.AJAX请求
Feb 23 Javascript
JavaScript中${pageContext.request.contextPath}取值问题及解决方案
Dec 08 Javascript
使用angular帮你实现拖拽的示例
Jul 05 Javascript
读懂CommonJS的模块加载
Apr 19 Javascript
微信小程序录音实现功能并上传(使用node解析接收)
Feb 26 Javascript
javascript-hashchange事件和历史状态管理实例分析
Apr 18 Javascript
使用vue中的混入mixin优化表单验证插件问题
Jul 02 #Javascript
vue history 模式打包部署在域名的二级目录的配置指南
Jul 02 #Javascript
简单了解微信小程序的目录结构
Jul 01 #Javascript
vue如何实现自定义底部菜单栏
Jul 01 #Javascript
微信小程序如何利用getCurrentPages进行页面传值
Jul 01 #Javascript
vue中的面包屑导航组件实例代码
Jul 01 #Javascript
Vue动态面包屑功能的实现方法
Jul 01 #Javascript
You might like
php简单提示框alert封装函数
2010/08/08 PHP
php计算当前程序执行时间示例
2014/04/24 PHP
PHP实现合并discuz用户
2015/08/05 PHP
PHP实现HTML页面静态化的方法
2015/11/04 PHP
php的4种常用运行方式详解
2016/12/22 PHP
Javascript 的addEventListener()及attachEvent()区别分析
2009/05/21 Javascript
textarea不能通过maxlength属性来限制字数的解决方法
2014/09/01 Javascript
js实现select下拉框菜单
2015/12/08 Javascript
jstree的简单实例
2016/12/01 Javascript
通过BootStrap-select插件 js jQuery控制select属性变化
2017/01/03 Javascript
JS实现css hover操作的方法示例
2017/04/07 Javascript
JS中图片压缩的方法小结
2017/11/14 Javascript
vue 设置路由的登录权限的方法
2018/07/03 Javascript
解决vue router组件状态刷新消失的问题
2018/08/01 Javascript
配置eslint规范项目代码风格
2019/03/11 Javascript
微信小程序之 catalog 切换实现解析
2019/09/12 Javascript
Vue常用的全选/反选的示例代码
2020/02/19 Javascript
python shutil文件操作工具使用实例分析
2019/12/25 Python
flask利用flask-wtf验证上传的文件的方法
2020/01/17 Python
Python实现序列化及csv文件读取
2020/01/19 Python
django修改models重建数据库的操作
2020/03/31 Python
Python脚本破解压缩文件口令实例教程(zipfile)
2020/06/14 Python
Python实现敏感词过滤的4种方法
2020/09/12 Python
Python实现FTP文件定时自动下载的步骤
2020/12/19 Python
俄罗斯GamePark游戏商店网站:购买游戏、游戏机和配件
2020/03/13 全球购物
阿迪达斯越南官网:adidas越南
2020/07/19 全球购物
家庭教育先进个人事迹材料
2014/01/24 职场文书
应届生求职信范文
2014/06/30 职场文书
2014年医生工作总结
2014/11/21 职场文书
2014年法务工作总结
2014/12/11 职场文书
2015年学校体育工作总结
2015/04/22 职场文书
聋哑人盗窃罪辩护词
2015/05/21 职场文书
离职证明格式样本
2015/06/12 职场文书
新郎父亲婚礼致辞
2015/07/27 职场文书
Python Parser的用法
2021/05/12 Python
SQL 窗口函数实现高效分页查询的案例分析
2021/05/21 SQL Server