hello大家好,今天给大家带来一款用JDK17写的防向日葵的远程控制软件 源码。 此源码为完整版,适合大作业和毕设。对java感兴趣的还可以学习网络编程思路。本文也带来了系统实现的基本原理。
源码为自己开发的源码, 商用必究!!!
从源码种你可以学习到:
-
tcp网络编程基础,以及一些优化:"禁用Nagle算法,减少延时" , "启用TCP保活机制" 等等。
-
如何使用java进行屏幕录制,然后压缩。
-
如何实现一款中转服务器(为了使两个不同局域网的客户端进行远程连接)。
-
如何进行鼠标的坐标映射,以及屏幕dpi的处理。
-
如何进行键盘的映射,以及tab键无效的处理。
视频演示
www.bilibili.com/video/BV13A...
图片演示



技术栈描述
项目框架
JDK17 + Java Swing界面 + TCP Socket网络
完整游戏源码,我已经整理清楚,移步:
系统实现
中转服务器的实现
中转服务器就是为了将客户端A的指令透传到客户端B , 达到两个不同局域网的客户端进行网络交互。首先需要有客户端code对应客户端socket的映射类:

typescript
public class TcpClientSessions {
private static final TcpClientSessions instance = new TcpClientSessions();
// 存储客户端会话信息 <客户端code设备码 , session>
private final Map<String, ClientSession> clientSessions = new ConcurrentHashMap<>();
private TcpClientSessions() {
}
public static TcpClientSessions getInstance() {
return instance;
}
public void addSession(String code, ClientSession session) {
clientSessions.put(code, session);
}
public ClientSession getSession(String code) {
return clientSessions.get(code);
}
public ClientSession removeSession(TcpClientConnection connection) {
ClientSession session = null;
for (Map.Entry<String, ClientSession> entry : clientSessions.entrySet()) {
if (entry.getValue().getConnection() == connection) {
session = entry.getValue();
break;
}
}
if (session != null) {
clientSessions.remove(session.getCode());
}
return session;
}
}

代码用到的单例, 后面方便使用。ClientSession 就相当于一个客户端socket,后续转发消息都是先通过code找到这个ClientSession ,然后再发送消息。
客户端启动时需要注册,代表自己已经在线,下面是处理客户端注册的逻辑:

ini
private void handleRemoteCodeRequest(TcpClientConnection connection, BaseMessage message) {
try {
// 生成远程码和密码
String remoteCode = NetworkUtil.generateRemoteCode();
String password = NetworkUtil.generatePassword();
// 创建客户端会话
ClientSession session = new ClientSession();
session.setCode(remoteCode);
session.setPassword(password);
session.setConnection(connection);
// 存储会话信息
TcpClientSessions.getInstance().addSession(remoteCode, session);
RemoteCodeResponse response = RemoteCodeResponse.success(remoteCode, password);
BaseMessage responseMessage = new BaseMessage(MessageType.REMOTE_CODE_RESPONSE, response);
connection.sendMessage(responseMessage);
} catch (Exception e) {
e.printStackTrace();
RemoteCodeResponse response = RemoteCodeResponse.failure("服务器内部错误");
BaseMessage responseMessage = new BaseMessage(MessageType.REMOTE_CODE_RESPONSE, response);
connection.sendMessage(responseMessage);
}
}

有了这个全局的客户端code对应的socket映射,后续的发送自己的屏幕流数据,等指令都只需要相同的转发处理就可以了。
到此中转服务器核心就是如此。
被控客户端发送自己屏幕实现
客户端当收到发起屏幕的指令后, 需要通过 JavaRobotScreenService 来实现实时循环上传当前屏幕。 所以应该是放到一个线程里面实现:

scss
// 一直循环 上传屏幕流数据
new Thread() {
@Override
public void run() {
while (running) {
start0(receiverCode , senderCode);
try {
Thread.sleep(settingsService.getSettings().getRobotCaptureTime());
} catch (InterruptedException e) {
}
}
}
}.start();

为了防止cpu飙升,我们通过sleep 来控制上传的间隔,也相当于fps帧。 start0 为核心方法:

