{"id":15014307,"url":"https://github.com/cool-coding/remote-desktop-control","last_synced_at":"2025-05-16T15:06:04.765Z","repository":{"id":39826035,"uuid":"143126122","full_name":"Cool-Coding/remote-desktop-control","owner":"Cool-Coding","description":"远程桌面控制(Spring+Netty+Swing)","archived":false,"fork":false,"pushed_at":"2023-04-17T17:41:27.000Z","size":319,"stargazers_count":971,"open_issues_count":6,"forks_count":312,"subscribers_count":38,"default_branch":"master","last_synced_at":"2025-04-03T12:09:31.604Z","etag":null,"topics":["netty","spring","swing"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cool-Coding.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-08-01T08:22:19.000Z","updated_at":"2025-04-02T04:48:47.000Z","dependencies_parsed_at":"2022-07-14T04:20:31.156Z","dependency_job_id":"251c11b4-9a87-44b2-98b4-8eb49200a074","html_url":"https://github.com/Cool-Coding/remote-desktop-control","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cool-Coding%2Fremote-desktop-control","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cool-Coding%2Fremote-desktop-control/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cool-Coding%2Fremote-desktop-control/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cool-Coding%2Fremote-desktop-control/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cool-Coding","download_url":"https://codeload.github.com/Cool-Coding/remote-desktop-control/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248565034,"owners_count":21125414,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["netty","spring","swing"],"created_at":"2024-09-24T19:45:26.855Z","updated_at":"2025-04-12T11:52:47.705Z","avatar_url":"https://github.com/Cool-Coding.png","language":"Java","readme":"\u003e \u003cfont size=\"5\" \u003e【目录】\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[1.前言](#前言)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[2.初现端倪](#初现端倪)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[3.款款深入](#款款深入)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[4.责任细分](#责任细分])\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[5.功能层级图](#功能层级图)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[6.项目结构](#项目结构)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[7.关键类设计](#关键类设计)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[8.一些设计想法](#一些设计想法)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[9.待优化](#待优化)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[10.一点心得](#一点心得)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[11.效果演示](#效果演示)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[12.项目导入及运行](#项目导入及运行)\u003c/font\u003e  \n\u003e \u003cfont size=\"4\"\u003e[13.版本变化](#版本变化)\u003c/font\u003e  \n\n## 前言\n远程桌面控制的产品已经有很多很多，我做此项目的初衷并不是要开发出一个商用的产品，只是出于兴趣爱好，做一个开源的项目，之前也没有阅读过任何远程桌面控制的项目源码，只是根据自己已有的经验设计开发，肯定有许多不足，有兴趣的朋友欢迎修改优化。\n\n## 初现端倪\n一般需要远程控制的场景发生在公司和家之间，由于公司和家里的电脑一般都在局域网内，所以不能直接相连，需要第三方中转，所以至少有三方,如下图。   \n![](https://upload-images.jianshu.io/upload_images/6752673-bdfc1646a00bd3d8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n负责中转的第三方是服务器，控制端和傀儡端(被控制端)相对于服务器来说都是客户端，都和服务器直接相连，也就是说控制端不和傀儡端相连。\n\n## 款款深入\n\n\u003e **约定:**\n\u003e - 控制端M(Master)\n\u003e - 服务器S(Server)\n\u003e - 傀儡端P(Puppet)\n\n\u003e **为了叙述方便,以下如不做特别说明,M表示控制端,S表示服务端,P表示傀儡端。**\n\n如果要达到控制傀儡的目的，应该怎么做呢？三方之间至少要发生什么交互呢？\n![三方会谈](https://upload-images.jianshu.io/upload_images/6752673-601784990497dcd4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\u003e 控制端、傀儡端的接收器和服务器中的转发器都是一个，为便于流程的清晰，分开画了。\n\n## 责任细分\n![责任细分](https://upload-images.jianshu.io/upload_images/6752673-434fe6b4600d463f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看出三者交互主要通过命令形式(命令可以带数据也可以不带数据)，发送、转发、接收命令，然后做出相应的动作。\n从上图中看到，服务端不仅需要转数据，还需要记录存活的傀儡以及维护控制端和傀儡之间的关系，其实还得处理一些异常情况，比如远程过程中，傀儡断开，过一会又连接上，傀儡是否需要继续给控制端发送屏幕截图。\n\n## 功能层级图\n\n粗粒度分一下，可以分为三层：Desktop层负责UI处理，CommandHandler层负责命令处理(接收和发送),Netty网络层负责数据的网络传输。\n\n![功能层级图](https://upload-images.jianshu.io/upload_images/6752673-f8658fede3bab7ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n具体来看一下commandHandler层：\n![commandhandler](https://upload-images.jianshu.io/upload_images/6752673-55b5f559f4823e05.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\nCommandHandlerLoader工具类会根据Netty或Desktop层传入的Command到配置文件commandhandlers中查找对应的处理类，动态加载，然后进行逻辑处理，这样对于后期命令添加是非常方便的，命令与命令之间，以及命令与Netty/Desktop之间解耦。\n\n## 项目结构\n![总体顶目结构](https://upload-images.jianshu.io/upload_images/6752673-822b7d4301573cd8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n这个项目一共有四个子模块:\n- **server**:  服务端\n- **puppet**: 傀儡端\n- **master**  控制端\n- **common**: 前面三者共用的一些类或接口。\n各个子模块的包结构类似，我们看其中的一个子模块puppet即可。\n![puppet](https://upload-images.jianshu.io/upload_images/6752673-a4762e940be18b9a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n| 包名  |描述  |\n|-- | -- |\n|commandhandler|命令处理器|\n|constants|常量类，包括配置参数常量、异常消息常量、和消息常量|\n| exception|自定义的一些业务异常类 |\n|netty|Netty网络通信的相关类|\n|ui|界面操作的相关类|\n|PuppetStarter|启动器类|\n|Resources/commandhandlers|命令对应的处理器配置文件|\n\n## 关键类设计\n下面来看一下关键几个类的设计:\n### 请求/响应类 Invocation\n```java\npublic class Invocation implements Serializable {\n    /**\n     * ID(客户端标识(控制端为'M',傀儡端为'P')+MAC地址+序列号)\n     */\n    private String id;\n\n    /**\n     * 傀儡名\n     */\n    private String puppetName;\n\n    /**\n     * 命令\n     */\n    private Enum\u003cCommands\u003e command;\n\n    /**\n     * 值\n     */\n    private Object value;\n\n    //省略getter、setter方法\n\n    @Override\n    public String toString() {\n        return \"Response{\" +\n                \"requestId='\" + requestId + '\\'' +\n                \", puppetName='\" + puppetName + '\\'' +\n                \", command=\" + command +\n                \", value=\" + value +\n                '}';\n    }\n}\n```\n其中id的作用有两点：\n1. 用于标识是来自M的请求，还是P的请求。 \n2. 用于标识一次请求或响应，可以将M和P串联起来，用于请求追踪。  \n\nInvocation类是一个基类，请求类(Request)和响应类(Response)在此基础之上扩展。\nInvocation类中有一个成员变量是命令command，我们来看一下:\n\n### 命令类 Commands\n```java\n/**\n * @author cool-coding\n * 2018/7/27\n * 命令\n */\npublic enum Commands{\n    /**\n     * 控制端或傀儡端连接服务器时的命令\n     */\n    CONNECT,\n\n    /**\n     * 控制命令\n     * 1.主人向服务器发送控制请求\n     * 2.服务器将控制命令发给傀儡\n     * 3.傀儡收到控制命令，将向服务器发送截屏\n     */\n    CONTROL,\n\n    /**\n     * 傀儡发送心跳给服务器\n     */\n    HEARTBEAT,\n\n    /**\n     * 傀儡发送屏幕截图命令\n     */\n    SCREEN,\n\n    /**\n     * 控制端发送键盘事件\n     */\n    KEYBOARD,\n\n    /**\n     * 控制端发送鼠标事件\n     */\n    MOUSE,\n\n    /**\n     * 断开控制傀儡\n     */\n    TERMINATE,\n\n    /**\n     * 清晰度\n     */\n    QUALITY\n}\n```\n目前一共有8个命令，有的命令是M和P共用，有的是一方单用。\n\n### 命令处理接口 ICommandHandler\n```java\npublic interface ICommandHandler\u003cT\u003e {\n    /**\n     * \n     * @param ctx           当前channel处理器上下文\n     * @param inbound       channel输入对象\n     * @throws Exception    异常\n     */\n    void handle(ChannelHandlerContext ctx,T inbound) throws Exception;\n}\n```\nICommandHandler接口是所有命令处理类的父接口，Netty ChannelHandler在处理请求时，根据不同的命令，寻找对应的处理类。\n\n## 一些设计想法\n### 心跳与屏幕截图\n心跳和屏幕截图都是定时向服务器发送，所以在设计时这两者同时只有一个活动即可。即发送心跳时不发送屏幕截图，发送屏幕截图时不发送心跳，控制结束后，继续发送心跳。这两者之间的控制由Puppet模块中 ***ConnectCommandHandler*** 类中的 ***HeartBeatAndScreenSnapShotTaskManagement*** 内部类控制。\n\n### 命令分层\n通过对用例和流程的分析，发现命令出现的频率比较高，于是考虑将命令处理单独独立出来，采取动态加载的方式，使其与ChannelHandler解耦，使用后期扩展，而且当命令很多时，不需要一次都加载，只是在使用时按需加载，减少JVM加载类的字节码量，此处参考了SPI思想。而添加命令，势必会修改界面，我使用模板模式，预留出菜单，界面体，界面属性设置等，修改时只需继续相关类并修改，然后在spring配置文件进行配置即可。\n\n### 序列号和Puppet名称生成器\n请求和响应类中都有ID属性，其中一部分是通过序列号生成器生成的，所以提供了 *SequenceGenerate* 接口和一个简单的实现类SimpleSequenceGenerator。同理还有当傀儡连接服务器时，服务器生成唯一的傀儡名，也提供了一个简单的实现类SimplePuppetNameGenerator。\n\n### 图像处理\n图像的数据相对于纯命令来说大了许多，所以需要想办法减少图像传输的数据，大致有两种方式：\n- **选择合适的图片格式，并进行压缩**：我这里选择了jpg格式，并使用Google Thumbnailator工具进行等宽高压缩，因为jpg具有较高的压缩比,但是代价是压缩后图像的质量不是太理想。\n- 只传输变化的图像：很多时候图像变化的部分并不太多，可以只传输变化的区域，传输到控制端后，控制端只绘制变化的区域。  \n    (1). **像素级别**: 我的思路是在傀儡端保持前一次传输时的截屏，和本次截屏图像进行像素级的比较，将不同的像素保存到一个对象数组中，记录像素的位置和像素值，传输到控制端后，根据像素位置和要替换的像素进行绘制    \n    (2). **区域级别**：只记录变化图像的开始点(左上角)和结束点(右下角)，然后绘制以这两个点框定的矩形式区域。   \n我尝试了这两种方式，没有达到很好的效果，由于时间有限，没有更深入研究，最终采取了压缩图像的方式。若有更好的方式，可以通过继承Puppet模块中抽象类*AbstractRobotReplay*，实现屏幕截屏方法*byte[] getScreenSnapshot()*,然后继承Master模块中抽像类*AbstractDisplayPuppet*实现其中的paint方法(也可以继承现有的实现类 *PuppetScreen* ，覆盖相应的方法)，然后将自定义的类在spring配置文件中配置，替换掉现在的实现类即可。\n\n## 待优化\n- 快速按键的情况、双击时响应的比较慢。传输命令需要时间，所以快速按键时命令产生滞后现象，而傀儡端图像传输到控制端后，Swing是单线程处理AWT事件(鼠标、键盘、绘图等)，若此时仍在按键，则会阻塞，等到按键结束之后，再进行图像的绘制。进行了如下尝试。\n   \u003e 1. 将命令发送采用异步方式，将命令存放在队列中，开启一个线程依次处理，这样可以减轻awt工作负担。\n   \u003e 2. 鼠标移动时，在移动过程中不发送命令，等待移动结束发送：实现方式是移动事件响应方式中添加一个计数器，再采用一个延迟线程，判断计数器值是否变化，如果延迟时间到时仍没有变化，则发送“移动命令”，但当移动后单击，会先发送单击命令，再发送鼠标移动命令，也不可行。\n   \u003e 3. 傀儡端在发送屏幕截图时，与上一次进行比较，如果没有变化，则不发送，减少发送数据量，也减少awt负担。\n\n## 一点心得\n\n1. 需求分析很重要，分析需求中各对象的属性和行为，以及对象之间的关系，这是后面功能、领域模型、静态/动态模型分析的基础。\n2. 设计静态模型时，需要根据SOLID原则进行设计，例如远程控制中命令较多，就抽像出一层，为每个命令单独写处理逻辑(当然多个命令也可以共用同一处理逻辑)，既符合单一职责原则，又符合开闭原则，将影响降到最低，具体很大的灵活性。又如Master模块中的 ***IDisplayPuppet*** 接口，此接口是控制端显示傀儡屏幕的接口，供控制端主窗口 ***MasterDesktop*** 和 ***Listener*** 调用。\n```java\n/**\n * @author Cool-Coding\n *         2018/8/2\n * 傀儡控制屏幕接口\n */\npublic interface IDisplayPuppet {\n    /**\n     * 启动窗口显示傀儡桌面\n     */\n    void launch();\n    /**\n     * 刷新桌面\n     * @param bytes\n     */\n    void refresh(byte[] bytes);\n    /**\n     *\n     * @return 傀儡名称\n     */\n    String getPuppetName();\n}\n```\n接口中这三个方法前两个方法launch和refresh，都是主窗口启动傀儡控制窗口和刷新屏幕必须的方法，第三个方法是由于发送命令时，需要知道傀儡名称，而实体之间是面向接口设计的，所以需要提供获取傀儡自身名称的方法。\n\n3.日志、异常处理   \n\n   日志和异常处理是相当重要的，好的日志记录方式和好的异常处理方式能够使项目结构更加清晰，怎么样才算好呢，人者见仁，智者见智。\n\n  **日志**   \n  \n    1. 记录程序关键步骤的上下文信息，例如记录请求或响应的数据以及附加的消息，记录此处建议使用trace/debug级别。\n    2. 记录业务流程的日志，使用info/error级别，这一部分日志主要是应用日志，例如控制端发起控制，成功或失败消息。\n    3. 日志最好通过统一的口径记录，便于结构清晰和日志管理\n\n  **异常**   \n  \n    1. 一定不要catch异常不处理，而且不要catch Throwable，因为Throwable包括了Error和Exception,Error一般都是不可恢复的错误，无法在程序中手工处理，不应该catch住。\n    2. 一般下层在记录异常日志，并向上抛出后，上层不需要处理，直接继续向上抛出即可，如果为了让异常具体业务含义，便于异常问题查找，可以封装一些关键的业务异常。\n    3. 异常最好集中处理,如springmvc:将异常集中在一个异常处理类中处理。\n\n有两篇文章，我觉得不错，推荐给大家，我也从中参考了一些方法。\n\n[Java 日志管理最佳实践](https://blog.csdn.net/f525921307/article/details/50519443)\n\n[Java异常处理的10个最佳实践](http://www.importnew.com/20139.html)\n\n## 效果演示\n\n\u003e - **Centos6.5**：傀儡端\n\u003e - **Windows**： 控制端、服务器\n1. 启动服务器、傀儡、控制端   \n2. 复制傀儡名   \n![傀儡名](https://upload-images.jianshu.io/upload_images/6752673-802a8f5903d2aea6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)  \n也可以通过日志获取:\n ![](https://upload-images.jianshu.io/upload_images/6752673-573e31c59731cbd7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n3. 将名称输入控制端  \n![](https://upload-images.jianshu.io/upload_images/6752673-8ae57ded64d33d94.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n4. 控制端打开一个远程屏幕   \n![](https://upload-images.jianshu.io/upload_images/6752673-327308e1b21731d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n5. 可以进行鼠标(单击，双击，右键，拖动等)或键盘(单键或组合键等)操作，并可调整屏幕清晰度。\n![](https://github.com/Cool-Coding/photos/blob/master/remote-desktop-control/remote.gif)\n\n## 项目导入及运行\n- 开发工具:Intellij IDEA  \n- JDK1.8  \n1. IntelliJ IDEA\n   File-\u003eNew-\u003eProject from version control-\u003eGithub/Git  \n   输入Url:https://github.com/Cool-Coding/remote-desktop-control.git   \n   ![](https://github.com/Cool-Coding/photos/blob/master/remote-desktop-control/import.png)\n2. 导入后项目结构如下图，Maven会自动加载依赖的Jar包   \n   ![](https://github.com/Cool-Coding/photos/blob/master/remote-desktop-control/import_result.png)\n3. 调试运行\n   - 配置子项目server/master/puppet resources文件夹下对应的配置文件server-config.txt/master-config.txt/puppet-config.txt\n     主要是配置服务器IP与端口号，其它一般保持不变即可。\n   - 运行子项目server/master/puppet 类ServerStarter/MasterStarter/PuppetStart\n     配置好IP和端口后，分别运行Server/Master/Puppet端，Master和Puppet运行后会自动连接服务端，如果服务器不可用，\n     Puppet会不断连接，而Master会报出错误消息，以后需要手工点击菜单连接。\n4. 发布运行\n   直接使用maven打包成jar,然后分别执行对应打包模块的jar包即可\n      \n## 版本变化\n\n1. V0.1.0 \n\u003e 20180802\n- 实现控制端Master通过鼠标键盘远程控制傀儡端Puppet   \n2. V0.1.1\n\u003e 20180916\n- master: 命令使用队列方式，单线程消费，减轻awt压力，加快响应屏幕刷新\n- puppet获取屏幕截图时，若前后两次获取的屏幕截图无变化，则不发送。\n- bugs fixed\n  - puppet重连时之前发送心跳的任务仍在运行  \n3. V0.1.2\n\u003e 20181221\n- 当puppet重连服务端时，保持ID不变\n- master的命令保存到阻塞队列中，再由单线程取出发给服务器\n   多个mouse moved命令，只发送最后一个mouse moved命令，减少\n   无效命令和带宽流量\n- bugs fixed\n   - puppet两次截图不变化，则不再发送截图，如果master断开，则无法检测到，修改为屏幕截图无变化时，发送心跳数据包\n   - master由于某些原因在没有向puppet发送TERMINATE命令时断开，则Master再次请求被控制Puppet时，\n   若puppet中本次截图与上次截图数据一样，则不发送，则Master控制端会不显示puppet屏幕截图\n- 优化体验   \n   - master向puppet发送命令失败，如果鼠标移动，则会一有移动就发送，导致反复出现相同消息\n4. V0.1.3\n\u003e 20210117\n- 使用Maven打包\n- 修复master关闭链接后无法再次链接问题\n    \n4. V0.1.4\n\u003e 20210124\n- 优化puppet响应master控制命令性能\n- 解耦robot与puppet,为puppet提供[gorobot](https://github.com/Cool-Coding/robotgo)、javarobot两种robot,并可灵活配置; \n  puppet与gorobot之间通过GRPC通信\n      \n## 讨论\n**bug反馈及建议**：https://github.com/Cool-Coding/remote-desktop-control/issues\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcool-coding%2Fremote-desktop-control","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcool-coding%2Fremote-desktop-control","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcool-coding%2Fremote-desktop-control/lists"}