Springboot及Websocket实现windows远程桌面控制
一、背景说明
最近在一个项目中用到了通过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属性,达到修改截图的效果,同时,要监听键盘及鼠标事件,发送到后台进行回放。
image标签,用于远程桌面的截图显示
收到后端的截图数据时,进行回放显示在web界面上
处理键盘事件和鼠标事件
,发送到后端
- 最后增加些校验等,让需要带上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