ini
private void start0(String receiverCode , String sendCode) {
try {
System.out.println("正在共享屏幕");
// 捕获屏幕截图
long s = System.currentTimeMillis();
BufferedImage screenshot = robot.createScreenCapture(screenRect);
s = System.currentTimeMillis() - s;
// 将图像转换为JPEG字节数组(包含质量压缩)
long s1 = System.currentTimeMillis();
byte[] imageData = imageToJpegBytesWithQuality(screenshot);
s1 = System.currentTimeMillis() - s1;
// 构建包含接收者信息的完整数据包
if (imageData != null && imageData.length > 0) {
ScreenData screenData = new ScreenData();
screenData.setReceoverCode(receiverCode);
screenData.setImageData(imageData);
screenData.setSendCode(sendCode);
screenData.setCaptureCost(s);
screenData.setEncodeCost(s1);
screenData.setDpiScale(dpiScale);
screenData.setNetworkStartTime(System.currentTimeMillis());
RemoteControlClient.getInstance().sendTCP(new BaseMessage(MessageType.SCREEN_DATA, screenData));
}
} catch (Exception e) {
log.error("捕获屏幕数据时发生错误", e);
}
}

首先通过robot工具捕获屏幕,然后进行高效压缩,转换成支持socket传输的字节流,进行发送。屏幕数据流首先发送到中转服务器,然后解析到receiverCode,转发到对应的客户端。
客户端接受屏幕流进行展示
展示数据很简单,就是将字节数组反转成 BufferedImage ,然后显示到组件上,一个重要的问题就是需要处理对方屏幕的dpi。 不然会出现显示不完整的问题。

ini
public void showScreenData(ScreenData data) {
// 在后台线程中处理图像解码,避免阻塞UI线程
TaskExcuteService.getInstance().getScreenUpdater().execute(() -> {
BufferedImage decode = null ;
remoteDpiScale = data.getDpiScale() ;
try {
if(data.getImageData() != null ) {
// java robot - 优化:直接使用ByteArrayInputStream,避免额外的包装流
decode = ImageIO.read(new ByteArrayInputStream(data.getImageData()));
// 更新帧大小显示
SwingUtilities.invokeLater(() -> {
frameSizeLabel.setText("帧大小: " + formatBytes(data.getImageData().length)+",捕获屏幕耗时:"+data.getCaptureCost()+"ms"
+",编码耗时:"+data.getEncodeCost()+"ms");
});
}if (decode != null) {
// 直接设置远程图像
remoteImage = decode;
connected = true;
// 计算缩放比例
Dimension panelSize = desktopPanel.getSize();
if (panelSize.width > 0 && panelSize.height > 0) {
scaleX = (double) panelSize.width / remoteImage.getWidth();
scaleY = (double) panelSize.height / remoteImage.getHeight();
// 保持图像比例
double scale = Math.min(scaleX, scaleY);
scaleX = scaleY = scale;
}
desktopPanel.repaint();
}
} catch (Exception e) {
log.error("显示屏幕数据失败: {}", e.getMessage());
}
});
}

客户端指令处理的统一处理器
为了代码的简洁性,将所有的指令都统一放到了 TcpMessageHandler 进行处理,这样后续迭代和开发都有保障。

typescript
@Override
public void accept(BaseMessage response) {
try {
// 处理不同类型的消息
switch (response.getMessageType()) {
case REMOTE_CODE_RESPONSE: // 获取的 设备码回应
applyCode((RemoteCodeResponse) response.getData());
break;
case REMOTE_CONNECT_RESPONSE: // 远程身份校验 回应
remoteCheck((RemoteConnectResponse) response.getData());
break;
case SHARE_SCREEN: // 需要分享自己的屏幕
startScreen(response);
break;
case SCREEN_DATA: // 收到了对方的屏幕数据
showScreenData(response);
break;
case MOUSE_EVENT: // 处理 鼠标事件
mouse(response);
break;
case KEYBOARD_EVENT: // 键盘事件
keyboard(response);
break;
case Close_Screen: // 关闭屏幕
closeScreen(response);
break;
case SETTINGS: // 设置
settings(response);
}
} catch (Exception e) {
e.printStackTrace();
}
}

系统部署
将源码导入到idea中,这个项目就是一个普通的maven管理的项目, 导入前,请设置好maven的仓库配置。

设置好JDK的环境为17

启动中转服务器

启动客户端
启动客户端需要先设置中转服务器的IP, 否则界面会提示连接失败。

启动main函数
