前言
遠(yuǎn)程桌面控制的產(chǎn)品已經(jīng)有很多很多尉桩,我做此項(xiàng)目的初衷并不是要開發(fā)出一個(gè)商用的產(chǎn)品郊闯,只是出于興趣愛好妻献,做一個(gè)開源的項(xiàng)目,之前也沒有閱讀過任何遠(yuǎn)程桌面控制的項(xiàng)目源碼团赁,只是根據(jù)自己已有的經(jīng)驗(yàn)設(shè)計(jì)開發(fā)育拨,肯定有許多不足,有興趣的朋友可以留言討論與支持欢摄。
初現(xiàn)端倪
一般需要遠(yuǎn)程控制的場(chǎng)景發(fā)生在公司和家之間至朗,由于公司和家里的電腦一般都在局域網(wǎng)內(nèi),所以不能直接相連剧浸,需要第三方中轉(zhuǎn)锹引,所以至少有三方,如下圖。
負(fù)責(zé)中轉(zhuǎn)的第三方是服務(wù)器唆香,控制端和傀儡端(被控制端)相對(duì)于服務(wù)器來說都是客戶端嫌变,都和服務(wù)器直接相連,也就是說控制端不和傀儡端相連躬它。
款款深入
約定:
- 控制端M(Master)
- 服務(wù)器S(Server)
- 傀儡端P(Puppet)
為了敘述方便,以下如不做特別說明,M表示控制端,S表示服務(wù)端,P表示傀儡端腾啥。
如果要達(dá)到控制傀儡的目的,應(yīng)該怎么做呢冯吓?三方之間至少要發(fā)生什么交互呢倘待?
控制端、傀儡端的接收器和服務(wù)器中的轉(zhuǎn)發(fā)器都是一個(gè)组贺,為便于流程的清晰凸舵,分開畫了。
責(zé)任細(xì)分
可以看出三者交互主要通過命令形式(命令可以帶數(shù)據(jù)也可以不帶數(shù)據(jù))失尖,發(fā)送啊奄、轉(zhuǎn)發(fā)渐苏、接收命令,然后做出相應(yīng)的動(dòng)作菇夸。
從上圖中看到琼富,服務(wù)端不僅需要轉(zhuǎn)數(shù)據(jù),還需要記錄存活的傀儡以及維護(hù)控制端和傀儡之間的關(guān)系庄新,其實(shí)還得處理一些異常情況鞠眉,比如遠(yuǎn)程過程中,傀儡斷開择诈,過一會(huì)又連接上械蹋,傀儡是否需要繼續(xù)給控制端發(fā)送屏幕截圖。
功能層級(jí)圖
粗粒度分一下吭从,可以分為三層:Desktop層負(fù)責(zé)UI處理朝蜘,CommandHandler層負(fù)責(zé)命令處理,Netty網(wǎng)絡(luò)層負(fù)責(zé)數(shù)據(jù)的網(wǎng)絡(luò)傳輸恶迈。
具體來看一下commandHandler層:
CommandHandlerLoader工具類會(huì)根據(jù)Netty或Desktop層傳入的Command到配置文件commandhandlers中查找對(duì)應(yīng)的處理類涩金,動(dòng)態(tài)加載,然后進(jìn)行邏輯處理暇仲,這樣對(duì)于后期命令添加是非常方便的步做,命令與命令之間,以及命令與Netty/Deskto之間解耦奈附。
項(xiàng)目結(jié)構(gòu)
這個(gè)項(xiàng)目一共有四個(gè)子模塊:
- server: 服務(wù)端
- puppet: 傀儡端
- master 控制端
-
common: 前面三者共用的一些類或接口全度。
各個(gè)子模塊的包結(jié)構(gòu)類似,我們看其中的一個(gè)子模塊puppet即可斥滤。
包名 | 描述 |
---|---|
commandhandler | 命令處理器 |
constants | 常量類将鸵,包括配置參數(shù)常量、異常消息常量佑颇、和消息常量 |
exception | 自定義的一些業(yè)務(wù)異常類 |
netty | Netty網(wǎng)絡(luò)通信的相關(guān)類 |
ui | 界面操作的相關(guān)類 |
PuppetStarter | 啟動(dòng)器類 |
Resources/commandhandlers | 命令對(duì)應(yīng)的處理器配置文件 |
關(guān)鍵類設(shè)計(jì)
下面來看一下關(guān)鍵幾個(gè)類的設(shè)計(jì):
請(qǐng)求/響應(yīng)類 Invocation
public class Invocation implements Serializable {
/**
* ID(客戶端標(biāo)識(shí)(控制端為'M',傀儡端為'P')+MAC地址+序列號(hào))
*/
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的作用有兩點(diǎn):
- 用于標(biāo)識(shí)是來自M的請(qǐng)求,還是P的請(qǐng)求挑胸。
- 用于標(biāo)識(shí)一次請(qǐng)求或響應(yīng),可以將M和P串聯(lián)起來,用于請(qǐng)求追蹤见擦。
Invocation類是一個(gè)基類沼头,請(qǐng)求類(Request)和響應(yīng)類(Response)在此基礎(chǔ)之上擴(kuò)展。
Invocation類中有一個(gè)成員變量是命令command解藻,我們來看一下:
命令類 Commands
/**
* @author cool-coding
* 2018/7/27
* 命令
*/
public enum Commands{
/**
* 控制端或傀儡端連接服務(wù)器時(shí)的命令
*/
CONNECT,
/**
* 控制命令
* 1.主人向服務(wù)器發(fā)送控制請(qǐng)求
* 2.服務(wù)器將控制命令發(fā)給傀儡
* 3.傀儡收到控制命令老充,將向服務(wù)器發(fā)送截屏
*/
CONTROL,
/**
* 傀儡發(fā)送心跳給服務(wù)器
*/
HEARTBEAT,
/**
* 傀儡發(fā)送屏幕截圖命令
*/
SCREEN,
/**
* 控制端發(fā)送鍵盤事件
*/
KEYBOARD,
/**
* 控制端發(fā)送鼠標(biāo)事件
*/
MOUSE,
/**
* 斷開控制傀儡
*/
TERMINATE,
/**
* 清晰度
*/
QUALITY
}
目前一共有8個(gè)命令,有的命令是M和P共用螟左,有的是一方單用蚂维。
命令處理接口 ICommandHandler
public interface ICommandHandler<T> {
/**
*
* @param ctx 當(dāng)前channel處理器上下文
* @param inbound channel輸入對(duì)象
* @throws Exception 異常
*/
void handle(ChannelHandlerContext ctx,T inbound) throws Exception;
}
ICommandHandler接口是所有命令處理類的父接口戳粒,Netty ChannelHandler在處理請(qǐng)求時(shí),根據(jù)不同的命令虫啥,尋找對(duì)應(yīng)的處理類蔚约。
一些設(shè)計(jì)想法
心跳與屏幕截圖
心跳和屏幕截圖都是定時(shí)向服務(wù)器發(fā)送,所以在設(shè)計(jì)時(shí)這兩者同時(shí)只有一個(gè)活動(dòng)即可涂籽。即發(fā)送心跳時(shí)不發(fā)送屏幕截圖苹祟,發(fā)送屏幕截圖時(shí)不發(fā)送心跳,控制結(jié)束后评雌,繼續(xù)發(fā)送心跳树枫。這兩者之間的控制由Puppet模塊中ConnectCommandHandler類中的HeartBeatAndScreenSnapShotTaskManagement內(nèi)部類控制。
命令分層
通過對(duì)用例和流程的分析景东,發(fā)現(xiàn)命令出現(xiàn)的頻率比較高砂轻,于是考慮將命令處理單獨(dú)獨(dú)立出來,采取動(dòng)態(tài)加載的方式斤吐,使其與ChannelHandler解耦搔涝,使用后期擴(kuò)展,而且當(dāng)命令很多時(shí)和措,不需要一次都加載庄呈,只是在使用時(shí)按需加載,減少JVM加載類的字節(jié)碼量派阱,此處參考了SPI思想诬留。而添加命令,勢(shì)必會(huì)修改界面贫母,我使用模板模式文兑,預(yù)留出菜單,界面體腺劣,界面屬性設(shè)置等绿贞,修改時(shí)只需繼續(xù)相關(guān)類并修改,然后在spring配置文件進(jìn)行配置即可誓酒。
序列號(hào)和Puppet名稱生成器
請(qǐng)求和響應(yīng)類中都有ID屬性樟蠕,其中一部分是通過序列號(hào)生成器生成的,所以提供了SequenceGenerate接口和一個(gè)簡(jiǎn)單的實(shí)現(xiàn)類SimpleSequenceGenerator靠柑。同理還有當(dāng)傀儡連接服務(wù)器時(shí)寨辩,服務(wù)器生成唯一的傀儡名,也提供了一個(gè)簡(jiǎn)單的實(shí)現(xiàn)類SimplePuppetNameGenerator歼冰。
圖像處理
圖像的數(shù)據(jù)相對(duì)于純命令來說大了許多靡狞,所以需要想辦法減少圖像傳輸?shù)臄?shù)據(jù),大致有兩種方式:
- 選擇合適的圖片格式隔嫡,并進(jìn)行壓縮:我這里選擇了jpg格式甸怕,并使用Google Thumbnailator工具進(jìn)行等寬高壓縮甘穿,因?yàn)閖pg具有較高的壓縮比,但是代價(jià)是壓縮后圖像的質(zhì)量不是太理想。
- 只傳輸變化的圖像:很多時(shí)候圖像變化的部分并不太多梢杭,可以只傳輸變化的區(qū)域温兼,傳輸?shù)娇刂贫撕螅刂贫酥焕L制變化的區(qū)域武契。
(1). 像素級(jí)別: 我的思路是在傀儡端保持前一次傳輸時(shí)的截屏募判,和本次截屏圖像進(jìn)行像素級(jí)的比較,將不同的像素保存到一個(gè)對(duì)象數(shù)組中咒唆,記錄像素的位置和像素值届垫,傳輸?shù)娇刂贫撕螅鶕?jù)像素位置和要替換的像素進(jìn)行繪制
(2). 區(qū)域級(jí)別:只記錄變化圖像的開始點(diǎn)(左上角)和結(jié)束點(diǎn)(右下角)全释,然后繪制以這兩個(gè)點(diǎn)框定的矩形式區(qū)域装处。
我嘗試了這兩種方式,沒有達(dá)到很好的效果浸船,由于時(shí)間有限妄迁,沒有更深入研究,最終采取了壓縮圖像的方式糟袁。若有更好的方式判族,可以通過繼承Puppet模塊中抽象類AbstractRobotReplay躺盛,實(shí)現(xiàn)屏幕截屏方法byte[] getScreenSnapshot(),然后繼承Master模塊中抽像類AbstractDisplayPuppet實(shí)現(xiàn)其中的paint方法(也可以繼承現(xiàn)有的實(shí)現(xiàn)類PuppetScreen项戴,覆蓋相應(yīng)的方法),然后將自定義的類在spring配置文件中配置槽惫,替換掉現(xiàn)在的實(shí)現(xiàn)類即可周叮。
待優(yōu)化
- 快速按鍵的情況、雙擊時(shí)響應(yīng)的比較慢界斜。傳輸命令需要時(shí)間仿耽,所以快速按鍵時(shí)命令產(chǎn)生滯后現(xiàn)象,而傀儡端圖像傳輸?shù)娇刂贫撕蟾鬓保琒wing是單線程處理AWT事件(鼠標(biāo)项贺、鍵盤、繪圖等)峭判,若此時(shí)仍在按鍵开缎,則會(huì)阻塞,等到按鍵結(jié)束之后林螃,再進(jìn)行圖像的繪制奕删,進(jìn)行如下嘗試:
- 將命令發(fā)送采用異步方式,將命令存放在隊(duì)列中疗认,開啟一個(gè)線程依次處理完残,這樣可以減輕awt工作負(fù)擔(dān)伏钠,加快響應(yīng)屏幕刷新。經(jīng)測(cè)試谨设,屏幕刷新確定快了熟掂,但是命令發(fā)送的不及時(shí),響應(yīng)變慢扎拣,最終放棄這種方式打掘,依然使用同步發(fā)送。
- 鼠標(biāo)移動(dòng)時(shí)鹏秋,在移動(dòng)過程中不發(fā)送命令尊蚁,等待移動(dòng)結(jié)束發(fā)送:實(shí)現(xiàn)方式是移動(dòng)事件響應(yīng)方式中添加一個(gè)計(jì)數(shù)器,再采用一個(gè)延遲線程侣夷,判斷計(jì)數(shù)器值是否變化横朋,如果延遲時(shí)間到時(shí)仍沒有變化,則發(fā)送“移動(dòng)命令”百拓,但當(dāng)移動(dòng)后單擊琴锭,會(huì)先發(fā)送單擊命令,再發(fā)送鼠標(biāo)移動(dòng)命令衙传,也不可行决帖。
- 傀儡端在發(fā)送屏幕截圖時(shí),與上一次進(jìn)行比較蓖捶,如果沒有變化地回,則不發(fā)送,減少發(fā)送數(shù)據(jù)量俊鱼,也減少awt負(fù)擔(dān)刻像。
一點(diǎn)心得
- 需求分析很重要,分析需求中各對(duì)象的屬性和行為并闲,以及對(duì)象之間的關(guān)系细睡,這是后面功能、領(lǐng)域模型帝火、靜態(tài)/動(dòng)態(tài)模型分析的基礎(chǔ)溜徙。
- 設(shè)計(jì)靜態(tài)模型時(shí),需要根據(jù)SOLID原則進(jìn)行設(shè)計(jì)犀填,例如遠(yuǎn)程控制中命令較多蠢壹,就抽像出一層,為每個(gè)命令單獨(dú)寫處理邏輯(當(dāng)然多個(gè)命令也可以共用同一處理邏輯)宏浩,既符合單一職責(zé)原則知残,又符合開閉原則,將影響降到最低,具體很大的靈活性求妹。又如Master模塊中的IDisplayPuppet接口乏盐,此接口是控制端顯示傀儡屏幕的接口,供控制端主窗口MasterDesktop和*Listener調(diào)用制恍。
/**
* @author Cool-Coding
* 2018/8/2
* 傀儡控制屏幕接口
*/
public interface IDisplayPuppet {
/**
* 啟動(dòng)窗口顯示傀儡桌面
*/
void launch();
/**
* 刷新桌面
* @param bytes
*/
void refresh(byte[] bytes);
/**
*
* @return 傀儡名稱
*/
String getPuppetName();
}
接口中這三個(gè)方法前兩個(gè)方法launch和refresh父能,都是主窗口啟動(dòng)傀儡控制窗口和刷新屏幕必須的方法,第三個(gè)方法是由于發(fā)送命令時(shí)净神,需要知道傀儡名稱何吝,而實(shí)體之間是面向接口設(shè)計(jì)的,所以需要提供獲取傀儡自身名稱的方法鹃唯。
-
日志爱榕、異常處理
日志和異常處理是相當(dāng)重要的,好的日志記錄方式和好的異常處理方式能夠使項(xiàng)目結(jié)構(gòu)更加清晰坡慌,怎么樣才算好呢黔酥,人者見仁,智者見智洪橘。
我的心得是:
日志- 記錄程序關(guān)鍵步驟的上下文信息跪者,例如記錄請(qǐng)求或響應(yīng)的數(shù)據(jù)以及附加的消息,記錄此處建議使用trace/debug級(jí)別熄求。
- 記錄業(yè)務(wù)流程的日志渣玲,使用info/error級(jí)別,這一部分日志主要是應(yīng)用日志弟晚,例如控制端發(fā)起控制忘衍,成功或失敗消息。
- 日志最好通過統(tǒng)一的口徑記錄指巡,便于結(jié)構(gòu)清晰和日志管理
異常
一定不要catch異常不處理淑履,而且不要catch Throwable隶垮,因?yàn)門hrowable包括了Error和Exception,Error一般都是不可恢復(fù)的錯(cuò)誤藻雪,無法在程序中手工處理,不應(yīng)該catch住狸吞。
一般下層在記錄異常日志勉耀,并向上拋出后,上層不需要處理蹋偏,直接繼續(xù)向上拋出即可便斥,如果為了讓異常具體業(yè)務(wù)含義,便于異常問題查找威始,可以封裝一些關(guān)鍵的業(yè)務(wù)異常枢纠。
異常最好集中處理,如springmvc:將異常集中在一個(gè)異常處理類中處理。
有兩篇文章黎棠,我覺得不錯(cuò)晋渺,推薦給大家镰绎,我也從中參考了一些方法。
Java 日志管理最佳實(shí)踐
Java異常處理的10個(gè)最佳實(shí)踐
效果演示
- Centos6.5:傀儡端
- Windows: 控制端木西、服務(wù)器
啟動(dòng)服務(wù)器畴栖、傀儡、控制端
-
復(fù)制傀儡名
-
將名稱輸入控制端
-
控制端打開一個(gè)遠(yuǎn)程屏幕
-
可以進(jìn)行鼠標(biāo)(單擊八千,雙擊吗讶,右鍵,拖動(dòng)等)或鍵盤(單鍵或組合鍵等)操作恋捆,并可調(diào)整屏幕清晰度照皆。
討論
bug反饋及建議:https://github.com/Cool-Coding/remote-desktop-control/issues
GitHub源碼
https://github.com/Cool-Coding/remote-desktop-control
如果覺得還不錯(cuò),Star支持一下吧沸停,歡迎有興趣的朋友Pull Request,共同開發(fā)出一款好用的遠(yuǎn)程桌面控制軟件