【目录】
1.前言
2.初现端倪
3.款款深入
4.责任细分
5.功能层级图
6.项目结构
7.关键类设计
8.一些设计想法
9.待优化
10.一点心得
11.效果演示
12.项目导入及运行
13.版本变化
远程桌面控制的产品已经有很多很多,我做此项目的初衷并不是要开发出一个商用的产品,只是出于兴趣爱好,做一个开源的项目,之前也没有阅读过任何远程桌面控制的项目源码,只是根据自己已有的经验设计开发,肯定有许多不足,有兴趣的朋友欢迎留言讨论。
一般需要远程控制的场景发生在公司和家之间,由于公司和家里的电脑一般都在局域网内,所以不能直接相连,需要第三方中转,所以至少有三方,如下图。
负责中转的第三方是服务器,控制端和傀儡端(被控制端)相对于服务器来说都是客户端,都和服务器直接相连,也就是说控制端不和傀儡端相连。
约定:
- 控制端M(Master)
- 服务器S(Server)
- 傀儡端P(Puppet)
为了叙述方便,以下如不做特别说明,M表示控制端,S表示服务端,P表示傀儡端。
如果要达到控制傀儡的目的,应该怎么做呢?三方之间至少要发生什么交互呢?
控制端、傀儡端的接收器和服务器中的转发器都是一个,为便于流程的清晰,分开画了。
可以看出三者交互主要通过命令形式(命令可以带数据也可以不带数据),发送、转发、接收命令,然后做出相应的动作。 从上图中看到,服务端不仅需要转数据,还需要记录存活的傀儡以及维护控制端和傀儡之间的关系,其实还得处理一些异常情况,比如远程过程中,傀儡断开,过一会又连接上,傀儡是否需要继续给控制端发送屏幕截图。
粗粒度分一下,可以分为三层:Desktop层负责UI处理,CommandHandler层负责命令处理(接收和发送),Netty网络层负责数据的网络传输。
具体来看一下commandHandler层:
CommandHandlerLoader工具类会根据Netty或Desktop层传入的Command到配置文件commandhandlers中查找对应的处理类,动态加载,然后进行逻辑处理,这样对于后期命令添加是非常方便的,命令与命令之间,以及命令与Netty/Deskto之间解耦。
这个项目一共有四个子模块:
包名 | 描述 |
---|---|
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。
图像的数据相对于纯命令来说大了许多,所以需要想办法减少图像传输的数据,大致有两种方式:
- 将命令发送采用异步方式,将命令存放在队列中,开启一个线程依次处理,这样可以减轻awt工作负担。
- 鼠标移动时,在移动过程中不发送命令,等待移动结束发送:实现方式是移动事件响应方式中添加一个计数器,再采用一个延迟线程,判断计数器值是否变化,如果延迟时间到时仍没有变化,则发送“移动命令”,但当移动后单击,会先发送单击命令,再发送鼠标移动命令,也不可行。
- 傀儡端在发送屏幕截图时,与上一次进行比较,如果没有变化,则不发送,减少发送数据量,也减少awt负担。
/**
* @author Cool-Coding
* 2018/8/2
* 傀儡控制屏幕接口
*/
public interface IDisplayPuppet {
/**
* 启动窗口显示傀儡桌面
*/
void launch();
/**
* 刷新桌面
* @param bytes
*/
void refresh(byte[] bytes);
/**
*
* @return 傀儡名称
*/
String getPuppetName();
}
接口中这三个方法前两个方法launch和refresh,都是主窗口启动傀儡控制窗口和刷新屏幕必须的方法,第三个方法是由于发送命令时,需要知道傀儡名称,而实体之间是面向接口设计的,所以需要提供获取傀儡自身名称的方法。
3.日志、异常处理
日志和异常处理是相当重要的,好的日志记录方式和好的异常处理方式能够使项目结构更加清晰,怎么样才算好呢,人者见仁,智者见智。
日志
1. 记录程序关键步骤的上下文信息,例如记录请求或响应的数据以及附加的消息,记录此处建议使用trace/debug级别。
2. 记录业务流程的日志,使用info/error级别,这一部分日志主要是应用日志,例如控制端发起控制,成功或失败消息。
3. 日志最好通过统一的口径记录,便于结构清晰和日志管理
异常
1. 一定不要catch异常不处理,而且不要catch Throwable,因为Throwable包括了Error和Exception,Error一般都是不可恢复的错误,无法在程序中手工处理,不应该catch住。
2. 一般下层在记录异常日志,并向上抛出后,上层不需要处理,直接继续向上抛出即可,如果为了让异常具体业务含义,便于异常问题查找,可以封装一些关键的业务异常。
3. 异常最好集中处理,如springmvc:将异常集中在一个异常处理类中处理。
有两篇文章,我觉得不错,推荐给大家,我也从中参考了一些方法。
- Centos6.5:傀儡端
- Windows: 控制端、服务器
启动服务器、傀儡、控制端
复制傀儡名
也可以通过日志获取:
将名称输入控制端
控制端打开一个远程屏幕
可以进行鼠标(单击,双击,右键,拖动等)或键盘(单键或组合键等)操作,并可调整屏幕清晰度。
File->Project Structure
创建Artifacts
Moudle应总是选择desktop-control-parent
Main Class根据服务器、控制端、傀儡端需要选择对应的启动类
截图中只有common包的操作方法,其它子项目也是相同操作方法,不再截图,如打包Master,则需要打包common和master;打包puppet,则需要打包 common和puppet;打包server,则需要打包common和server。
(1) 修改name名称
(2) 选中common包下的'common' compile output,右键选择Put into Output Root
(3) 选择common包下所有依赖的jar包,右键选择Extract Into Output Root
运行打好的jar包
java -jar xxxx.jar
20180802
20180916
20181221
bug反馈及建议:https://github.com/Cool-Coding/remote-desktop-control/issues
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。