Springboot中使用Robot及Websocket实现windows远程桌面控制

@TOC


一、背景说明

最近在一个项目中用到了通过Web进行windows远程桌面访问的功能,使用了Apache Guacamole来进行实现,见我另一篇:通过浏览器html5操作Windows远程桌面,linux,记Apache Guacamole的安装与使用,达到了项目目标。

想自己简单实现一个springboot项目开箱即用的简单远程桌面示例,想了下自己通过Jdk中的Robot类进行远程桌面的截图,通过websocket发送给web前端界面展示,同时监听web界面上的按键操作,通过websocket发送到后台,通过Robot类进行键盘事件的重放,来达到远程桌面的效果。

二、实现过程

1.先进行Robot类进行截图的单元测试

首先写代码进行Robot的单元测试,进行桌面的截图操作。

package cn.gzsendi;

import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;

public class Test {
	
	public static void main(String[] args) throws AWTException, IOException {
		
		Robot robot = new Robot();
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		Dimension dimension = toolkit.getScreenSize();//获取到远程桌面的屏幕大小信息
		Rectangle rectangle = new Rectangle(0, 0, (int)dimension.getWidth(), (int)dimension.getHeight());
		BufferedImage bufferedImage =  robot.createScreenCapture(rectangle);
		FileOutputStream baos = new FileOutputStream(new File("d:/temp/test.jpg"));
		ImageIO.write(bufferedImage, "jpg", baos);
		
	}

}
复制代码

2.新建一个springboot工程,并添加websocket支持

		<!-- websocket start -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- websocket end -->
复制代码

3.在springboot工程启动时开启定时任务进行截图抓取任务的启动

在这里插入图片描述

4.RobotService类中进行截图任务代码编写

进行截图任务的处理,如果有客户端连接上来,将进行截图并广播发送给所有的客户端

	/**
	 * 进行截图任务的处理,如果有客户端连接上来,将进行截图并广播发送给所有的客户端
	 */
	public void startCaputureTask(){
		
		while(true){
		
			try {
				
				//100毫秒检查一次,如果有客户端,并且满足需要截图的条件,就截图一张发给所有的客户端,可以调整这个值,值越小延迟越小
				Thread.sleep(100l);
				
				//遍历所有在线的客户端
				Map<String,WebSocketSession> webSocketSessions  = MyWebSocketHandler.webSocketSessions;
				
				//没有websocket客户端连上的话,直接就退出本轮循环,不需要进行截图处理
				if(webSocketSessions.size() == 0 ) {
					//logger.info("webSocketSessions.size() == 0");
					continue;
				}
				
				//如果超过5秒没有收到键盘或鼠标事件,说明可以停止截图给客户端,节省性能。
				if((System.currentTimeMillis() - lastestActionTime) > 5000){
					//logger.info("exceed 5 seconds not keyboard event arrived, stop send images.");
					continue;
				}
				
				byte[] data = getCapture(robot,rectangle);
				ImageIcon icon = new ImageIcon(data);
				remoteImageWidth = icon.getIconWidth();
				remoteImageHeigth = icon.getIconHeight();
				
				//遍历发送给所有的客户端连接
				for(WebSocketSession webSocketSession : webSocketSessions.values()) {
					if(webSocketSession.isOpen()) {
						webSocketSession.sendMessage(new BinaryMessage(data));
					}
				}
			
			} catch (Exception e) {
				logger.error("startCaputureTaskError",e);
			}
		
		}
		
	}
复制代码

抓图的代码如下

	/**
	 * 得到屏幕截图数据
	 * @return
	 */
	private byte[] getCapture(Robot robot,Rectangle rectangle) {
		
		BufferedImage bufferedImage =  robot.createScreenCapture(rectangle);
		
		//获得一个内存输出流
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		
		//将图片数据写入内存流中
		try {
			
			//原始图片,现在用下面的压缩图片法替换了
			ImageIO.write(bufferedImage, "jpg", baos);
			
			//进行图片压缩,图片尺寸不变,压缩图片文件大小outputQuality实现,参数1为最高质量
			//Thumbnails.of(bufferedImage).scale(1f).outputQuality(0.25f).outputFormat("jpg").toOutputStream(baos);
			
		} catch (IOException e) {
			logger.error("图片写入出现异常",e);
		}
		
		return baos.toByteArray();
	}
复制代码

5.MyWebSocketHandler中进行客户端键盘事件的处理

