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远程控制实现