阐述
应着标题所说,web端实现远程桌面控制,方案有好几种,达到的效果就是类似于向日葵一样可以远程桌面,但是操作方可以不用安装客户端,只需要一个web浏览器即可实现,桌面端需写一个程序用来socket连接和执行Windows指令。
实现方案
使用webSocket
实现web端和桌面端的实时TCP通讯和连接,连接后桌面端获取自己的桌面流以照片流的形式截图发送blob格式给web端,web端再接收后将此格式解析再赋值在img标签上,不停的接收覆盖,类似于快照的形式来呈现画面,再通过api获取web端的鼠标事件和键盘事件通过webSocket发送给客户端让他执行Windows事件,以此来达到远程控制桌面控制效果。
Demo实现
因为为了方便同事观看,所以得起一个框架服务,我习惯用vue3了,但大部分都是js代码,可参考改写。
html只需一行搞定。
<div><img ref="imageRef" class="remote" src="" ></div><div> <img ref="imageRef" class="remote" src="" > </div><div> <img ref="imageRef" class="remote" src="" > </div>
接下来就是socket连接,用socket库和直接new webSocket都可以连接,我习惯用库了,因为多一个失败了可以自动连接的功能,少写一点代码?,库也是轻量级才几k大小。顺便把socket心跳
也加上,这个和对端协商好就行了,一般都是ping/pong加个type值,发送时记得处理一下使用json格式发送,如果连接后60秒后没有互相发送消息客户端就会认为你是失联断开连接了,所以他就会强行踢掉你连接状态,所以心跳机制还是必不可少的。
import ReconnectingWebSocket from 'reconnecting-websocket'const remoteControl = '192.168.1.175'const scoketURL = `ws://${remoteControl}:10086/Echo`const imageRef = ref()onMounted(() => {createdWebsocket()})const createdWebsocket = () => {socket = new ReconnectingWebSocket(scoketURL)socket.onopen = function () {console.log('连接已建立')resetHeart()}socket.onmessage = function (event) {// console.log(event.data)const objectDta = JSON.parse(event.data)console.log(objectDta)if (objectDta.type === 100) {resetHeart()}}socket.onclose = function () {console.log('断开连接')}}let heartTime = null // 心跳定时器实例let socketHeart = 0 // 心跳次数let HeartTimeOut = 20000 // 心跳超时时间let socketError = 0 // 错误次数// socket 重置心跳const resetHeart = () => {socketHeart = 0socketError = 0clearInterval(heartTime)sendSocketHeart()}const sendSocketHeart = () => {heartTime = setInterval(() => {if (socketHeart <= 3) {console.log('心跳发送:', socketHeart)socket.send(JSON.stringify({type: 100,key: 'ping'}))socketHeart = socketHeart + 1} else {reconnect()}}, HeartTimeOut)}// socket重连const reconnect = () => {socket.close()if (socketError <= 3) {clearInterval(heartTime)socketError = socketError + 1console.log('socket重连', socketError)} else {console.log('重试次数已用完的逻辑', socketError)clearInterval(heartTime)}}import ReconnectingWebSocket from 'reconnecting-websocket' const remoteControl = '192.168.1.175' const scoketURL = `ws://${remoteControl}:10086/Echo` const imageRef = ref() onMounted(() => { createdWebsocket() }) const createdWebsocket = () => { socket = new ReconnectingWebSocket(scoketURL) socket.onopen = function () { console.log('连接已建立') resetHeart() } socket.onmessage = function (event) { // console.log(event.data) const objectDta = JSON.parse(event.data) console.log(objectDta) if (objectDta.type === 100) { resetHeart() } } socket.onclose = function () { console.log('断开连接') } } let heartTime = null // 心跳定时器实例 let socketHeart = 0 // 心跳次数 let HeartTimeOut = 20000 // 心跳超时时间 let socketError = 0 // 错误次数 // socket 重置心跳 const resetHeart = () => { socketHeart = 0 socketError = 0 clearInterval(heartTime) sendSocketHeart() } const sendSocketHeart = () => { heartTime = setInterval(() => { if (socketHeart <= 3) { console.log('心跳发送:', socketHeart) socket.send( JSON.stringify({ type: 100, key: 'ping' }) ) socketHeart = socketHeart + 1 } else { reconnect() } }, HeartTimeOut) } // socket重连 const reconnect = () => { socket.close() if (socketError <= 3) { clearInterval(heartTime) socketError = socketError + 1 console.log('socket重连', socketError) } else { console.log('重试次数已用完的逻辑', socketError) clearInterval(heartTime) } }import ReconnectingWebSocket from 'reconnecting-websocket' const remoteControl = '192.168.1.175' const scoketURL = `ws://${remoteControl}:10086/Echo` const imageRef = ref() onMounted(() => { createdWebsocket() }) const createdWebsocket = () => { socket = new ReconnectingWebSocket(scoketURL) socket.onopen = function () { console.log('连接已建立') resetHeart() } socket.onmessage = function (event) { // console.log(event.data) const objectDta = JSON.parse(event.data) console.log(objectDta) if (objectDta.type === 100) { resetHeart() } } socket.onclose = function () { console.log('断开连接') } } let heartTime = null // 心跳定时器实例 let socketHeart = 0 // 心跳次数 let HeartTimeOut = 20000 // 心跳超时时间 let socketError = 0 // 错误次数 // socket 重置心跳 const resetHeart = () => { socketHeart = 0 socketError = 0 clearInterval(heartTime) sendSocketHeart() } const sendSocketHeart = () => { heartTime = setInterval(() => { if (socketHeart <= 3) { console.log('心跳发送:', socketHeart) socket.send( JSON.stringify({ type: 100, key: 'ping' }) ) socketHeart = socketHeart + 1 } else { reconnect() } }, HeartTimeOut) } // socket重连 const reconnect = () => { socket.close() if (socketError <= 3) { clearInterval(heartTime) socketError = socketError + 1 console.log('socket重连', socketError) } else { console.log('重试次数已用完的逻辑', socketError) clearInterval(heartTime) } }
成功稳定连接后那么恭喜你完成第一步了,接下来就是获取对端发来的照片流了,使用socket.onmessage
api用来接收对端消息,需要转一下json,因为发送的数据照片流很快,控制台直接刷屏了,所以简单处理一下。收到照片流把blob格式处理一下再使用window.URL.createObjectURL(blob)
赋值给img即可。
socket.onmessage = function (event) {// console.log(event.data)if (event.data instanceof Blob) { // 处理桌面流const data = event.dataconst blob = new Blob([data], { type: "image/jpg" })imageRef.value.src = window.URL.createObjectURL(blob)} else {const objectDta = JSON.parse(event.data)console.log(objectDta)if (objectDta.type === 100) {resetHeart()}}}socket.onmessage = function (event) { // console.log(event.data) if (event.data instanceof Blob) { // 处理桌面流 const data = event.data const blob = new Blob([data], { type: "image/jpg" }) imageRef.value.src = window.URL.createObjectURL(blob) } else { const objectDta = JSON.parse(event.data) console.log(objectDta) if (objectDta.type === 100) { resetHeart() } } }socket.onmessage = function (event) { // console.log(event.data) if (event.data instanceof Blob) { // 处理桌面流 const data = event.data const blob = new Blob([data], { type: "image/jpg" }) imageRef.value.src = window.URL.createObjectURL(blob) } else { const objectDta = JSON.parse(event.data) console.log(objectDta) if (objectDta.type === 100) { resetHeart() } } }
此时页面可以呈现画面了,并且是可以看得到对面操作的,但让人挠头的是,分辨率和尺寸不对,有上下和左右的滚动条显示,并不是百分百的,解决这个问题倒是不难,但如需考虑获取自身的鼠标坐标发送给对端,这个坐标必须准确无误,简单来说就是分辨率自适应
,因为web端使用的电脑屏幕大小是不一样的,切桌面端发送给你的桌面流比如是全屏分辨率的,以此得做适配
,这个放后面解决,先来处理鼠标和键盘事件
,纪录下来并发送对应的事件给桌面端。记得去除浏览器的拖动和鼠标右键事件,以免效果紊乱。
const watchControl = () => { // 监听事件window.ondragstart = function (e) { // 移除拖动事件e.preventDefault()}window.ondragend = function (e) { // 移除拖动事件e.preventDefault()}window.onkeydown = function (e) { // 键盘按下console.log('键盘按下', e)socket.send(JSON.stringify({ type: 0, key: e.keyCode }))}window.onkeyup = function (e) { // 键盘抬起console.log('键盘抬起', e)socket.send(JSON.stringify({ type: 1, key: e.keyCode }))}window.onmousedown = function (e) { // 鼠标单击按下console.log('单击按下', e)console.log(newPageX, 'newPageX')console.log(newPageY, 'newPageY')socket.send(JSON.stringify({ type: 5, x: pageX, y: pageY }))}window.onmouseup = function (e) { // 鼠标单击抬起console.log('单击抬起', e)socket.send(JSON.stringify({ type: 6, x: pageX, y: pageY }))}window.oncontextmenu = function (e) { // 鼠标右击console.log('右击', e)e.preventDefault()socket.send(JSON.stringify({ type: 4, x: pageX, y: pageY }))}window.ondblclick = function (e) { // 鼠标双击console.log('双击', e)}window.onmousewheel = function (e) { // 鼠标滚动console.log('滚动', e)const moving = e.deltaY / e.wheelDeltaYsocket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving }))}window.onmousemove = function (e) { // 鼠标移动if (!timer) {timer = setTimeout(function () {console.log("鼠标移动:X轴位置" + e.pageX + ";Y轴位置:" + e.pageY)socket.send(JSON.stringify({ type: 2, x: pageX, y: pageY }))timer = null}, 60)}}}const watchControl = () => { // 监听事件 window.ondragstart = function (e) { // 移除拖动事件 e.preventDefault() } window.ondragend = function (e) { // 移除拖动事件 e.preventDefault() } window.onkeydown = function (e) { // 键盘按下 console.log('键盘按下', e) socket.send(JSON.stringify({ type: 0, key: e.keyCode })) } window.onkeyup = function (e) { // 键盘抬起 console.log('键盘抬起', e) socket.send(JSON.stringify({ type: 1, key: e.keyCode })) } window.onmousedown = function (e) { // 鼠标单击按下 console.log('单击按下', e) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 5, x: pageX, y: pageY })) } window.onmouseup = function (e) { // 鼠标单击抬起 console.log('单击抬起', e) socket.send(JSON.stringify({ type: 6, x: pageX, y: pageY })) } window.oncontextmenu = function (e) { // 鼠标右击 console.log('右击', e) e.preventDefault() socket.send(JSON.stringify({ type: 4, x: pageX, y: pageY })) } window.ondblclick = function (e) { // 鼠标双击 console.log('双击', e) } window.onmousewheel = function (e) { // 鼠标滚动 console.log('滚动', e) const moving = e.deltaY / e.wheelDeltaY socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving })) } window.onmousemove = function (e) { // 鼠标移动 if (!timer) { timer = setTimeout(function () { console.log("鼠标移动:X轴位置" + e.pageX + ";Y轴位置:" + e.pageY) socket.send(JSON.stringify({ type: 2, x: pageX, y: pageY })) timer = null }, 60) } } }const watchControl = () => { // 监听事件 window.ondragstart = function (e) { // 移除拖动事件 e.preventDefault() } window.ondragend = function (e) { // 移除拖动事件 e.preventDefault() } window.onkeydown = function (e) { // 键盘按下 console.log('键盘按下', e) socket.send(JSON.stringify({ type: 0, key: e.keyCode })) } window.onkeyup = function (e) { // 键盘抬起 console.log('键盘抬起', e) socket.send(JSON.stringify({ type: 1, key: e.keyCode })) } window.onmousedown = function (e) { // 鼠标单击按下 console.log('单击按下', e) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 5, x: pageX, y: pageY })) } window.onmouseup = function (e) { // 鼠标单击抬起 console.log('单击抬起', e) socket.send(JSON.stringify({ type: 6, x: pageX, y: pageY })) } window.oncontextmenu = function (e) { // 鼠标右击 console.log('右击', e) e.preventDefault() socket.send(JSON.stringify({ type: 4, x: pageX, y: pageY })) } window.ondblclick = function (e) { // 鼠标双击 console.log('双击', e) } window.onmousewheel = function (e) { // 鼠标滚动 console.log('滚动', e) const moving = e.deltaY / e.wheelDeltaY socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving })) } window.onmousemove = function (e) { // 鼠标移动 if (!timer) { timer = setTimeout(function () { console.log("鼠标移动:X轴位置" + e.pageX + ";Y轴位置:" + e.pageY) socket.send(JSON.stringify({ type: 2, x: pageX, y: pageY })) timer = null }, 60) } } }
现在就可以实现远程控制了,发送的事件类型根据桌面端服务需要什么参数协商好就成,接下来就是处理分辨率适配问题了,解决办法大致就是赋值img图片后拿到他的参数分辨率,然后获取自身浏览器的宽高,除以他的分辨率再乘上自身获取的鼠标坐标就OK了,获取img图片事件需要延迟一下,因为是后面socket连接后才赋值的图片,否则宽高就一直是0,加在watchControl事件里面,发送时坐标也要重新计算。
const watchControl = () => {console.dir(imageRef.value)imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度clientHeight = document.body.offsetHeight......window.onmousedown = function (e) { // 鼠标单击按下console.log('单击按下', e)const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))console.log(newPageX, 'newPageX')console.log(newPageY, 'newPageY')socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY }))}}const watchControl = () => { console.dir(imageRef.value) imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度 imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度 clientHeight = document.body.offsetHeight ...... window.onmousedown = function (e) { // 鼠标单击按下 console.log('单击按下', e) const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY })) } }const watchControl = () => { console.dir(imageRef.value) imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度 imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度 clientHeight = document.body.offsetHeight ...... window.onmousedown = function (e) { // 鼠标单击按下 console.log('单击按下', e) const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY })) } }
现在就几乎大功告成了,坐标稳定发送,获取的也是正确计算出来的,下面再做一些socket加密优化,还有事件优化,集成到项目里面离开时还是要清除所有事件和socket连接
,直接上完整全部代码。
<template><div><img ref="imageRef" class="remote" src="" /></div></template><script setup>import ReconnectingWebSocket from 'reconnecting-websocket'import { Base64 } from 'js-base64'onMounted(() => {createdWebsocket()})const route = useRoute()let socket = nullconst secretKey = 'keyXXXXXXX'const remoteControl = '192.168.1.xxx'const scoketURL = `ws://${remoteControl}:10086/Echo?key=${Base64.encode(secretKey)}`const imageRef = ref()let timer = nullconst clientWidth = document.documentElement.offsetWidthlet clientHeight = nullconst widthCss = (window.innerWidth) + 'px'const heightCss = (window.innerHeight) + 'px'const imgWidth = ref() // 图片宽度const imgHeight = ref() // 图片高度const createdWebsocket = () => {socket = new ReconnectingWebSocket(scoketURL)socket.onopen = function () {console.log('连接已建立')resetHeart()setTimeout(() => {watchControl()}, 500)}socket.onmessage = function (event) {if (event.data instanceof Blob) { // 处理桌面流const data = event.dataconst blob = new Blob([data], { type: 'image/jpg' })imageRef.value.src = window.URL.createObjectURL(blob)} else {const objectDta = JSON.parse(event.data)console.log(objectDta)if (objectDta.type === 100) {resetHeart()}}}socket.onclose = function () {console.log('断开连接')}}const handleMousemove = (e) => { // 鼠标移动if (!timer) {timer = setTimeout(function () {const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))// console.log(newPageX, 'newPageX')// console.log(newPageY, 'newPageY')// console.log('鼠标移动:X轴位置' + e.pageX + ';Y轴位置:' + e.pageY)socket.send(JSON.stringify({ type: 2, x: newPageX, y: newPageY }))timer = null}, 60)}}const handleKeydown = (e) => { // 键盘按下console.log('键盘按下', e)socket.send(JSON.stringify({ type: 0, key: e.keyCode }))}const handleMousedown = (e) => { // 鼠标单击按下console.log('单击按下', e)const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))console.log(newPageX, 'newPageX')console.log(newPageY, 'newPageY')socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY }))}const handleKeyup = (e) => { // 键盘抬起console.log('键盘抬起', e)socket.send(JSON.stringify({ type: 1, key: e.keyCode }))}const handleMouseup = (e) => { // 鼠标单击抬起console.log('单击抬起', e)const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))console.log(newPageX, 'newPageX')console.log(newPageY, 'newPageY')socket.send(JSON.stringify({ type: 6, x: newPageX, y: newPageY }))}const handleContextmenu = (e) => { // 鼠标右击console.log('右击', e)e.preventDefault()const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))console.log(newPageX, 'newPageX')console.log(newPageY, 'newPageY')socket.send(JSON.stringify({ type: 4, x: newPageX, y: newPageY }))}const handleDblclick = (e) => { // 鼠标双击console.log('双击', e)}const handleMousewheel = (e) => { // 鼠标滚动console.log('滚动', e)const moving = e.deltaY / e.wheelDeltaYsocket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving }))}const watchControl = () => { // 监听事件console.dir(imageRef.value)imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度clientHeight = document.body.offsetHeightwindow.ondragstart = function (e) { // 移除拖动事件e.preventDefault()}window.ondragend = function (e) { // 移除拖动事件e.preventDefault()}window.addEventListener('mousemove', handleMousemove)window.addEventListener('keydown', handleKeydown)window.addEventListener('mousedown', handleMousedown)window.addEventListener('keyup', handleKeyup)window.addEventListener('mouseup', handleMouseup)window.addEventListener('contextmenu', handleContextmenu)window.addEventListener('dblclick', handleDblclick)window.addEventListener('mousewheel', handleMousewheel)}let heartTime = null // 心跳定时器实例let socketHeart = 0 // 心跳次数const HeartTimeOut = 20000 // 心跳超时时间let socketError = 0 // 错误次数// socket 重置心跳const resetHeart = () => {socketHeart = 0socketError = 0clearInterval(heartTime)sendSocketHeart()}const sendSocketHeart = () => {heartTime = setInterval(() => {if (socketHeart <= 3) {console.log('心跳发送:', socketHeart)socket.send(JSON.stringify({type: 100,key: 'ping'}))socketHeart = socketHeart + 1} else {reconnect()}}, HeartTimeOut)}// socket重连const reconnect = () => {socket.close()if (socketError <= 3) {clearInterval(heartTime)socketError = socketError + 1console.log('socket重连', socketError)} else {console.log('重试次数已用完的逻辑', socketError)clearInterval(heartTime)}}onBeforeUnmount(() => {socket.close()console.log('组件销毁')window.removeEventListener('mousemove', handleMousemove)window.removeEventListener('keydown', handleKeydown)window.removeEventListener('mousedown', handleMousedown)window.removeEventListener('keyup', handleKeyup)window.removeEventListener('mouseup', handleMouseup)window.removeEventListener('contextmenu', handleContextmenu)window.removeEventListener('dblclick', handleDblclick)window.removeEventListener('mousewheel', handleMousewheel)})</script><style scoped>.remote {width: v-bind(widthCss);height: v-bind(heightCss);}</style><template> <div> <img ref="imageRef" class="remote" src="" /> </div> </template> <script setup> import ReconnectingWebSocket from 'reconnecting-websocket' import { Base64 } from 'js-base64' onMounted(() => { createdWebsocket() }) const route = useRoute() let socket = null const secretKey = 'keyXXXXXXX' const remoteControl = '192.168.1.xxx' const scoketURL = `ws://${remoteControl}:10086/Echo?key=${Base64.encode(secretKey)}` const imageRef = ref() let timer = null const clientWidth = document.documentElement.offsetWidth let clientHeight = null const widthCss = (window.innerWidth) + 'px' const heightCss = (window.innerHeight) + 'px' const imgWidth = ref() // 图片宽度 const imgHeight = ref() // 图片高度 const createdWebsocket = () => { socket = new ReconnectingWebSocket(scoketURL) socket.onopen = function () { console.log('连接已建立') resetHeart() setTimeout(() => { watchControl() }, 500) } socket.onmessage = function (event) { if (event.data instanceof Blob) { // 处理桌面流 const data = event.data const blob = new Blob([data], { type: 'image/jpg' }) imageRef.value.src = window.URL.createObjectURL(blob) } else { const objectDta = JSON.parse(event.data) console.log(objectDta) if (objectDta.type === 100) { resetHeart() } } } socket.onclose = function () { console.log('断开连接') } } const handleMousemove = (e) => { // 鼠标移动 if (!timer) { timer = setTimeout(function () { const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) // console.log(newPageX, 'newPageX') // console.log(newPageY, 'newPageY') // console.log('鼠标移动:X轴位置' + e.pageX + ';Y轴位置:' + e.pageY) socket.send(JSON.stringify({ type: 2, x: newPageX, y: newPageY })) timer = null }, 60) } } const handleKeydown = (e) => { // 键盘按下 console.log('键盘按下', e) socket.send(JSON.stringify({ type: 0, key: e.keyCode })) } const handleMousedown = (e) => { // 鼠标单击按下 console.log('单击按下', e) const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY })) } const handleKeyup = (e) => { // 键盘抬起 console.log('键盘抬起', e) socket.send(JSON.stringify({ type: 1, key: e.keyCode })) } const handleMouseup = (e) => { // 鼠标单击抬起 console.log('单击抬起', e) const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 6, x: newPageX, y: newPageY })) } const handleContextmenu = (e) => { // 鼠标右击 console.log('右击', e) e.preventDefault() const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 4, x: newPageX, y: newPageY })) } const handleDblclick = (e) => { // 鼠标双击 console.log('双击', e) } const handleMousewheel = (e) => { // 鼠标滚动 console.log('滚动', e) const moving = e.deltaY / e.wheelDeltaY socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving })) } const watchControl = () => { // 监听事件 console.dir(imageRef.value) imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度 imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度 clientHeight = document.body.offsetHeight window.ondragstart = function (e) { // 移除拖动事件 e.preventDefault() } window.ondragend = function (e) { // 移除拖动事件 e.preventDefault() } window.addEventListener('mousemove', handleMousemove) window.addEventListener('keydown', handleKeydown) window.addEventListener('mousedown', handleMousedown) window.addEventListener('keyup', handleKeyup) window.addEventListener('mouseup', handleMouseup) window.addEventListener('contextmenu', handleContextmenu) window.addEventListener('dblclick', handleDblclick) window.addEventListener('mousewheel', handleMousewheel) } let heartTime = null // 心跳定时器实例 let socketHeart = 0 // 心跳次数 const HeartTimeOut = 20000 // 心跳超时时间 let socketError = 0 // 错误次数 // socket 重置心跳 const resetHeart = () => { socketHeart = 0 socketError = 0 clearInterval(heartTime) sendSocketHeart() } const sendSocketHeart = () => { heartTime = setInterval(() => { if (socketHeart <= 3) { console.log('心跳发送:', socketHeart) socket.send( JSON.stringify({ type: 100, key: 'ping' }) ) socketHeart = socketHeart + 1 } else { reconnect() } }, HeartTimeOut) } // socket重连 const reconnect = () => { socket.close() if (socketError <= 3) { clearInterval(heartTime) socketError = socketError + 1 console.log('socket重连', socketError) } else { console.log('重试次数已用完的逻辑', socketError) clearInterval(heartTime) } } onBeforeUnmount(() => { socket.close() console.log('组件销毁') window.removeEventListener('mousemove', handleMousemove) window.removeEventListener('keydown', handleKeydown) window.removeEventListener('mousedown', handleMousedown) window.removeEventListener('keyup', handleKeyup) window.removeEventListener('mouseup', handleMouseup) window.removeEventListener('contextmenu', handleContextmenu) window.removeEventListener('dblclick', handleDblclick) window.removeEventListener('mousewheel', handleMousewheel) }) </script> <style scoped> .remote { width: v-bind(widthCss); height: v-bind(heightCss); } </style><template> <div> <img ref="imageRef" class="remote" src="" /> </div> </template> <script setup> import ReconnectingWebSocket from 'reconnecting-websocket' import { Base64 } from 'js-base64' onMounted(() => { createdWebsocket() }) const route = useRoute() let socket = null const secretKey = 'keyXXXXXXX' const remoteControl = '192.168.1.xxx' const scoketURL = `ws://${remoteControl}:10086/Echo?key=${Base64.encode(secretKey)}` const imageRef = ref() let timer = null const clientWidth = document.documentElement.offsetWidth let clientHeight = null const widthCss = (window.innerWidth) + 'px' const heightCss = (window.innerHeight) + 'px' const imgWidth = ref() // 图片宽度 const imgHeight = ref() // 图片高度 const createdWebsocket = () => { socket = new ReconnectingWebSocket(scoketURL) socket.onopen = function () { console.log('连接已建立') resetHeart() setTimeout(() => { watchControl() }, 500) } socket.onmessage = function (event) { if (event.data instanceof Blob) { // 处理桌面流 const data = event.data const blob = new Blob([data], { type: 'image/jpg' }) imageRef.value.src = window.URL.createObjectURL(blob) } else { const objectDta = JSON.parse(event.data) console.log(objectDta) if (objectDta.type === 100) { resetHeart() } } } socket.onclose = function () { console.log('断开连接') } } const handleMousemove = (e) => { // 鼠标移动 if (!timer) { timer = setTimeout(function () { const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) // console.log(newPageX, 'newPageX') // console.log(newPageY, 'newPageY') // console.log('鼠标移动:X轴位置' + e.pageX + ';Y轴位置:' + e.pageY) socket.send(JSON.stringify({ type: 2, x: newPageX, y: newPageY })) timer = null }, 60) } } const handleKeydown = (e) => { // 键盘按下 console.log('键盘按下', e) socket.send(JSON.stringify({ type: 0, key: e.keyCode })) } const handleMousedown = (e) => { // 鼠标单击按下 console.log('单击按下', e) const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY })) } const handleKeyup = (e) => { // 键盘抬起 console.log('键盘抬起', e) socket.send(JSON.stringify({ type: 1, key: e.keyCode })) } const handleMouseup = (e) => { // 鼠标单击抬起 console.log('单击抬起', e) const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 6, x: newPageX, y: newPageY })) } const handleContextmenu = (e) => { // 鼠标右击 console.log('右击', e) e.preventDefault() const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率 const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight)) console.log(newPageX, 'newPageX') console.log(newPageY, 'newPageY') socket.send(JSON.stringify({ type: 4, x: newPageX, y: newPageY })) } const handleDblclick = (e) => { // 鼠标双击 console.log('双击', e) } const handleMousewheel = (e) => { // 鼠标滚动 console.log('滚动', e) const moving = e.deltaY / e.wheelDeltaY socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving })) } const watchControl = () => { // 监听事件 console.dir(imageRef.value) imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度 imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度 clientHeight = document.body.offsetHeight window.ondragstart = function (e) { // 移除拖动事件 e.preventDefault() } window.ondragend = function (e) { // 移除拖动事件 e.preventDefault() } window.addEventListener('mousemove', handleMousemove) window.addEventListener('keydown', handleKeydown) window.addEventListener('mousedown', handleMousedown) window.addEventListener('keyup', handleKeyup) window.addEventListener('mouseup', handleMouseup) window.addEventListener('contextmenu', handleContextmenu) window.addEventListener('dblclick', handleDblclick) window.addEventListener('mousewheel', handleMousewheel) } let heartTime = null // 心跳定时器实例 let socketHeart = 0 // 心跳次数 const HeartTimeOut = 20000 // 心跳超时时间 let socketError = 0 // 错误次数 // socket 重置心跳 const resetHeart = () => { socketHeart = 0 socketError = 0 clearInterval(heartTime) sendSocketHeart() } const sendSocketHeart = () => { heartTime = setInterval(() => { if (socketHeart <= 3) { console.log('心跳发送:', socketHeart) socket.send( JSON.stringify({ type: 100, key: 'ping' }) ) socketHeart = socketHeart + 1 } else { reconnect() } }, HeartTimeOut) } // socket重连 const reconnect = () => { socket.close() if (socketError <= 3) { clearInterval(heartTime) socketError = socketError + 1 console.log('socket重连', socketError) } else { console.log('重试次数已用完的逻辑', socketError) clearInterval(heartTime) } } onBeforeUnmount(() => { socket.close() console.log('组件销毁') window.removeEventListener('mousemove', handleMousemove) window.removeEventListener('keydown', handleKeydown) window.removeEventListener('mousedown', handleMousedown) window.removeEventListener('keyup', handleKeyup) window.removeEventListener('mouseup', handleMouseup) window.removeEventListener('contextmenu', handleContextmenu) window.removeEventListener('dblclick', handleDblclick) window.removeEventListener('mousewheel', handleMousewheel) }) </script> <style scoped> .remote { width: v-bind(widthCss); height: v-bind(heightCss); } </style>
现在就算是彻底大功告成了,加密密钥或者方式还是和对端协商,流畅度和清晰度也不错的,简单办公还是没问题的,和不开会员的向日葵效果差不多,后面的优化方式大致围绕着图片压缩
来做应该能达到更加流畅的效果,如果项目是https的话socket服务也要升级成wss协议,大致就这样,若有不正确的地方还请指正一番??。