在MyWebSocketHandler类中,回放处理客户端发送过来的键盘或鼠标事件,在服务端这边重新执行一遍robotService.actionEvent(playload);

    /**
     * @Description: 收到消息的回调
     * @Param: [webSocketSession, webSocketMessage]
     * @return: void
     */
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
    	
    	//设置更新最后一后键盘或鼠标事件的到达时间
    	robotService.setLastestActionTime(System.currentTimeMillis());
        
    	if (webSocketMessage instanceof TextMessage) {
    		
    		//logger.info("用户:{},发送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
    		Map<String,Object> playload = JsonUtil.castToObject(webSocketMessage.getPayload().toString());
    		
    		//回放处理客户端发送过来的键盘或鼠标事件,在服务端这边重新执行一遍
    		robotService.actionEvent(playload);
    		
    	} else if (webSocketMessage instanceof BinaryMessage) {
        	
        	

        } else if (webSocketMessage instanceof PongMessage) {
        	
        	

        } else {
            logger.error("Unexpected WebSocket message type: " + webSocketMessage);
        }
    }
复制代码

根据前端传过来的事件类型来决定如何重放键盘或鼠标事件,mousedown表示鼠标按下事件,mouseup表示鼠标弹开事件,mousemove表示鼠标移动事件,keydown表示键盘按下,keyup表示键盘弹开事件。

	//回放处理客户端发送过来的键盘或鼠标事件
	public void actionEvent(Map<String,Object> playload){
		
		String openType = JsonUtil.getString(playload, "openType");
		
		if("mousedown".equals(openType)){
			
			//鼠标按下事件
			logger.info("鼠标按下事件,{}",JsonUtil.toJSONString(playload));
			
			int clientX = JsonUtil.getInteger(playload, "clientX");
    		int clientY = JsonUtil.getInteger(playload, "clientY");
    		int button = JsonUtil.getInteger(playload, "button");
    		int imageWidth = JsonUtil.getInteger(playload, "imageWidth");
    		int imageHeight = JsonUtil.getInteger(playload, "imageHeight");
    		
    		//这里为什么要这样转?说明如下:
    		//假如浏览器的image区域为1200*800,远程桌面的截图区为900*700
    		//那么在浏览器上点击了clientX=77,clientY=88这个坐标时,实际上在远程
    		//桌面上正确的坐标应该为:
    		//remoteClientX = clientX * remoteImageWidth/imageWidth;
    		//即:remoteClientX = 77 * 900 / 1200
    		//remoteClientY同理.
    		int remoteClientX = clientX * remoteImageWidth/imageWidth;
    		int remoteClientY = clientY * remoteImageHeigth/imageHeight;
    		
    		//移动鼠标到正确的坐标
    		robot.mouseMove( remoteClientX , remoteClientY );
    		
    		//然后进行鼠标的按下
    		if(button == 0) {
    			robot.mousePress(InputEvent.BUTTON1_MASK);//左键
    		}else if(button == 1) {
    			robot.mousePress(InputEvent.BUTTON2_MASK);//中间键
    		}else if(button == 2) {
    			robot.mousePress(InputEvent.BUTTON3_MASK);//右键
    		}
    		
			
		}else if("mouseup".equals(openType)){
			
			//鼠标弹开事件
			logger.info("鼠标弹开事件,{}",JsonUtil.toJSONString(playload));
			
			int clientX = JsonUtil.getInteger(playload, "clientX");
    		int clientY = JsonUtil.getInteger(playload, "clientY");
    		int button = JsonUtil.getInteger(playload, "button");
    		int imageWidth = JsonUtil.getInteger(playload, "imageWidth");
    		int imageHeight = JsonUtil.getInteger(playload, "imageHeight");
    		int remoteClientX = clientX*remoteImageWidth/imageWidth;
    		int remoteClientY = clientY*remoteImageHeigth/imageHeight;
    		
    		//移动鼠标到正确的坐标
    		robot.mouseMove( remoteClientX , remoteClientY );
			
    		//然后进行鼠标的弹起
    		if(button == 0) {
    			robot.mouseRelease(InputEvent.BUTTON1_MASK);//左键
    		}else if(button == 1) {
    			robot.mouseRelease(InputEvent.BUTTON2_MASK);//中间键
    		}else if(button == 2) {
    			robot.mouseRelease(InputEvent.BUTTON3_MASK);//右键
    		}
    		
			
		}else if("mousemove".equals(openType)){
			
			//鼠标移动事件
			
			int clientX = JsonUtil.getInteger(playload, "pageX");
    		int clientY = JsonUtil.getInteger(playload, "pageY");
    		int imageWidth = JsonUtil.getInteger(playload, "imageWidth");
    		int imageHeight = JsonUtil.getInteger(playload, "imageHeight");
    		int remoteClientX = clientX*remoteImageWidth/imageWidth;
    		int remoteClientY = clientY*remoteImageHeigth/imageHeight;
    		
    		//将鼠标进行移动
    		robot.mouseMove( remoteClientX , remoteClientY );
    		
		}else if("keydown".equals(openType)){
			
			//键盘按下事件
			logger.info("键盘按下事件,{}",JsonUtil.toJSONString(playload));
			
			int keyCode = JsonUtil.getInteger(playload, "keyCode");
			robot.keyPress(changeKeyCode(keyCode));
			
		}else if("keyup".equals(openType)){
			
			//键盘弹开事件
			logger.info("键盘弹开事件,{}",JsonUtil.toJSONString(playload));
			int keyCode = JsonUtil.getInteger(playload, "keyCode");
			robot.keyRelease(changeKeyCode(keyCode));
		}
		
	}
	
