
简介基于Java C/S架构的远程监控系统完整项目包面向Java网络编程学习者、毕业设计开发者及对远程控制技术感兴趣的工程师。源码实现了连续截取被监控端屏幕变化、硬盘文件上传下载、鼠标键盘模拟、远程执行DOS命令、远程关机重启等功能核心采用Java Socket通信与Robot类屏幕捕获/事件重演开发平台为Eclipse。压缩包共78个文件包含30个Java源文件、41个编译后的class文件、1份Word论文文档及工程配置文件整体仅1.56MB源码结构清晰便于对照论文理解各模块设计。目前已有1039人学习下载。配套论文涵盖需求分析、概要设计、关键技术如屏幕截取、命令收发、文件传输与编码实现读者可借此掌握C/S远程监控系统的完整开发思路并直接复用或改进代码用于课程设计或商业项目。1. 一份课程设计级的 JAVA C/S 远程监控源码包值得拆开看什么如果你正在找一份能直接跑起来的 JAVA C/S 远程监控系统源代码作为课程设计、毕业设计或者局域网内自用工具的学习底座这个资源包值得你在 Eclipse 里花一个晚上拆开。它对应的场景很典型主控端用 Java Socket 建立连接被控端用 AWT Robot 截取屏幕并回传再配合文件上传下载、鼠标键盘模拟和 DOS 命令执行拼成一个完整的 C/S 闭环。资源包同时带了源代码和配套的 WORD 论文文档论文部分是按照国家标准的软件工程流程写的从需求分析到概要设计再到编码和测试内容结构可以直接参考着改自己的论文。我把它下下来翻过一遍之后最直接的感受是这份代码的工程组织方式比网上很多碎片化的单文件 demo 完整得多。被控端有隐藏运行和开机自启的逻辑主控端有独立的命令处理界面文件传输是线程化的。当然它的开发平台标注是 JDK 1.5.0 Eclipse 3.1 Windows XP这个年代背景决定了你在新机器上复现时会碰到一些兼容性坑后面我会逐条列出来。2. 系统架构与核心原理Socket 和 Robot 是怎么组合成一套远程控制方案的2.1 命令通道设计一条“命令名:端口”消息如何打通两台机器这套系统的网络结构是典型的 C/S 模型加双通道设计。主控端先启动并监听一个 TCP 端口被控端启动后则打开一个指定的 UDP 端口用于接收命令。主控端要控制某台被控端时会先向被控端的 UDP 端口发送一条文本消息消息格式固定为ordername:port中间用冒号分隔。被控端收到后解析出命令名和端口号再主动用 TCP 去连接主控端上对应命令的服务端口。我最初看这个设计的时候觉得绕把连接方向做成被控端主动连主控端但想深一层就明白了多数个人电脑处于 NAT 后面主控端主动连被控端经常连不上反过来被控端向外发起连接反而更容易穿透。这也是为什么后来不少远控工具都采用了同样的反向连接思路。public void startCommandListener(int udpPort) { DatagramSocket socket new DatagramSocket(udpPort); byte[] buf new byte[128]; while (!shutdownFlag) { DatagramPacket packet new DatagramPacket(buf, buf.length); socket.receive(packet); String message new String(packet.getData(), 0, packet.getLength(), UTF-8); // 解析命令格式约定为 ordername:port String[] parts message.split(:); if (parts.length 2) { String orderName parts[0].trim(); int tcpPort Integer.parseInt(parts[1].trim()); executeOrder(orderName, tcpPort); } else { log(无法解析的命令: message); } } }这段逻辑对应资源包里OrderMap.java和Client.java这一组类的职责OrderMap维护命令名和命令处理实现类的映射关系Client负责启动 UDP 监听。需要注意executeOrder里要根据命令名找到对应的处理类再以新线程方式运行不能阻塞 UDP 接收循环否则后续命令会全部排队超时。双通道的好处还体现在数据流的隔离上屏幕截图的数据量很大如果和命令控制共用一条 TCP 连接一个慢速的截屏线程会堵住后续指令的收发。资源包里把命令通道和业务数据通道分开文件上传、截屏传输各自独立连接这是很符合工程实践的做法。2.2 屏幕截取链路Robot 抓屏、JPEG 压缩、TCP 传输屏幕监控的核心是java.awt.Robot。这个类可以生成屏幕坐标上的鼠标移动、键盘按键等系统级事件也可以直接截取屏幕区域返回BufferedImage。整套系统的原理就是把 Robot 截到的图像序列通过网络传给主控端再由主控端把图像画在画布组件上以此实现“实时桌面预览”。public void run() { try { Robot robot new Robot(); Rectangle screenRect new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); while (running) { BufferedImage screen robot.createScreenCapture(screenRect); ImageIO.write(screen, jpeg, socket.getOutputStream()); // 控制抓屏间隔数值越小画面越连贯CPU 占用也越高 Thread.sleep(snapInterval); } } catch (Exception e) { log(屏幕截取线程异常: e.getMessage()); } }这段代码对应资源包里的SendImageThread.java。实际使用时snapInterval我一般会设置在 100 到 200 毫秒之间低于 100 毫秒每秒 10 帧以上的 JPEG 编码开销会显著拉高 CPU高于 300 毫秒画面会明显卡顿远程操作时鼠标定位困难。这里还有一个容易被忽略的点ImageIO.write默认的 JPEG 压缩质量不算高如果对画面清晰度有要求建议用ImageWriter手动指定压缩比JPEG 质量参数在 0.6 到 0.8 之间比较合适。同时要把socket.getOutputStream()包一层BufferedOutputStream否则每次 JPEG 写入都是一次系统调用局域网内还不明显跨网段时吞吐量会差很多。再看资源包里GetImageThread.java对应的接收端。它的任务是从 socket 输入流读图像字节流再解码成BufferedImage传给主控端界面刷新。这里的配套参数是图像编码格式和时间间隔接收端每收到一帧就更新一次画面主控端面板上不需要做额外的缓存。BufferedImage img ImageIO.read(socket.getInputStream()); if (img ! null) { Graphics g canvas.getGraphics(); g.drawImage(img, 0, 0, canvas.getWidth(), canvas.getHeight(), null); g.dispose(); }注意drawImage的缩放行为canvas的宽高和被控端屏幕分辨率往往不一致直接把图像塞进去会变形。正确的做法是先算好缩放比例再用drawImage的缩放重绘版本或者干脆让主控端窗口跟随被控端分辨率变化。double scaleX (double) canvas.getWidth() / screenWidth; double scaleY (double) canvas.getHeight() / screenHeight; g.drawImage(img, 0, 0, canvas.getWidth(), canvas.getHeight(), null); // 后续鼠标坐标换算同样使用该比例我一般会在canvas重绘事件里统一处理缩放避免GetImageThread直接操作 UI 组件造成线程安全隐患。这块代码写成单线程循环是可行的但在 Swing 里频繁刷新界面最好用SwingUtilities.invokeLater切回事件调度线程。3. 主控端与被控端拆解核心类职责与关键代码走读3.1 主控端界面与事件采集鼠标键盘事件如何封装并转发主控端的核心类集中在MainFrame.java和ControlInfo.java中。MainFrame负责搭建控制窗口窗口里放一个用于显示远程画面的画布同时监听这个画布上的鼠标事件和键盘事件。ControlInfo则封装每次操作的类型标识和参数比如鼠标按下、鼠标移动、键盘输入以及对应的坐标或键码。canvas.addMouseMotionListener(new MouseMotionAdapter() { Override public void mouseMoved(MouseEvent e) { // 把画布坐标换算为被控端实际分辨率下的坐标 int remoteX e.getX() * screenWidth / canvas.getWidth(); int remoteY e.getY() * screenHeight / canvas.getHeight(); ControlInfo info new ControlInfo(); info.setType(ControlInfo.MOUSE_MOVE); info.setX(remoteX); info.setY(remoteY); sendControl(info); } });这段代码对应资源包里的MouseOnPanel.java和OpreationMenu.java相关逻辑。坐标换算公式是这套系统的关键细节主控端只能拿到画布坐标而画布大小和远程屏幕不一定一致所以要把(canvasX, canvasY)按比例映射到(screenWidth, screenHeight)坐标系里。资源包原始代码里用的是位图坐标换算我在复现时发现如果直接把事件发送过去远程机器的鼠标位置和主控端画面上的鼠标位置完全对不上必须严格按比例换算。sendControl方法内部会把ControlInfo对象通过ObjectOutputStream写到 socket 流里。这里我用的是 Java 原生序列化不推荐为了省流量手动拼字符串序列化简单而且兼容性够好。需要注意ControlInfo类必须实现Serializable接口并且类名包名要和被控端完全一致否则反序列化直接报错。3.2 被控端事件重演与隐藏自启Robot 模拟操作的工程细节被控端收到ControlInfo消息面板后再用 Robot 重演这些事件。比如消息类型是鼠标移动就调用robot.mouseMove(remoteX, remoteY)是按键就调用robot.keyPress和robot.keyRelease。switch (info.getType()) { case ControlInfo.MOUSE_MOVE: robot.mouseMove(info.getX(), info.getY()); break; case ControlInfo.MOUSE_PRESS: robot.mousePress(InputEvent.getMaskForButton(info.getButton())); break; case ControlInfo.KEY_PRESS: robot.keyPress(info.getKeyCode()); break; case ControlInfo.KEY_RELEASE: robot.keyRelease(info.getKeyCode()); break; }鼠标按键这块有个坑不同平台上InputEvent.BUTTON1_DOWN_MASK的值不一致直接硬编码InputEvent.BUTTON1_MASK在 JDK 高版本上有问题。我一般会在事件封装端就把按钮值转换成getMaskForButton(button)的标准值接收端再反解。被控端隐藏运行是另一个重点。资源包里的autostart.java和客户端启动类做了两件事没有可见窗口以及随系统启动自动运行。早期的做法是把窗口setVisible(false)然后用启动项方式注册到注册表。资源包里还带了SFileUpThread.java、CStoreFileThread.java这组文件传输线程它们在隐藏窗口模式下依然能正常读写文件。这里我提一下我的习惯被控端启动后先在系统托盘放一个不可见的Frame既能持有 AWT 组件上下文又不干扰用户。手动注册自启动的做法是把程序路径写进注册表的HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run键或者直接复制一份 jar 到启动文件夹。不过 Windows 10 之后很多安全软件会对注册表启动项弹窗实测体验糟糕有条件的话我更推荐用sc create注册成系统服务的方式这在资源包里没有现成代码需要自己补。3.3 线程关系梳理图像线程和文件线程为什么必须分开资源包里的线程设计是典型的“一功能一线程”。SendImageThread持续发送截屏流GetImageThread持续接收文件上传下载分别由SFileUpThread、CStoreFileThread独立处理主控端的ServerDOSOrderUI.java再开一条命令通道执行远程 DOS 命令。各线程之间的数据流不交叉靠Parameter.java里记录的端口、IP、间隔时间等参数串联起来。public class Parameter { public static int UDP_PORT 8080; public static int IMAGE_PORT 9001; public static int FILE_PORT 9002; public static int CMD_PORT 9003; public static int snapInterval 150; public static String savePath C:/monitor_files; }把这些参数集中在一个类里的好处是主控端和被控端各自维护一份参数文件改端口或改截屏间隔只需调整一个类不需要到处翻。常见错误是在多个线程类里硬编码端口被控端换了网络环境后根本连不上。我接手这类旧代码时第一件事就是先扫一遍所有new Socket的地方把端口收回到Parameter里。4. 文件传输与磁盘操作上传下载的流处理和细节坑4.1 文件上传下载的协议封装文件名、文件长度、字节流文件传输这块资源包里的实现思路是传输方先发送文件名和文件长度接收方根据长度判断接收进度和结束时机。上传方向是被控端向主控端传文件下载方向反过来但两端的流处理逻辑对称。public void run() { try (Socket socket new Socket(host, port); DataInputStream in new DataInputStream(socket.getInputStream()); FileOutputStream fos new FileOutputStream(saveDir File.separator in.readUTF())) { long fileSize in.readLong(); byte[] buffer new byte[8192]; long received 0; int len; while (received fileSize (len in.read(buffer)) ! -1) { fos.write(buffer, 0, len); received len; } } catch (IOException e) { log(文件接收失败: e.getMessage()); } }参数说明saveDir是被控端或主控端的保存目录8192字节的缓冲块在局域网内是比较合理的值文件长度字段用long而不是int因为int最大值只有约 2GB传大文件时会溢出。发送端则要按同样顺序先写文件名、再写长度然后循环从本地文件流读取并写出。这一步顺序错了接收端会把文件名解析成长度整个传输直接就崩了。我看到有不少人在这段代码上栽过跟头发送端用FileInputStream.read从文件读取但read不保证一次读满缓冲需要循环直到返回 -1否则大文件传输时后半段数据会丢失。4.2 DOS 命令执行与输出回传别直接用 Runtime.exec远程执行 DOS 命令用的是ProcessBuilder比Runtime.exec更可控。DOSExcuter.java这个类的职责就是把主控端发来的命令字符串交给操作系统的 cmd 解释器执行再把输出回传给主控端显示在ServerDOSOrderUI界面上。Process process new ProcessBuilder(cmd.exe, /c, command) .redirectErrorStream(true) // 合并标准输出和错误输出避免 read 阻塞 .start(); try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream(), GBK))) { String line; while ((line reader.readLine()) ! null) { // 把输出逐行发送给主控端 sendCmdOutput(line); } } process.waitFor();注意这里的字符编码必须用GBK而不是默认的UTF-8。Windows XP 和大量中文版 Windows 系统的 cmd 输出都是 GBK 编码用 UTF-8 读会得到一堆乱码。同时redirectErrorStream(true)是必须的否则子进程的输出管道缓冲区满时线程会卡死在readLine()上。另外ProcessBuilder传参时不要手动拼整个命令字符串把命令体拆成参数列表传入。比如目录切换命令要写成new ProcessBuilder(cmd.exe, /c, cd /d path dir)一次调用完成否则每次exec都会开一个新 shell 进程之前的cd操作不会保留。4.3 跨盘符与目录权限问题远端操作时最容易翻车的点远程文件上传下载时被控端的路径往往不在程序所在分区比如程序装在 C 盘用户文件放在 D 盘。如果资源包里用的相对路径C:/monitor_files那面对 D 盘文件就束手无策。我一般会在协议里增加一个路径字段允许主控端直接指定绝对路径然后在校验层把路径限定在合法盘符下。文件传输开始前先确认目录存在且可写常见的错误是不检查saveDir是否存在直接写入流抛出一堆FileNotFoundException。5. 避坑指南老代码在新环境下的典型翻车点5.1 现象被控端连上了但主控端收不到截图或者收到一张全黑的图原因高分辨率或高 DPI 缩放下Robot 截取的区域超出主控端桌面实际可访问范围有些精简版 Windows 的远程桌面会话里根本没有活动的桌面会话Robot 截下来就是黑屏。解决把主控端会话保持在前台或者用robot.createScreenCapture前先获取GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds()作为截取范围。如果程序是作为系统服务启动的要改成在用户会话里启动否则拿不到桌面上下文。5.2 现象文件传到一半连接断开日志里报SocketException: Connection reset原因发送端写完字节流后直接调用了socket.close()此时接收端可能还在从流里读数据或者发送端循环里某次read()返回 -1 后没有判断把 -1 当作正常数据签名。本质上是双方对“传输结束”的判定不一致。解决发送端在文件写完后先调用socket.shutdownOutput()等接收端确认再关闭连接。接收端在循环里判断received fileSize的同时也要处理len -1的情况打印缺失字节数方便排查是网络中断还是文件长度算错了。5.3 现象JDK 1.5 写的代码在 JDK 8 以上编译时报泛型或序列化错误原因JDK 5 之后的版本对泛型类型检查越来越严格旧的Vector、Hashtable裸类型代码在高版本 JDK 里直接警告甚至报错。ObjectInputStream在反序列化时如果找不到类定义也会抛出ClassNotFoundException两边类名或包名不一致就是这种报错。解决编译时用-Xlint:unchecked查看具体告警项重点检查OrderMap、ControlInfo相关类的包名是否在两端一致如果 JDK 版本实在太新可以在项目里保留一个 JDK 8 的编译环境旧代码不要硬上最新 LTS。5.4 现象远程关机或重启失败被控端没有反应原因Windows 从 Vista 开始普通权限的 Java 进程不能直接调用shutdown.exe系统会弹出 UAC 提示或者直接拒绝。资源包的论文里写的是直接Runtime.getRuntime().exec(shutdown -s -t 0)在 XP 上可行Win7 以上大概率不行。解决把被控端进程以管理员权限运行或者在调用 shutdown 命令前先用ProcessBuilder执行runas /user:Administrator提权更干净的方案是把被控端注册成 Windows 服务以 SYSTEM 账户运行这样关机命令不会被 UAC 拦。5.5 现象主控端画面流畅但鼠标事件响应迟钝点击位置偏移原因canvas尺寸变化后没有同步更新坐标换算比例或者mouseMoved事件发送频率过高TCP 缓冲被塞满命令排队滞后。部分老代码里直接把e.getX()当作远程坐标发送没考虑分辨率比例偏移是必然的。解决在窗口setSize或resize时重新计算宽高比例鼠标移动事件做节流最简单的做法是记录上次发送坐标和时间50 毫秒内移动距离小于 2 像素就跳过不要每个 AWT 事件都往外发。这样既减少带宽占用又避免被控端鼠标来回抖动。6. 把 Eclipse 工程变成可交付的 jar 包打包参数与自启部署细节拿到这份资源包后你在 Eclipse 里直接Run As是可以跑通的但交付给别人或者部署到被控端机器时总不能要求对方装 Eclipse 吧。所以最后一件事是把工程打成可执行的 jar 包并配置好被控端的隐藏自启。先检查src目录下的 manifest 文件比如资源包里那个server.mf和moon.mf。server.mf对应主控端可执行 jar内容核心就两行指定Main-Class和Class-Path。Manifest-Version: 1.0 Main-Class: com.monitor.ConnectClientFrame Class-Path: lib/这里的Main-Class对应主控端入口类也就是资源包里的ConnectClientFrame.java被控端以Client.java或autostart.java作为入口。手动编写 manifest 时注意文件末尾必须换行否则 Java 会报 “Invalid or corrupt jarfile”。在命令行打包Eclipse 的 Export 向导也行但批处理脚本更适合反复构建。我先贴一套我最常用的命令行流程mkdir -p bin javac -encoding UTF-8 -d bin -cp . src/com/monitor/*.java jar cfme monitor-server.jar server.mf com.monitor.ConnectClientFrame -C bin . jar cfme monitor-client.jar client.mf com.monitor.Client -C bin .参数说明-encoding UTF-8解决中文注释和字符串的乱码问题这套老代码里大量用中文提示不加这个参数在高版本 JDK 下编译基本会报编码错误jar cfme后面的e参数用于指定入口类等价于在 manifest 里写Main-Class。打包结束后分别验证两个 jar 能否正常启动主控端执行java -jar monitor-server.jar被控端执行java -jar monitor-client.jar。被控端的自启部署我早期会写进注册表启动项。现在更稳定的做法是配合sc命令注册成系统服务让 bat 脚本作为启动命令sc create RemoteMonitor binPath cmd /c C:\monitor\start-client.bat start auto注意sc的等号后面一定要跟一个空格否则报参数错误。启动脚本里要设置工作目录因为 jar 包里的相对路径是按照部署目录解释的cd /d C:\monitor start javaw -jar monitor-client.jar最后一步验证。所有线程互相独立经常出现程序“看起来在运行但功能全失效”的情况。我验证时固定走一遍清单先看 UDP 端口有没有被监听用netstat -ano | findstr 8080再触发一次截屏看主控端画面是否刷新再传一个 100MB 以上的文件确认文件传输线程不会中途断连最后执行一条跨盘符的 DOS 命令比如dir D:\ /b确认输出正常回传。接手这类老代码时我的习惯是对照资源包里的论文需求功能清单逐项打钩。从那以后我做任何远程控制项目都会先把进程的工作目录、编码方式、网络端口这三个基础项固定住再谈功能扩展。这套流程虽然简单但每次都帮我省掉至少半个小时的排障时间希望帮到你。本文还有配套的精品资源点击获取