现在的位置: 首页 > 综合 > 正文

Java版远程控制V1.0

2012年11月13日 ⁄ 综合 ⁄ 共 14815字 ⁄ 字号 评论关闭

Java版远程控制V1.0

syxChina(http://syxchina.cnblogs.com/ )

Java版远程控制 

1背景 

2技术实现 

2-1 屏幕截图 

2-2 远程控制 

3 具体实现 

3-1被控制端 

3-2控制端 

3-3 总结 

4 总结 

1背景

本来希望做个远程控制,发布到web服务器,使用浏览器applet远程控制,这样就 可以修改你发布的web项目了,以此为初衷,制作了远程控制V1版本,但发现问题还是比较多的,并且我想到了web服务器只开特定的几个端口,心彻底凉了,并且V1版本使用的是TCP协议,限制比较多,先总结出来,等以后有时间再完善,或者希望感兴趣的朋友继续下去。

2技术实现

最初的构想是被控方使劲的截图发送给控制方,效率方面暂时考虑,先把第一个版本完成。

wps_clip_image-25140

简单的说就是:被控制端循环的发送本机屏幕截图给控制端,并接收控制端传事的事件数据在本机相对位置做回放;控制端显示接收到的屏幕图片,并将在图片上接受到的事件数据发送给控制端。

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 总结

效果图:

首先运行服务器(被控方):

wps_clip_image-31799

运行客服端(控制端):

wps_clip_image-6473

wps_clip_image-3442

占用带宽:

wps_clip_image-26828

在局域网中应该是可以使用的,我用了2M的带宽的电信网,测试也是比较流畅的!

4 总结

第一个版本基本完成,只是实现了最基本的功能---监控和远控,当然很粗糙的一个版本还有需要需要改进的,因为使用TCP的方法,所以使用的双方需要在同一个内网或者外网,网上说可以使用UDP打洞穿通内网?!等有时间再可以尝试下。在屏幕传输上还有很大的改动空间,首先可以先把压缩成gif再传输(因为gif压缩太耗时,本人测试的时候1440x900,使用ImageIO压缩要耗700ms,而压缩jpg,jpeg,bmp等都很耗时,所以选择了JPEGImageEncoder),使用多线程发送图片(在局域网中带宽不是问题),使用局部发送方法(可以把屏幕分成mxn个快,只有当快中内容改变时才发送相应的快),希望感兴趣的朋友帮我完善下一个版本,需要源代码的EMAIL!

参考文章:

Google

蓝杰java远程控制实现

屏幕监控的一种图像压缩传输方法

Java实现远程屏幕监视

抱歉!评论已关闭.