# springboot-websocket-remote-windows **Repository Path**: hejun-fork/springbootwebsockettest ## Basic Information - **Project Name**: springboot-websocket-remote-windows - **Description**: Springboot中使用Robot及Websocket实现windows远程桌面控制 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 13 - **Created**: 2022-10-30 - **Last Updated**: 2023-03-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Springboot及Websocket实现windows远程桌面控制 ### 一、背景说明 最近在一个项目中用到了通过Web进行windows远程桌面访问的功能,使用了Apache Guacamole来进行实现,见我另一篇:通过浏览器html5操作Windows远程桌面,linux,记Apache Guacamole的安装与使用,达到了项目目标。 想自己简单实现一个springboot项目开箱即用的简单远程桌面示例,想了下自己通过Jdk中的Robot类进行远程桌面的截图,通过websocket发送给web前端界面展示,同时监听web界面上的按键操作,通过websocket发送到后台,通过Robot类进行键盘事件的重放,来达到远程桌面的效果。 ### 二、实现过程 #### 1.先进行Robot类进行截图的单元测试 1.先进行Robot类进行截图的单元测试 ``` java 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支持 ``` org.springframework.boot spring-boot-starter-websocket ``` #### 3.在springboot工程启动时开启定时任务进行截图抓取任务的启动 ``` package cn.gzsendi; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import cn.gzsendi.websocket.service.RobotService; //测试地址:http://ip:8081/remotewin?accessToken=123456 //或者 http://ip:8081/page/remotewin.html?accessToken=123456 //可以在虚机里面启动服务,然后在电脑本机进行测试 //注意:本程序要求服务器不能在本机, 不能在本地127.0.0.1测试,在本地电脑测试会产生镜中镜的效果,无限下去。 @SpringBootApplication public class WebsocketApplicationStarter { public static void main(String[] args) { SpringApplicationBuilder builder = new SpringApplicationBuilder(WebsocketApplicationStarter.class); ConfigurableApplicationContext ctx = builder.headless(false).run(args); //服务端开启定时抓取截图并发给客户端的处理 RobotService robotService = ctx.getBean(RobotService.class); robotService.startCaputureTask(); } } ``` #### 4.RobotService类中进行截图任务代码编写 ``` /** * 进行截图任务的处理,如果有客户端连接上来,将进行截图并广播发送给所有的客户端 */ public void startCaputureTask(){ while(true){ try { //100毫秒检查一次,如果有客户端,并且满足需要截图的条件,就截图一张发给所有的客户端,可以调整这个值,值越小延迟越小 Thread.sleep(100l); //遍历所有在线的客户端 Map 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 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 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属性,达到修改截图的效果,同时,要监听键盘及鼠标事件,发送到后台进行回放。 -- image标签,用于远程桌面的截图显示 ![输入图片说明](doc/1.png) -- 收到后端的截图数据时,进行回放显示在web界面上 ![输入图片说明](doc/2.png) -- 处理键盘事件和鼠标事件,发送到后端 ![输入图片说明](doc/3.png) -- 最后增加些校验等,让需要带上accessToken参数才能访问 ![输入图片说明](doc/4.png) -- 后台的校验代码,默认密码为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 ![输入图片说明](doc/5.gif)