【目錄】
1.前言
2.初現端倪
3.款款深入
4.責任細分
5.功能層級圖
6.項目結構
7.關鍵類設計
8.一些設計想法
9.待優化
10.一點心得
11.效果演示
12.討論
13.GitHub源碼
## 前言 遠程桌面控制的產品已經有很多很多,我做此項目的初衷並不是要開發出一個商用的產品,只是出於興趣愛好,做一個開源的項目,之前也沒有閱讀過任何遠程桌面控制的項目源碼,只是根據自己已有的經驗設計開發,肯定有許多不足,有興趣的朋友歡迎留言討論。
初現端倪
一般需要遠程控制的場景發生在公司和家之間,由於公司和家里的電腦一般都在局域網內,所以不能直接相連,需要第三方中轉,所以至少有三方,如下圖。
負責中轉的第三方是服務器,控制端和傀儡端(被控制端)相對於服務器來說都是客戶端,都和服務器直接相連,也就是說控制端不和傀儡端相連。
款款深入
約定:
- 控制端M(Master)
- 服務器S(Server)
- 傀儡端P(Puppet)
為了敘述方便,以下如不做特別說明,M表示控制端,S表示服務端,P表示傀儡端。
如果要達到控制傀儡的目的,應該怎么做呢?三方之間至少要發生什么交互呢?
控制端、傀儡端的接收器和服務器中的轉發器都是一個,為便於流程的清晰,分開畫了。
責任細分
可以看出三者交互主要通過命令形式(命令可以帶數據也可以不帶數據),發送、轉發、接收命令,然后做出相應的動作。
從上圖中看到,服務端不僅需要轉數據,還需要記錄存活的傀儡以及維護控制端和傀儡之間的關系,其實還得處理一些異常情況,比如遠程過程中,傀儡斷開,過一會又連接上,傀儡是否需要繼續給控制端發送屏幕截圖。
功能層級圖
粗粒度分一下,可以分為三層:Desktop層負責UI處理,CommandHandler層負責命令處理,Netty網絡層負責數據的網絡傳輸。
具體來看一下commandHandler層:
CommandHandlerLoader工具類會根據Netty或Desktop層傳入的Command到配置文件commandhandlers中查找對應的處理類,動態加載,然后進行邏輯處理,這樣對於后期命令添加是非常方便的,命令與命令之間,以及命令與Netty/Deskto之間解耦。
項目結構
這個項目一共有四個子模塊:
- server: 服務端
- puppet: 傀儡端
- master 控制端
- common: 前面三者共用的一些類或接口。
各個子模塊的包結構類似,我們看其中的一個子模塊puppet即可。
包名 | 描述 |
---|---|
commandhandler | 命令處理器 |
constants | 常量類,包括配置參數常量、異常消息常量、和消息常量 |
exception | 自定義的一些業務異常類 |
netty | Netty網絡通信的相關類 |
ui | 界面操作的相關類 |
PuppetStarter | 啟動器類 |
Resources/commandhandlers | 命令對應的處理器配置文件 |
關鍵類設計
下面來看一下關鍵幾個類的設計:
請求/響應類 Invocation
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的作用有兩點:
- 用於標識是來自M的請求,還是P的請求。
- 用於標識一次請求或響應,可以將M和P串聯起來,用於請求追蹤。
Invocation類是一個基類,請求類(Request)和響應類(Response)在此基礎之上擴展。
Invocation類中有一個成員變量是命令command,我們來看一下:
命令類 Commands
/**
* @author cool-coding
* 2018/7/27
* 命令
*/
public enum Commands{
/**
* 控制端或傀儡端連接服務器時的命令
*/
CONNECT,
/**
* 控制命令
* 1.主人向服務器發送控制請求
* 2.服務器將控制命令發給傀儡
* 3.傀儡收到控制命令,將向服務器發送截屏
*/
CONTROL,
/**
* 傀儡發送心跳給服務器
*/
HEARTBEAT,
/**
* 傀儡發送屏幕截圖命令
*/
SCREEN,
/**
* 控制端發送鍵盤事件
*/
KEYBOARD,
/**
* 控制端發送鼠標事件
*/
MOUSE,
/**
* 斷開控制傀儡
*/
TERMINATE,
/**
* 清晰度
*/
QUALITY
}
目前一共有8個命令,有的命令是M和P共用,有的是一方單用。
命令處理接口 ICommandHandler
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配置文件進行配置即可。
序列號和Puppet名稱生成器
請求和響應類中都有ID屬性,其中一部分是通過序列號生成器生成的,所以提供了SequenceGenerate接口和一個簡單的實現類SimpleSequenceGenerator。同理還有當傀儡連接服務器時,服務器生成唯一的傀儡名,也提供了一個簡單的實現類SimplePuppetNameGenerator。
圖像處理
圖像的數據相對於純命令來說大了許多,所以需要想辦法減少圖像傳輸的數據,大致有兩種方式:
- 選擇合適的圖片格式,並進行壓縮:我這里選擇了jpg格式,並使用Google Thumbnailator工具進行等寬高壓縮,因為jpg具有較高的壓縮比,但是代價是壓縮后圖像的質量不是太理想。
- 只傳輸變化的圖像:很多時候圖像變化的部分並不太多,可以只傳輸變化的區域,傳輸到控制端后,控制端只繪制變化的區域。
(1). 像素級別: 我的思路是在傀儡端保持前一次傳輸時的截屏,和本次截屏圖像進行像素級的比較,將不同的像素保存到一個對象數組中,記錄像素的位置和像素值,傳輸到控制端后,根據像素位置和要替換的像素進行繪制
(2). 區域級別:只記錄變化圖像的開始點(左上角)和結束點(右下角),然后繪制以這兩個點框定的矩形式區域。
我嘗試了這兩種方式,沒有達到很好的效果,由於時間有限,沒有更深入研究,最終采取了壓縮圖像的方式。若有更好的方式,可以通過繼承Puppet模塊中抽象類AbstractRobotReplay,實現屏幕截屏方法byte[] getScreenSnapshot(),然后繼承Master模塊中抽像類AbstractDisplayPuppet實現其中的paint方法(也可以繼承現有的實現類PuppetScreen,覆蓋相應的方法),然后將自定義的類在spring配置文件中配置,替換掉現在的實現類即可。
待優化
- 快速按鍵的情況、雙擊時響應的比較慢。傳輸命令需要時間,所以快速按鍵時命令產生滯后現象,而傀儡端圖像傳輸到控制端后,Swing是單線程處理AWT事件(鼠標、鍵盤、繪圖等),若此時仍在按鍵,則會阻塞,等到按鍵結束之后,再進行圖像的繪制,進行了如下嘗試:
- 將命令發送采用異步方式,將命令存放在隊列中,開啟一個線程依次處理,這樣可以減輕awt工作負擔,加快響應屏幕刷新。經測試,屏幕刷新確定快了,但是命令發送的不及時,響應變慢,最終放棄這種方式,依然使用同步發送。
- 鼠標移動時,在移動過程中不發送命令,等待移動結束發送:實現方式是移動事件響應方式中添加一個計數器,再采用一個延遲線程,判斷計數器值是否變化,如果延遲時間到時仍沒有變化,則發送“移動命令”,但當移動后單擊,會先發送單擊命令,再發送鼠標移動命令,也不可行。
- 傀儡端在發送屏幕截圖時,與上一次進行比較,如果沒有變化,則不發送,減少發送數據量,也減少awt負擔。
一點心得
- 需求分析很重要,分析需求中各對象的屬性和行為,以及對象之間的關系,這是后面功能、領域模型、靜態/動態模型分析的基礎。
- 設計靜態模型時,需要根據SOLID原則進行設計,例如遠程控制中命令較多,就抽像出一層,為每個命令單獨寫處理邏輯(當然多個命令也可以共用同一處理邏輯),既符合單一職責原則,又符合開閉原則,將影響降到最低,具體很大的靈活性。又如Master模塊中的IDisplayPuppet接口,此接口是控制端顯示傀儡屏幕的接口,供控制端主窗口MasterDesktop和*Listener調用。
/**
* @author Cool-Coding
* 2018/8/2
* 傀儡控制屏幕接口
*/
public interface IDisplayPuppet {
/**
* 啟動窗口顯示傀儡桌面
*/
void launch();
/**
* 刷新桌面
* @param bytes
*/
void refresh(byte[] bytes);
/**
*
* @return 傀儡名稱
*/
String getPuppetName();
}
接口中這三個方法前兩個方法launch和refresh,都是主窗口啟動傀儡控制窗口和刷新屏幕必須的方法,第三個方法是由於發送命令時,需要知道傀儡名稱,而實體之間是面向接口設計的,所以需要提供獲取傀儡自身名稱的方法。
-
日志、異常處理
日志和異常處理是相當重要的,好的日志記錄方式和好的異常處理方式能夠使項目結構更加清晰,怎么樣才算好呢,人者見仁,智者見智。
我的心得是:
日志- 記錄程序關鍵步驟的上下文信息,例如記錄請求或響應的數據以及附加的消息,記錄此處建議使用trace/debug級別。
- 記錄業務流程的日志,使用info/error級別,這一部分日志主要是應用日志,例如控制端發起控制,成功或失敗消息。
- 日志最好通過統一的口徑記錄,便於結構清晰和日志管理
異常
-
一定不要catch異常不處理,而且不要catch Throwable,因為Throwable包括了Error和Exception,Error一般都是不可恢復的錯誤,無法在程序中手工處理,不應該catch住。
-
一般下層在記錄異常日志,並向上拋出后,上層不需要處理,直接繼續向上拋出即可,如果為了讓異常具體業務含義,便於異常問題查找,可以封裝一些關鍵的業務異常。
-
異常最好集中處理,如springmvc:將異常集中在一個異常處理類中處理。
有兩篇文章,我覺得不錯,推薦給大家,我也從中參考了一些方法。
Java 日志管理最佳實踐
Java異常處理的10個最佳實踐
效果演示
- Centos6.5:傀儡端
- Windows: 控制端、服務器
-
啟動服務器、傀儡、控制端
-
復制傀儡名
也可以通過也可以通過日志獲取:
-
將名稱輸入控制端
-
控制端打開一個遠程屏幕
-
可以進行鼠標(單擊,雙擊,右鍵,拖動等)或鍵盤(單鍵或組合鍵等)操作,並可調整屏幕清晰度。
討論
bug反饋及建議:https://github.com/Cool-Coding/remote-desktop-control/issues
GitHub源碼
https://github.com/Cool-Coding/remote-desktop-control
如果覺得還不錯,Star支持一下吧,歡迎有興趣的朋友提PR,共同開發出一款好用的遠程桌面控制軟件。