遠(yuǎn)程桌面控制(Spring+Netty+Swing)

前言

遠(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ā)生什么交互呢倘待?


三方會(huì)談

控制端、傀儡端的接收器和服務(wù)器中的轉(zhuǎn)發(fā)器都是一個(gè)组贺,為便于流程的清晰凸舵,分開畫了。

責(zé)任細(xì)分

責(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ò)傳輸恶迈。

功能層級(jí)圖

具體來看一下commandHandler層:


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)

總體頂目結(jié)構(gòu)

這個(gè)項(xiàng)目一共有四個(gè)子模塊:

  • server: 服務(wù)端
  • puppet: 傀儡端
  • master 控制端
  • common: 前面三者共用的一些類或接口全度。
    各個(gè)子模塊的包結(jié)構(gòu)類似,我們看其中的一個(gè)子模塊puppet即可斥滤。
    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):

  1. 用于標(biāo)識(shí)是來自M的請(qǐng)求,還是P的請(qǐng)求挑胸。
  2. 用于標(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)行如下嘗試:
    1. 將命令發(fā)送采用異步方式,將命令存放在隊(duì)列中疗认,開啟一個(gè)線程依次處理完残,這樣可以減輕awt工作負(fù)擔(dān)伏钠,加快響應(yīng)屏幕刷新。經(jīng)測(cè)試谨设,屏幕刷新確定快了熟掂,但是命令發(fā)送的不及時(shí),響應(yīng)變慢扎拣,最終放棄這種方式打掘,依然使用同步發(fā)送。
    2. 鼠標(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)命令衙传,也不可行决帖。
    3. 傀儡端在發(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)更加清晰坡慌,怎么樣才算好呢黔酥,人者見仁,智者見智洪橘。
    我的心得是:
    日志

    1. 記錄程序關(guān)鍵步驟的上下文信息跪者,例如記錄請(qǐng)求或響應(yīng)的數(shù)據(jù)以及附加的消息,記錄此處建議使用trace/debug級(jí)別熄求。
    2. 記錄業(yè)務(wù)流程的日志渣玲,使用info/error級(jí)別,這一部分日志主要是應(yīng)用日志弟晚,例如控制端發(fā)起控制忘衍,成功或失敗消息。
    3. 日志最好通過統(tǒng)一的口徑記錄指巡,便于結(jié)構(gòu)清晰和日志管理

    異常

    1. 一定不要catch異常不處理淑履,而且不要catch Throwable隶垮,因?yàn)門hrowable包括了Error和Exception,Error一般都是不可恢復(fù)的錯(cuò)誤藻雪,無法在程序中手工處理,不應(yīng)該catch住狸吞。

    2. 一般下層在記錄異常日志勉耀,并向上拋出后,上層不需要處理蹋偏,直接繼續(xù)向上拋出即可便斥,如果為了讓異常具體業(yè)務(wù)含義,便于異常問題查找威始,可以封裝一些關(guān)鍵的業(yè)務(wù)異常枢纠。

    3. 異常最好集中處理,如springmvc:將異常集中在一個(gè)異常處理類中處理。

有兩篇文章黎棠,我覺得不錯(cuò)晋渺,推薦給大家镰绎,我也從中參考了一些方法。
Java 日志管理最佳實(shí)踐
Java異常處理的10個(gè)最佳實(shí)踐

效果演示

  • Centos6.5:傀儡端
  • Windows: 控制端木西、服務(wù)器
  1. 啟動(dòng)服務(wù)器畴栖、傀儡、控制端

  2. 復(fù)制傀儡名
    傀儡名

    也可以通過日志獲取:
  3. 將名稱輸入控制端
  4. 控制端打開一個(gè)遠(yuǎn)程屏幕
  5. 可以進(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)程桌面控制軟件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纵寝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子星立,更是在濱河造成了極大的恐慌爽茴,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绰垂,死亡現(xiàn)場(chǎng)離奇詭異室奏,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)劲装,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門胧沫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人占业,你說我怎么就攤上這事绒怨。” “怎么了谦疾?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵南蹂,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我念恍,道長(zhǎng)六剥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任峰伙,我火速辦了婚禮疗疟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瞳氓。我一直安慰自己策彤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著店诗,像睡著了一般叽赊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上必搞,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天必指,我揣著相機(jī)與錄音,去河邊找鬼恕洲。 笑死塔橡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的霜第。 我是一名探鬼主播葛家,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼泌类!你這毒婦竟也來了癞谒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤刃榨,失蹤者是張志新(化名)和其女友劉穎弹砚,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枢希,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡桌吃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了苞轿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茅诱。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖搬卒,靈堂內(nèi)的尸體忽然破棺而出瑟俭,到底是詐尸還是另有隱情,我是刑警寧澤契邀,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布摆寄,位于F島的核電站,受9級(jí)特大地震影響蹂安,放射性物質(zhì)發(fā)生泄漏椭迎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一田盈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缴阎,春花似錦允瞧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痹升。三九已至,卻和暖如春畦韭,著一層夾襖步出監(jiān)牢的瞬間疼蛾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工艺配, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留察郁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓转唉,卻偏偏與公主長(zhǎng)得像皮钠,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赠法,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容