复制代码

进行键盘按键回放的时候要做一些特殊处理,进行keyCode的改变,因为浏览器的键盘事件和Java的awt的事件代码,有些是不一样的,需要进行转换

//进行keyCode的改变,因为浏览器的键盘事件和Java的awt的事件代码,有些是不一样的,需要进行转换,
	//比如浏览器中13表示回车,但在Java的awt中是用10表示
	//这里可能转换不全,比如F1-F12键都没有处理,因为浏览器现在没有禁用这些键,如果需要支持,可以继续在这里加上
	private int changeKeyCode(int sourceKeyCode){
		
		//回车
		if(sourceKeyCode == 13) return 10;
		
		//,< 188 -> 44
		if(sourceKeyCode == 188) return 44;
		
		//.>在Js中为190,但在Java中为46
		if(sourceKeyCode == 190) return 46;
		
		// /?在Js中为191,但在Java中为47
		if(sourceKeyCode == 191) return 47;
		
		//;: 186 -> 59
		if(sourceKeyCode == 186) return 59;
		
		//[{ 219 -> 91
		if(sourceKeyCode == 219) return 91;
		
		//\| 220 -> 92
		if(sourceKeyCode == 220) return 92;
		
		//-_ 189->45
		if(sourceKeyCode == 189) return 45;
		
		//=+ 187->61
		if(sourceKeyCode == 187) return 61;
		
		//]} 221 -> 93
		if(sourceKeyCode == 221) return 93;
		
		//DEL
		if(sourceKeyCode == 46) return 127;
		
		//Ins
		if(sourceKeyCode == 45) return 155;
		
		return sourceKeyCode;
	}
复制代码

6.前端代码的实现

前端通过直接在html里面放一个image标签就行了,然后通过浏览器进行websocket连接到后端服务,然后发现有截图数据进来,就修改image的src属性,达到修改截图的效果,同时,要监听键盘及鼠标事件,发送到后台进行回放。

  1. image标签,用于远程桌面的截图显示

在这里插入图片描述 2. 收到后端的截图数据时,进行回放显示在web界面上 在这里插入图片描述 3. 处理键盘事件和鼠标事件,发送到后端 在这里插入图片描述 4. 最后增加些校验等,让需要带上accessToken参数才能访问 在这里插入图片描述 后台的校验代码,默认密码为123456,如果需要实现复杂点的密码验证,如存到数据里面等,可以修改这里的逻辑

package cn.gzsendi.web.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tokenController")
public class TokenController {
	
	@Value("${accessToken:123456}")
    private String accessToken;
	
	@GetMapping("/check")
	public String check(String accessToken){
		return this.accessToken.equals(accessToken) ? "success" :"fail";
	}

}
复制代码

三、效果演示

访问地址:http://192.168.0.103:8081/remotewin?accessToken=123456 在这里插入图片描述

四、源代码提供

github:   https://github.com/jxlhljh/springbootwebsockettest.git
gitee:  https://gitee.com/jxlhljh/springbootwebsockettest.git
复制代码

Supongo que te gusta

Origin juejin.im/post/7067396611894657061
Recomendado
Clasificación