使用Netty+Spring+Swing實現遠程桌面控制

前言

遠程桌面控制的產品已經有不少不少,我作此項目的初衷並非要開發出一個商用的產品,只是出於興趣愛好,作一個開源的項目,以前也沒有閱讀過任何遠程桌面控制的項目源碼,只是根據本身已有的經驗設計開發,確定有許多不足,有興趣的朋友能夠留言討論與支持。html

初現端倪

通常須要遠程控制的場景發生在公司和家之間,因爲公司和家裏的電腦通常都在局域網內,因此不能直接相連,須要第三方中轉,因此至少有三方,以下圖。
java

負責中轉的第三方是服務器,控制端和傀儡端(被控制端)相對於服務器來講都是客戶端,都和服務器直接相連,也就是說控制端不和傀儡端相連。git

款款深刻

約定:github

  • 控制端M(Master)
  • 服務器S(Server)
  • 傀儡端P(Puppet)

爲了敘述方便,如下如不作特別說明,M表示控制端,S表示服務端,P表示傀儡端。spring

若是要達到控制傀儡的目的,應該怎麼作呢?三方之間至少要發生什麼交互呢?
三方會談數組

控制端、傀儡端的接收器和服務器中的轉發器都是一個,爲便於流程的清晰,分開畫了。

責任細分

責任細分

能夠看出三者交互主要經過命令形式(命令能夠帶數據也能夠不帶數據),發送、轉發、接收命令,而後作出相應的動做。
從上圖中看到,服務端不只須要轉數據,還須要記錄存活的傀儡以及維護控制端和傀儡之間的關係,其實還得處理一些異常狀況,好比遠程過程當中,傀儡斷開,過一會又鏈接上,傀儡是否須要繼續給控制端發送屏幕截圖。服務器

功能層級圖

粗粒度分一下,能夠分爲三層:Desktop層負責UI處理,CommandHandler層負責命令處理,Netty網絡層負責數據的網絡傳輸。網絡

功能層級圖

具體來看一下commandHandler層:
commandhandlermvc

CommandHandlerLoader工具類會根據Netty或Desktop層傳入的Command到配置文件commandhandlers中查找對應的處理類,動態加載,而後進行邏輯處理,這樣對於後期命令添加是很是方便的,命令與命令之間,以及命令與Netty/Deskto之間解耦。異步

項目結構

整體頂目結構

這個項目一共有四個子模塊:

  • server: 服務端
  • puppet: 傀儡端
  • master 控制端
  • common: 前面三者共用的一些類或接口。

各個子模塊的包結構相似,咱們看其中的一個子模塊puppet便可。
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的做用有兩點:

  1. 用於標識是來自M的請求,仍是P的請求。
  2. 用於標識一次請求或響應,能夠將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事件沒法的邏輯抽離出來使用異步,則沒法保證命令的順序性,可能須要使用到消息隊列,並且按鍵速度很快的狀況下,按鍵命令的發送會產生必定的延遲。

一點心得

  • 需求分析很重要,分析需求中各對象的屬性和行爲,以及對象之間的關係,這是後面功能、領域模型、靜態/動態模型分析的基礎。
  • 設計靜態模型時,須要根據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,都是主窗口啓動傀儡控制窗口和刷新屏幕必須的方法,第三個方法是因爲發送命令時,須要知道傀儡名稱,而實體之間是面向接口設計的,因此須要提供獲取傀儡自身名稱的方法。

  • 日誌、異常處理
    日誌和異常處理是至關重要的,好的日誌記錄方式和好的異常處理方式可以使項目結構更加清晰,怎麼樣纔算好呢,人者見仁,智者見智。

個人心得是:
日誌

1. 記錄程序關鍵步驟的上下文信息,例如記錄請求或響應的數據以及附加的消息,記錄此處建議使用trace/debug級別。
2. 記錄業務流程的日誌,使用info/error級別,這一部分日誌主要是應用日誌,例如控制端發起控制,成功或失敗消息。
3. 日誌最好經過統一的口徑記錄,便於結構清晰和日誌管理

異常

    1. 必定不要catch異常不處理,並且不要catch Throwable,由於Throwable包括了Error和Exception,Error通常都是不可恢復的錯誤,沒法在程序中手工處理,不該該catch住。
    1. 通常下層在記錄異常日誌,並向上拋出後,上層不須要處理,直接繼續向上拋出便可,若是爲了讓異常具體業務含義,便於異常問題查找,能夠封裝一些關鍵的業務異常。
    2. 異常最好集中處理,如springmvc:將異常集中在一個異常處理類中處理。

    有兩篇文章,我以爲不錯,推薦給你們,我也從中參考了一些方法。
    Java 日誌管理最佳實踐
    Java異常處理的10個最佳實踐

    效果演示

    • Centos6.5:傀儡端
    • Windows: 控制端、服務器
    1. 啓動服務器、傀儡、控制端
    2. 複製傀儡名傀儡名也能夠經過日誌獲取:
    3. 將名稱輸入控制端
    4. 控制端打開一個遠程屏幕
    5. 能夠進行鼠標(單擊,雙擊,右鍵,拖動等)或鍵盤(單鍵或組合鍵等)操做,並可調整屏幕清晰度。

    討論

    bug反饋及建議https://github.com/Cool-Codin...

    GitHub源碼

    https://github.com/Cool-Codin...

    若是以爲還不錯,Star支持一下吧,想繼續開發的朋友歡迎提PR,共同開發出一款好用的遠程桌面控制軟件

    相關文章
    相關標籤/搜索