用WEB浏览器管理你的windows服务器

我们先上一张效果图

想实现以上效果,我们先得了解几个概念

1、VNC简介

VNC (Virtual Network Console),即虚拟网络控制台,它是一款基于 UNIX 和 Linux 操作系统的优秀远程控制工具软件,由著名的 AT&T 的欧洲研究实验室开发,远程控制能力强大,高效实用,并且免费开源。

VNC基本上是由两部分组成:一部分是客户端的应用程序(vncviewer);另外一部分是服务器端的应用程序(vncserver)。在任何安装了客户端的应用程序(vncviewer)的计算机都能十分方便地与安装了服务器端的应用程序(vncserver)的计算机相互连接。

2、noVNC

NoVnc:Web 端的Vnc软件、可以直接通过网页访问远程主机,采用 HTML5、WebSockets、Canvas和 JavaScript 实现,被普遍用在各大云平台中。网页就是一个客户端、类似 Windows 下的 vncviewer 。

在使用noVNC之前,必须得先装vncserver,这里我推荐tightvnc(安装方法可自行百度)

注意:到这步就有个坑了,noVNC需要通过WebSockets协议通信,但是VNC服务端并不支持WebSockets协议,是以传统的TCP协议来通信的,所以我们需要搭一个中间层,我这边使用了node.js

const express = require('express');
const expressWs = require('express-ws');
const router = express.Router();
expressWs(router);

const ws = require('../controllers/ws');

router.ws('/ws', ws.conn);

module.exports = router;

const net = require('net')
var utf8 = require('utf8');

const win_port = 5900;

class Ws extends Base {
  constructor() {
    super();
    this.conn = this.conn.bind(this)
    this.decodeBuffer = this.decodeBuffer.bind(this)
  }

  async conn(client, req) {
    const { id } = req.query
    if (!id) {
      client.close();
      return
    }
    remove(id)
    try {
      var target = net.createConnection(win_port, addr, function () {
        remove(id)
        console.log('connected to target');
      });
      target.on('data', function (data) {
        try {
          client.send(data);
        } catch (e) {
          console.log("Client closed, cleaning up target");
          target.end();
        }
      });
      target.on('end', function () {
        console.log('target disconnected');
        client.close();
      });
      target.on('error', function (error) {
        console.log('target connection error', error.message);
        target.end();
        client.close();
      });

      client.on('message', function (msg) {
        remove(id)
        target.write(msg);
      });
      client.on('close', (code, reason) => {
        console.log('WebSocket client disconnected: ' + code + ' [' + reason + ']');
        target.end();
      });
      client.on('error', function (a) {
        target.end();
      });
    } catch (error) {
      client.close();
    }
  }

  	
  decodeBuffer(buf) {
    var returnString = '';
    for (var i = 0; i < buf.length; i++) {
      if (buf[i] >= 48 && buf[i] <= 90) {
        returnString += String.fromCharCode(buf[i]);
      } else if (buf[i] === 95) {
        returnString += String.fromCharCode(buf[i]);
      } else if (buf[i] >= 97 && buf[i] <= 122) {
        returnString += String.fromCharCode(buf[i]);
      } else {
        var charToConvert = buf[i].toString(16);
        if (charToConvert.length === 0) {
          returnString += '\\x00';
        } else if (charToConvert.length === 1) {
          returnString += '\\x0' + charToConvert;
        } else {
          returnString += '\\x' + charToConvert;
        }
      }
    }
    return returnString;
  }

}

module.exports = new Ws();

然后前端开始接入websocket和novnc客户端,我这边使用了react

import React from 'react';
import { Button, Spin, Popconfirm, message } from 'antd';
import RFB from '@novnc/novnc/core/rfb.js';
import KeyTable from "@novnc/novnc/core/input/keysym.js";
import request from '@/utils/request'

const url = 'ws://localhost:8080/ws'

class Win extends React.Component {
  state = {
    screenLoading: true,
    tip: '实验环境连接中...',
  }

  rfb = null;

  count = 0;

  componentDidMount() {
    this.conn();
  }

  conn = () => {
    const { id } = this.props;
    const password = 'D47668F0-9582-48CA-989C-DEF5C35CE575';
    this.rfb = new RFB(document.getElementById('screen'), `${url}?id=${id}`, {
      credentials: { password },
      showDotCursor: true,
    });
    this.props.onRef(this)
    const { rfb } = this;
    rfb.addEventListener("connect", () => {
      rfb.scaleViewport = true;
      rfb.resizeSession = false;
      this.setState({
        screenLoading: false
      })
    });
    rfb.addEventListener("disconnect", () => {
      console.log('连接已断开')
      if (this.count < 3) {
        try {
          this.conn();
        } catch (error) {

        }
      }
      this.count ++;
    });
    window.onresize = function () {
      rfb.scaleViewport = true;
      rfb.resizeSession = false;
    }
  }

  sendCtrlAltDel = () => {
    this.rfb.sendCtrlAltDel();
  }

  sendPass = () => {
    const { rfb } = this;
    rfb.sendKey(KeyTable.XK_1, "Digit1", true);
    rfb.sendKey(KeyTable.XK_2, "Digit2", true);
    rfb.sendKey(KeyTable.XK_3, "Digit3", true);
    rfb.sendKey(KeyTable.XK_4, "Digit4", true);
    rfb.sendKey(KeyTable.XK_5, "Digit5", true);
    rfb.sendKey(KeyTable.XK_6, "Digit6", true);
    rfb.sendKey(KeyTable.XK_Return, "Enter", true);
  }

  componentDidCatch() {
    this.rfb = null;
  }

  	
  render() {
    const {
      screenLoading,
      tip
    } = this.state
    const {
      save
    } = this.props
    return (
      <Spin spinning={screenLoading} tip={tip}>
        {/* <p className="btns">
          {!save && (
            <>
              <Button size="small">全屏</Button>
              <Button size="small">隐藏/显示实验环境</Button>
            </>
          )}
          <Button size="small" onClick={this.sendCtrlAltDel}>CtrlAltDel</Button>
          <Button size="small" onClick={this.sendPass}>登录密码一键输入</Button>
        </p> */}
        <div id="screen" style={{ visibility : screenLoading ? 'hidden' : 'visible' }}></div>
      </Spin>
    )
  }
}

export default Win

好了,我们就可以愉快的在浏览器访问我们的服务器啦~

评论 抢沙发

表情
  1. #1

    来自四川成都的用户 3天前
    点赞,前排摸大佬沾点技术

  2. #2

    来自北京朝阳的用户 7天前
    贼详细,收藏了

  3. #3

    来自四川成都的用户 7天前
    有发布到github吗?

  4. #4

    来自重庆万州的用户 13天前
    good

  5. #5

    来自天津南开的用户 13天前
    虽然很多还看不懂 但是很赞

  6. #6

    来自辽宁沈阳的用户 26天前
    你们送过来的程序员已经见到了,挺厉害一小伙。另外我这边还缺个产品经理,什么时候给我送个过来?