远程桌面控制的产品已经有不少不少,我作此项目的初衷并非要开发出一个商用的产品,只是出于兴趣爱好,作一个开源的项目,以前也没有阅读过任何远程桌面控制的项目源码,只是根据本身已有的经验设计开发,确定有许多不足,有兴趣的朋友能够留言讨论与支持。html
通常须要远程控制的场景发生在公司和家之间,因为公司和家里的电脑通常都在局域网内,因此不能直接相连,须要第三方中转,因此至少有三方,以下图。java
负责中转的第三方是服务器,控制端和傀儡端(被控制端)相对于服务器来讲都是客户端,都和服务器直接相连,也就是说控制端不和傀儡端相连。git
约定:github
- 控制端M(Master)
- 服务器S(Server)
- 傀儡端P(Puppet)
为了叙述方便,如下如不作特别说明,M表示控制端,S表示服务端,P表示傀儡端。spring
若是要达到控制傀儡的目的,应该怎么作呢?三方之间至少要发生什么交互呢?数组
控制端、傀儡端的接收器和服务器中的转发器都是一个,为便于流程的清晰,分开画了。
能够看出三者交互主要经过命令形式(命令能够带数据也能够不带数据),发送、转发、接收命令,而后作出相应的动做。
从上图中看到,服务端不只须要转数据,还须要记录存活的傀儡以及维护控制端和傀儡之间的关系,其实还得处理一些异常状况,好比远程过程当中,傀儡断开,过一会又链接上,傀儡是否须要继续给控制端发送屏幕截图。服务器
粗粒度分一下,能够分为三层:Desktop层负责UI处理,CommandHandler层负责命令处理,Netty网络层负责数据的网络传输。网络
具体来看一下commandHandler层:mvc
CommandHandlerLoader工具类会根据Netty或Desktop层传入的Command到配置文件commandhandlers中查找对应的处理类,动态加载,而后进行逻辑处理,这样对于后期命令添加是很是方便的,命令与命令之间,以及命令与Netty/Deskto之间解耦。异步
这个项目一共有四个子模块:
各个子模块的包结构相似,咱们看其中的一个子模块puppet便可。
包名 | 描述 |
---|---|
commandhandler | 命令处理器 |
constants | 常量类,包括配置参数常量、异常消息常量、和消息常量 |
exception | 自定义的一些业务异常类 |
netty | Netty网络通讯的相关类 |
ui | 界面操做的相关类 |
PuppetStarter | 启动器类 |
Resources/commandhandlers | 命令对应的处理器配置文件 |
下面来看一下关键几个类的设计:
public class Invocation implements Serializable { /** * ID(客户端标识(控制端为'M',傀儡端为'P')+MAC地址+序列号) */ private String id; /** * 傀儡名 */ private String puppetName; /** * 命令 */ private Enum<Commands> command; /** * 值 */ private Object value; //省略getter、setter方法 @Override public String toString() { return "Response{" + "requestId='" + requestId + '\'' + ", puppetName='" + puppetName + '\'' + ", command=" + command + ", value=" + value + '}'; } }
其中id的做用有两点:
Invocation类是一个基类,请求类(Request)和响应类(Response)在此基础之上扩展。
Invocation类中有一个成员变量是命令command,咱们来看一下:
/** * @author cool-coding * 2018/7/27 * 命令 */ public enum Commands{ /** * 控制端或傀儡端链接服务器时的命令 */ CONNECT, /** * 控制命令 * 1.主人向服务器发送控制请求 * 2.服务器将控制命令发给傀儡 * 3.傀儡收到控制命令,将向服务器发送截屏 */ CONTROL, /** * 傀儡发送心跳给服务器 */ HEARTBEAT, /** * 傀儡发送屏幕截图命令 */ SCREEN, /** * 控制端发送键盘事件 */ KEYBOARD, /** * 控制端发送鼠标事件 */ MOUSE, /** * 断开控制傀儡 */ TERMINATE, /** * 清晰度 */ QUALITY }
目前一共有8个命令,有的命令是M和P共用,有的是一方单用。
public interface ICommandHandler<T> { /** * * @param ctx 当前channel处理器上下文 * @param inbound channel输入对象 * @throws Exception 异常 */ void handle(ChannelHandlerContext ctx,T inbound) throws Exception; }
ICommandHandler接口是全部命令处理类的父接口,Netty ChannelHandler在处理请求时,根据不一样的命令,寻找对应的处理类。
心跳和屏幕截图都是定时向服务器发送,因此在设计时这二者同时只有一个活动便可。即发送心跳时不发送屏幕截图,发送屏幕截图时不发送心跳,控制结束后,继续发送心跳。这二者之间的控制由Puppet模块中ConnectCommandHandler类中的HeartBeatAndScreenSnapShotTaskManagement内部类控制。
经过对用例和流程的分析,发现命令出现的频率比较高,因而考虑将命令处理单独独立出来,采起动态加载的方式,使其与ChannelHandler解耦,使用后期扩展,并且当命令不少时,不须要一次都加载,只是在使用时按需加载,减小JVM加载类的字节码量,此处参考了SPI思想。而添加命令,势必会修改界面,我使用模板模式,预留出菜单,界面体,界面属性设置等,修改时只需继续相关类并修改,而后在spring配置文件进行配置便可。
请求和响应类中都有ID属性,其中一部分是经过序列号生成器生成的,因此提供了SequenceGenerate接口和一个简单的实现类SimpleSequenceGenerator。同理还有当傀儡链接服务器时,服务器生成惟一的傀儡名,也提供了一个简单的实现类SimplePuppetNameGenerator。
图像的数据相对于纯命令来讲大了许多,因此须要想办法减小图像传输的数据,大体有两种方式:
我尝试了这两种方式,没有达到很好的效果,因为时间有限,没有更深刻研究,最终采起了压缩图像的方式。如有更好的方式,能够经过继承Puppet模块中抽象类AbstractRobotReplay,实现屏幕截屏方法byte[] getScreenSnapshot(),而后继承Master模块中抽像类AbstractDisplayPuppet实现其中的paint方法(也能够继承现有的实现类PuppetScreen,覆盖相应的方法),而后将自定义的类在spring配置文件中配置,替换掉如今的实现类便可。
/** * @author Cool-Coding * 2018/8/2 * 傀儡控制屏幕接口 */ public interface IDisplayPuppet { /** * 启动窗口显示傀儡桌面 */ void launch(); /** * 刷新桌面 * @param bytes */ void refresh(byte[] bytes); /** * * @return 傀儡名称 */ String getPuppetName(); }
接口中这三个方法前两个方法launch和refresh,都是主窗口启动傀儡控制窗口和刷新屏幕必须的方法,第三个方法是因为发送命令时,须要知道傀儡名称,而实体之间是面向接口设计的,因此须要提供获取傀儡自身名称的方法。
个人心得是:
日志
1. 记录程序关键步骤的上下文信息,例如记录请求或响应的数据以及附加的消息,记录此处建议使用trace/debug级别。 2. 记录业务流程的日志,使用info/error级别,这一部分日志主要是应用日志,例如控制端发起控制,成功或失败消息。 3. 日志最好经过统一的口径记录,便于结构清晰和日志管理
异常
有两篇文章,我以为不错,推荐给你们,我也从中参考了一些方法。
Java 日志管理最佳实践
Java异常处理的10个最佳实践
bug反馈及建议:https://github.com/Cool-Codin...
https://github.com/Cool-Codin...
若是以为还不错,Star支持一下吧,想继续开发的朋友欢迎提PR,共同开发出一款好用的远程桌面控制软件