Java版远程控制V1.0
syxChina(http://syxchina.cnblogs.com/ )
1背景
本来希望做个远程控制,发布到web服务器,使用浏览器applet远程控制,这样就 可以修改你发布的web项目了,以此为初衷,制作了远程控制V1版本,但发现问题还是比较多的,并且我想到了web服务器只开特定的几个端口,心彻底凉了,并且V1版本使用的是TCP协议,限制比较多,先总结出来,等以后有时间再完善,或者希望感兴趣的朋友继续下去。
2技术实现
最初的构想是被控方使劲的截图发送给控制方,效率方面暂时考虑,先把第一个版本完成。
简单的说就是:被控制端循环的发送本机屏幕截图给控制端,并接收控制端传事的事件数据在本机相对位置做回放;控制端显示接收到的屏幕图片,并将在图片上接受到的事件数据发送给控制端。
2-1 屏幕截图
2-1-1Robot类实现屏幕截图
java.awt.Robot是个很有趣类,提供了全屏截图和事件回放的功能,看代码:
public class TestRobotCaptrueScreenSpeed {
public static void main(String[] args) throws Exception {
new TestRobotCaptrueScreenSpeed().testSpeed(10);
}
public void testSpeed(int times) throws Exception {
Robot robot = new Robot();
Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
Rectangle screen = new Rectangle(size);
long sum = 0;
for(int i=1; i<=times; i+=1) {
long begin = System.currentTimeMillis();
BufferedImage image = robot.createScreenCapture(screen);
long end = System.currentTimeMillis();
sum += (end-begin);
U.debug(U.f("%d,time:%d", i, end- begin));
}
U.debug(U.f("avg:%d", sum/times));
}
}
/**output:
DEBUG:1,time:63
DEBUG:2,time:62
DEBUG:3,time:81
DEBUG:4,time:64
DEBUG:5,time:88
DEBUG:6,time:80
DEBUG:7,time:65
DEBUG:8,time:65
DEBUG:9,time:70
DEBUG:10,time:48
DEBUG:avg:68
*/
从结果可以知道全屏截图也是需要时间的,就一个线程一直截图,按照我机子的配置1s也只能14张图片...
也许你会想到多线程使用robot截图,把我们的main方法稍微修改下就可以:
public static void main(String[] args) throws Exception {
for(int i=0; i<10; i++)
new Thread() {
public void run() {
try {
new TestRobotCaptrueScreenSpeed().testSpeed(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
DEBUG:avg:388
DEBUG:avg:389
DEBUG:avg:388
DEBUG:avg:390
DEBUG:avg:388
DEBUG:avg:392
DEBUG:avg:405
DEBUG:avg:409
DEBUG:avg:405
DEBUG:avg:413
看来多线程下硬件条件有线,成效也不是很大啊,测试可以知道2~3个线程速度和cpu占有是比较合适,实际上没有太大本质的改变!
所以我们需要寻求更快的截图方法!
2-1-2windows 快捷键截图
就是模拟键盘上按下printscreen键,这样剪切板上就会有全屏图片,在获取这个张图片:
/**
* 测试从粘贴板中获取全屏图片
* @author syxChina
*
*/
public class CaptrueScreenFromClip {
public static void main(String[] args) throws Exception {
CaptrueScreenFromClip csfc = new CaptrueScreenFromClip();
csfc.test(10);
}
/**
* 测试times次
* @param times
* @throws Exception
*/
public void test(int times) throws Exception {
long sum = 0;
for(int i=1; i<=times; i+=1) {
long begin = System.currentTimeMillis();
createImage();
long end = System.currentTimeMillis();
sum += (end-begin);
U.debug(U.f("%d,time:%d", i, end- begin));
}
U.debug(U.f("avg:%d", sum/times));
}
/**
* 获取全屏截图
* @return
* @throws Exception
*/
public Image createImage() throws Exception {
Robot robot = new Robot();
robot.keyPress(KeyEvent.VK_PRINTSCREEN);
robot.keyRelease(KeyEvent.VK_PRINTSCREEN);
Thread.sleep(100);//不设置一个时间,会抛异常
Image image = getImageFromClipboard();
return image;
}
/**
* 返回粘贴板中的图片
* @return
* @throws Exception
*/
public Image getImageFromClipboard() throws Exception {
Clipboard sysc = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable cc = sysc.getContents(null);
if (cc == null)
return null;
else if (cc.isDataFlavorSupported(DataFlavor.imageFlavor))
return (Image) cc.getTransferData(DataFlavor.imageFlavor);
return null;
}
}
DEBUG:1,time:363
DEBUG:2,time:215
DEBUG:3,time:256
DEBUG:4,time:215
DEBUG:5,time:209
DEBUG:6,time:208
DEBUG:7,time:208
DEBUG:8,time:261
DEBUG:9,time:208
DEBUG:10,time:272
DEBUG:avg:241
代码中我们sleep(100),所以就算减去这个100,241-100=141,比我们的用robot效率低多了,并且这种方法在使用多线程时同步控制比较麻烦!
2-1-3 图片压缩
我们需要把图片压缩了之后发送,经测试,使用不同的压缩类库,压缩成不同的格式,在耗时和耗资源上差别还是比较明显的,不得不说下。
2-1-3-1 使用ImageIO压缩和JPEGEncoder压缩比较
public class TestImageZipSpeed {
public static void main(String[] args) throws Exception {
U.error("ImageIO测试:");
new TestImageZipSpeed().testImageIOSpeed(10);
U.error("JPEGEncoder测试:");
new TestImageZipSpeed().testJPEGEncoderSpeed(10);
}
public void testImageIOSpeed(int times) throws Exception {
Robot robot = new Robot();
Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
Rectangle screen = new Rectangle(size);
BufferedImage image = robot.createScreenCapture(screen);
String[] extArray = { "jpeg", "gif", "jpg", "png", "bmp" };
for (String ext : extArray) {
long sum = 0;
for (int i = 1; i <= times; i += 1) {
long begin = System.currentTimeMillis();
//BufferedImage image = robot.createScreenCapture(screen);
saveImage(image, ext, "C:/Users/syxChina/Desktop/test/screen." + ext);
long end = System.currentTimeMillis();
sum += (end - begin);
// U.debug(U.f("%d,ext=%s,time:%d", i,ext ,end- begin));
}
U.debug(U.f("%s,avg:%d", ext, sum / times));
}
}
public static void saveImage(RenderedImage image, String ext, String path) throws Exception {
FileOutputStream fos = new FileOutputStream(path);
ImageIO.write(image, ext, fos);
fos.flush();
fos.close();
}
public void testJPEGEncoderSpeed(int times) throws Exception {
Robot robot = new Robot();
Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
Rectangle screen = new Rectangle(size);
BufferedImage image = robot.createScreenCapture(screen);
long sum = 0;
for (int i = 1; i <= times; i += 1) {
long begin = System.currentTimeMillis();
//BufferedImage image = robot.createScreenCapture(screen);
saveJPEG(image, "C:/Users/syxChina/Desktop/test/screen.jpg");
long end = System.currentTimeMillis();
sum += (end - begin);
}
U.debug(U.f("jpgencoder,avg:%d", sum / times));
}
public static void saveJPEG(BufferedImage image, String path) throws Exception {
FileOutputStream out = new FileOutputStream(path);
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
encoder.encode(image);
out.close();
}
}
结果:
ERROR:ImageIO测试:
DEBUG:jpeg,avg:174
DEBUG:gif,avg:762
DEBUG:jpg,avg:140
DEBUG:png,avg:437
DEBUG:bmp,avg:277
ERROR:JPEGEncoder测试:
DEBUG:jpgencoder,avg:65
2-1-3-2 结果
从结果中可以看出,JPEGEncoder在生成大小和时间上有绝对的优势,所以我们使用这个方法压缩我们传输的图片!
如果您有好的方法,请分享给我!
2-1-4 总结
目前java下这2种方法比较常见,下次试试用JNI方法看看windows下速度有多块,但用robot用50ms感觉应该可以接受的!
所以我们使用Robot做全屏截图!如果您有更好的方法,希望你分享给我!
2-2 远程控制
2-2-1 使用Robot回放事件
见识下Robot的键盘和鼠标功能吧!
public class RobotTest {
// Robot使用示例
public static void main(String[] args) throws Exception {
java.awt.Robot robot = new java.awt.Robot();// 创建一个机器人对象
// 取得当前屏幕大小
Toolkit tk = java.awt.Toolkit.getDefaultToolkit();
java.awt.Dimension dm = tk.getScreenSize();
// 计算屏幕中心点
int x = (int) dm.getWidth() / 2;
int y = (int) dm.getHeight() / 2;
robot.mouseMove(x, y);// 将鼠标移动到屏幕中心
robot.mousePress(InputEvent.BUTTON1_MASK);// 按下鼠标左键
robot.mouseRelease(InputEvent.BUTTON1_MASK);// 松开鼠标左键
robot.keyPress(KeyEvent.VK_ENTER); // 模拟按下回车键
robot.keyRelease(KeyEvent.VK_ENTER);
robot.keyPress(KeyEvent.VK_SHIFT);// 按下SHIFT键
for (int i = 0; i < 10; i++) {
robot.keyPress('A' + i); // 在屏幕上打字
robot.keyRelease('A' + i);
Thread.sleep(500);
}
robot.keyRelease(KeyEvent.VK_SHIFT);// 松开SHIFT键
for (int i = 0; i < 11; i++) {// 将刚才输入的内容删除掉
robot.keyPress(KeyEvent.VK_BACK_SPACE);
robot.keyRelease(KeyEvent.VK_BACK_SPACE);
Thread.sleep(500);
}
robot.mousePress(KeyEvent.VK_BACK_SPACE);
robot.mouseRelease(KeyEvent.VK_BACK_SPACE);
}
}
所以只要我们用Robot在被控段执行鼠标和键盘事件,那么基本就可以了!
2-2-2 总结
经测试使用robot回放事件效果还是不错的,就算直接把Event发送到对象也就占几KB的流量,在使用TCP协议,效果可以接受的。
如果您有更好的方法,欢迎告诉我下!
3 具体实现
3-1被控制端
这里我们使用tcp的连接,后期有时间再升级。被控制端相当于一个ServerSocket来监听控制端请求,每当一个请求到来,控制端启动2个线程,一个是把被控制端画面传送给控制端,一个是把控制端控制信息发送给被控制端。
/**
* 服务器(被控制端)
* @author syxChina
*
*/
public class RCServer {
private static RCServer rcs = new RCServer();
public static void main(String[] args) throws Exception {
U.debug("start Remote Control Server...");
rcs.startServer(18080);
}
/**
* 根据特定端口启动服务器
* @param port
* @throws Exception
*/
public void startServer(int port) throws Exception {
U.debug(U.f("run server in port:%d", port));
ServerSocket ss = new ServerSocket(port);;
while (true) {
U.debug("Remote Control Server wait client...");
Socket client = ss.accept();
U.debug(U.f("a client[%s:%d] connect!", client.getLocalAddress(), client.getPort()));
InputStream in = client.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
OutputStream os = client.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
U.debug("socket open stream ok!");
ControlThread cont = new ControlThread(ois);
cont.start();//启动控制线程
CaptureThread capt = new CaptureThread(dos);
capt.start();//启动屏幕传输线程
}
}
public int stopServer() {
return 0;
}
}
/**
* 控制线程
* @author syxChina
*
*/
public class ControlThread extends Thread {
private ObjectInputStream ois;
private Robot robot;
public ControlThread(ObjectInputStream ois) {
this.ois = ois;
}
@Override
public void run() {
try {
robot = new Robot();
} catch (AWTException e1) {
}
while (true) {
try {
Object event = ois.readObject();
InputEvent e = (InputEvent) event;
actionEvent(e);
} catch (Exception e) {
U.debug("ControlThread over!");
return;
}
}
}
private void actionEvent(InputEvent e) throws Exception {
if (e instanceof java.awt.event.KeyEvent) {
KeyEvent ke = (KeyEvent) e;
int type = ke.getID();
if (type == java.awt.Event.KEY_PRESS) {
robot.keyPress(ke.getKeyCode());
}
if (type == java.awt.Event.KEY_RELEASE) {
robot.keyRelease(ke.getKeyCode());
}
}
if (e instanceof java.awt.event.MouseEvent) {
MouseEvent me = (MouseEvent) e;
int type = e.getID();
if (type == java.awt.Event.MOUSE_DOWN) {
robot.mousePress(getMouseClick(me.getButton()));
}else if (type == java.awt.Event.MOUSE_UP) {
robot.mouseRelease(getMouseClick(me.getButton()));
}else if (type == java.awt.Event.MOUSE_MOVE) {
robot.mouseMove(me.getX(), me.getY());
} else if(type == Event.MOUSE_DRAG) {
robot.mouseMove(me.getX(), me.getY());
}
}
}
/**
* 根据发送事的Mouse事件对象,转变为通用的Mouse按键代码
* @param button
* @return
*/
private int getMouseClick(int button) {
if (button == MouseEvent.BUTTON1) {
return InputEvent.BUTTON1_MASK;
}
if (button == MouseEvent.BUTTON2) {
return InputEvent.BUTTON2_MASK;
}
if (button == MouseEvent.BUTTON3) {
return InputEvent.BUTTON3_MASK;
}
return -1;
}
}
/**
* 屏幕传输线程
* @author syxChina
*
*/
public class CaptureThread extends Thread {
public static final int DPS = 20;//设置的dps,未用
public static final int THREAD_NUM = 5;//画面传输线程,未用
private DataOutputStream dos;//管道
private Robot robot;//robot
public CaptureThread(DataOutputStream dos) {
this.dos = dos;
}
@Override
public void run() {
try {
robot = new Robot();
} catch (AWTException e1) {
e1.printStackTrace();
}
final Toolkit tk = java.awt.Toolkit.getDefaultToolkit();
final Dimension dm = tk.getScreenSize();
final Rectangle rec = new Rectangle(dm);
try {
dos.writeDouble(dm.getHeight());
dos.writeDouble(dm.getWidth());
dos.flush();
} catch (IOException e1) {
U.error(U.f("send screen size[%dx%d] error!", dm.getHeight(), dm.getWidth()));
return;
}
while(true) {
try {
long begin = System.currentTimeMillis();
byte[] data = createImage(rec);
dos.writeInt(data.length);
dos.write(data);
dos.flush();
long end = System.currentTimeMillis();
U.debug(U.f("time=%d,size=%d", end-begin, data.length));
if((end-begin) < 1000/DPS) {
Thread.sleep(1000/DPS - (end-begin));
}
} catch (Exception e) {
U.debug("CaptrueThread over!");
return;
}
}
}
private byte[] createImage(Rectangle rec) throws IOException {
BufferedImage bufImage = robot.createScreenCapture(rec);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos);
encoder.encode(bufImage);
return baos.toByteArray();
}
}
3-2控制端
控制端相对被控制端轻松多了,主要做2件事,1发送控制信息,2接受控制端屏幕。
详见代码:
/**
* 控制端
* @author syxChina
*
*/
public class RCClient {
private ClientUI clientUI ;
private DataInputStream dis;
private ObjectOutputStream oos;
private Socket client;
/**
* 连接被控制端
* @param host
* @param port
* @return
*/
public int connect(String host, int port) {
int retCode = 0;
try {
client = new Socket(host, port);
U.debug(client);
oos = new ObjectOutputStream(client.getOutputStream());
dis = new DataInputStream(client.getInputStream());
U.debug("client open stream ok!");
} catch (UnknownHostException e) {
retCode = 1;
} catch (IOException e) {
retCode = 2;
}
return retCode;
}
/**
* 显示图形界面
* @throws Exception
* @throws ClassNotFoundException
*/
public void showClientUI() throws Exception, ClassNotFoundException {
clientUI = new ClientUI(dis, oos);
U.debug("start client UI");
clientUI.updateSize(readServerSize());
while(true) {
long begin = System.currentTimeMillis();
byte[] imageData = readBytes();
clientUI.update(imageData);
long end = System.currentTimeMillis();
U.debug(U.f("time=%d,size=%d", end-begin, imageData.length));
}
}
/**
* 读被控制段发送来的数据
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public byte[] readBytes() throws IOException, ClassNotFoundException {
int len = dis.readInt();
byte[] data = new byte[len];
dis.readFully(data);
return data;
}
/**
* 读被控制端分辨率
* @return
*/
public Dimension readServerSize() {
double height = 100;
double width = 100;
try {
height = dis.readDouble();
width = dis.readDouble();
} catch (IOException e) {
U.debug("read server SIZE error!");
}
return new Dimension((int)width, (int)height);
}
public static void main(String[] args) throws Exception {
String input = JOptionPane.showInputDialog("请输入要连接的服务器(192.168.0.2:18080):");
if(input == null) {
return;
}
Pattern pattern = Pattern.compile("(\\d+.\\d+.\\d+.\\d+):(\\d+)");
java.util.regex.Matcher m = pattern.matcher(input);
if(!m.matches()) {
return;
}
String host = m.group(1);
int port = Integer.parseInt(m.group(2));
RCClient rcc = new RCClient();
rcc = new RCClient();
U.debug(U.f("run client , connect server in [%s:%d]", host, port));
int retCode = rcc.connect(host, port);//连接指定的被控制端
if (retCode != 0) {
U.error(U.f("connect server[%s:%d] error!app exit!", host, port));
return;
}
try {
rcc.showClientUI();
} catch (Exception e) {
U.error("disconnect with server!");
}
}
}
/**
* 控制端界面
* @author syxChina
*
*/
public class ClientUI extends JFrame {
private DataInputStream dis;//接受被控制端发来的图片
private ObjectOutputStream oos;//发送控制事件
private JLabel backImage;//此本版使用一个JLable显示图片
public ClientUI(DataInputStream dis, ObjectOutputStream oos) {
this();
this.dis = dis;
this.oos = oos;
}
/**
* 根据图片数据更新控制端界面
* @param imageData
*/
public void update(byte[] imageData) {
ImageIcon image = new ImageIcon(imageData);
backImage.setIcon(image);
this.repaint();
}
public void updateSize(Dimension client) {
Dimension clientSize = getScreenSize();
double width = 0, height = 0;
if (clientSize.getWidth() >= client.getWidth()) {
width = client.getWidth()+60;
} else {
width = clientSize.getWidth();
}
if((clientSize.getHeight()-client.getHeight()) > 0) {
height = client.getHeight() + 60;
} else {
height = clientSize.getHeight();
}
setSize((int)width, (int)height);
}
private ClientUI() {
setDefaultCloseOperation(3 );
setSize(1050, 800);
backImage = new JLabel();
JPanel pane = new JPanel();
JScrollPane scrollPane = new JScrollPane(pane);
pane.setLayout(new FlowLayout());
pane.add(backImage);
add(scrollPane);
addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
sendEventObject(e);
}
public void keyReleased(KeyEvent e) {
sendEventObject(e);
}
public void keyTyped(KeyEvent e) {
}
});
addMouseWheelListener(new MouseWheelListener() {
public void mouseWheelMoved(MouseWheelEvent e) {
sendEventObject(e);
}
});
backImage.addMouseMotionListener(new MouseMotionListener() {
public void mouseDragged(MouseEvent e) {
sendEventObject(e);
}
public void mouseMoved(MouseEvent e) {
if(Math.random()>0.8)
sendEventObject(e);
}
});
backImage.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
sendEventObject(e);
}
public void mouseReleased(MouseEvent e) {
sendEventObject(e);
}
});
this.setVisible(true);
}
/**
* 发送事件
* @param event
*/
private void sendEventObject(java.awt.event.InputEvent event) {
try {
oos.writeObject(event);
} catch (Exception ef) {
ef.printStackTrace();
}
}
public Dimension getScreenSize() {
return Toolkit.getDefaultToolkit().getScreenSize();
}
}
本来用Log4J的,但eclipse打包的时候不是很方便,所以自己写了个简单工具类的:
public final class U {
public static String f(String str, Object ...os) {
return String.format(str, os);
}
public static void debug(Object message) {
System.out.println("DEBUG:"+message.toString());
}
public static void info(Object message) {
System.out.println("INFO :"+message.toString());
}
public static void error(Object message) {
System.err.println("ERROR:"+message.toString());
}
}
3-3 总结
效果图:
首先运行服务器(被控方):
运行客服端(控制端):
占用带宽:
在局域网中应该是可以使用的,我用了2M的带宽的电信网,测试也是比较流畅的!
4 总结
第一个版本基本完成,只是实现了最基本的功能---监控和远控,当然很粗糙的一个版本还有需要需要改进的,因为使用TCP的方法,所以使用的双方需要在同一个内网或者外网,网上说可以使用UDP打洞穿通内网?!等有时间再可以尝试下。在屏幕传输上还有很大的改动空间,首先可以先把压缩成gif再传输(因为gif压缩太耗时,本人测试的时候1440x900,使用ImageIO压缩要耗700ms,而压缩jpg,jpeg,bmp等都很耗时,所以选择了JPEGImageEncoder),使用多线程发送图片(在局域网中带宽不是问题),使用局部发送方法(可以把屏幕分成mxn个快,只有当快中内容改变时才发送相应的快),希望感兴趣的朋友帮我完善下一个版本,需要源代码的EMAIL!
参考文章:
蓝杰java远程控制实现