项目描述
该项目的需求是当我们使用noVnc远程连接另一台电脑后,能打开一款名叫tecplot的软件。
该项目时序图
项目实现思路
1.成功连接noVnc后,修改noVnc里面的源代码。采用websocket进行客户端和服务器之间的通讯
2.在websocket serve端采用父子进程的方式打开应用程序
3.使用log4j进行项目日志的管理
4.用pm2来启动node服务端项目
客户端实现
noVnc的使用和安装大家可以看一下这篇博客 All in Web | 基于web的远程桌面-noVNC - 知乎 (zhihu.com)
修改noVnc.html源码,在连接成功之后执行websocket的连接,发送消息
const CMD_CODE = { "OPEN":0, "CLOSE":1, } function NoVncClient(IP,urlFile) { this.pid = null; this.wsClient = null; this.IP = IP; this.fileUrl = urlFile; this.connect = connect; this.onMessage = receiveMsg; this.disConnect = disConnect } function connect() { console.log('this con', this) this.wsClient = new WebSocket(`ws://${this.IP}:8080`); const url = this.fileUrl; this.wsClient.onopen = function () { console.log(this.readyState,'连接成功') const sendMsg = { cmd:CMD_CODE.OPEN, url:url, } console.log('sendMsg',sendMsg) this.send(JSON.stringify(sendMsg)) } this.wsClient.addEventListener('error', function (event) { console.log('WebSocket error: ', event); }); } function receiveMsg() { const that = this; this.wsClient.onmessage = function (msg) { const receiveFromServerMsg = JSON.parse(msg.data) // UI.showStatus(receiveFromServerMsg, 'normal'); console.log('remote msg',receiveFromServerMsg) that.pid = receiveFromServerMsg.pid } } function disConnect() { console.log(this.pid,this,'this dis') const closeMsg = { pid:this.pid, cmd:CMD_CODE.CLOSE, url:this.fileUrl } console.log('close',closeMsg) // UI.showStatus(closeMsg, 'normal'); this.wsClient.send(JSON.stringify(closeMsg)) } function getUrlParamsByName(name) { let reg = new RegExp(`(?<=\b${name}=)[^&]*`), str = location.search || '', target = str.match(reg); if(target) { return target[0] } } const IP=document.domain; const noClient = new NoVncClient(IP,getUrlParamsByName('path')); noClient.connect() noClient.onMessage() window.onbeforeunload = function(){ noClient.disConnect() }
服务端实现
创建websocket连接
const ws =new WebSocketServer({ port:PORT })
收到客户端的消息
ws.on('connection', function(ws){ recLog.send({msg:'websocket connect success'}) ws.on('message',function(message){ console.log('父进程收到客户端的消息',JSON.parse(message)) receiveMsgFromClient(message); }) setInterval(() =>transmitMsg(ws),10000) })
向客户端发送消息
// 这里的ws是connection回调中的ws,而不是创建的ws实例. // 不同客户端的连接对应一个不同的ws ws.send(JSON.stringify(msg))
采用子进程启动应用程序
const parentInstance = ()=> { const parent = spawn("node", ["child.js"], { cwd: path.resolve(__dirname, './child'), stdio: [0, 1, 2, "ipc"] }) return parent; }
- options - cwd:Current working directory of the child process.子进程当前的工作目录 - stdio: ['inherit', 'inherit', 'inherit','ipc']使用ipc的方法进行通信
父进程转发消息给客户端和子进程
const transmitMsg = (ws)=>{ const parent = parentInstance() if(messageQue.length){ const msg = messageQue.shift() console.log('父进程发送给子进程消息') parent.send(msg) } parent.on("message",(data)=>{ const {code} = data; console.log('父进程收到子进程消息') const recLog = log4jInstance() const sendVal = JSON.stringify(data); switch (code) { case PROCESS_CODE.RUNNING: childProcessNum++; recLog.send({msg:sendVal}) ws.send(sendVal) break; case PROCESS_CODE.EXIT: childProcessNum--; ws.send(sendVal) recLog.send({msg:sendVal}) break; case PROCESS_CODE.ERROR: ws.send(sendVal) recLog.send({msg:sendVal}) break; default: break; } }) }
子进程接受,发送消息
process代表我当前的子进程
process.on('message', (data) => { console.log(data) }) process.send(msg)
process.on("message",(data)=>{ const {cmd,url} = data; const recLog = log4jInstance() switch (cmd) { case CMD_CODE.OPEN: execFilePromise(url).then((value)=>{ console.log('子进程发送给父进程') process.send(value) recLog.send({msg:value}) }).catch((err)=>{ process.send(err) recLog.send({msg:err,type:'error'}) }) break; case CMD_CODE.EXIT: const { pid } = data; console.log('kill',pid) kill(pid,(err)=>{ if(err){ process.send({msg:err,code:PROCESS_CODE.ERROR}) recLog.send({msg:err,type:'error'}) } else{ process.send({msg:"close success",code:PROCESS_CODE.EXIT}) recLog.send({msg:`kill ${pid} success`}) } }) break default: console.log('default'); break; } })
log4j日志管理
log4j配置文件
log4js.configure({ appenders: { cheese: { type: "file", filename: "cheese.log" } }, categories: { default: { appenders: ["cheese"], level: "error" } }, });
log4j的配置主要分成两个部分,分别是appenders和categories
appenders
appenders配置的是每一种的输出方式,你可以在appenders配置多种输出方式
out: { type: 'stdout', }, dataFile: { type: 'dateFile', numBackups:7,//7天之前的文件即删除 filename: '../logs/out.log', keepFileExt: true, maxLogSize: 20480000, backups: 4, },
比如我这里配置了两种方式分别采用了stdout控制台输出和dataFile文件输出
categories
categories表示你选择你在appenders里面配置的那种输出方式输出
categories: { default:{ appenders:['out','dataFile'], level:'debug', // 输出等级,只输出比这个等级高的 }, },
这里我采用我在appenders里面配置的两种输出方式,即会输出到控制台和out.log这个文件中
实例化logger
// 当我在getLogger()里面不传任何参数的时候,默认对应categories里面default配置 // default配置再会根据我里面的appenders数组,去找appender中out和dataFile的配置 let logger = log4js.getLogger();
我们还可以实例出不同的logger,分别输出不同类型的文件
categories: { default: { appenders: ['consoleOut', 'default'], level: 'all', }, error: { appenders: ['consoleOut', 'error'], level: 'warn', }, },
// 普通级别的logger,输出到控制台和日期分类的文件 const defaultLogger = log4js.getLogger('default') // 错误信息的logger,输出到控制台和error.log中 const errorLogger = log4js.getLogger('error')
Bug记录
父子进程同时写log4j日志时失败
当我用父子进程进行通信的时候,我需要在两个进程中都要记录一些log4j的日志信息。但是发现只有子进程的日志信息书写进了文件里面,而父进程的日志信息没有记录到。后来发现原来是不能两个进程不能同时写文件,所以我们只好单独开一个进程,把所有的信息交给这个进程来处理。
消息群发???
我们开不同的客户端,理论上应该是只能接收到自己的消息。但是在编码的过程中却发现能获得所有的消息。我们创建了一个父进程,父进程对应多个子进程,但是客户端获得了所有子进程的消息。子进程创建的实例太多,连自己都不清楚谁发送了消息。
改进:将子进程的实例和websocket连接成功后的ws实例储存起来,并生成uuid给定一个唯一的值,这样在收发消息的时候,根据uuid发送给指定的子进程,这样在创建子进程的过程当中,就不会失去对子进程的控制。