Java实现多线程远程投屏并打包可执行文件(从代码到.exe)
前言
又临近期末周了,各种PBL接踵而来。然后,就要去自主研讨室和组员开会,并与此同时发现研讨室的一体展示机并没有配数据线😥。于是,就要想尽办法将电脑投屏到一体机上去。所以,同学们就会选择去充乐播(一体机自带的投屏软件)的会员。或许,乐播的业务分析员会发现在期末月里,公司的会员数是有大幅提高的🤣。所以我就决定自己写一个投屏的软件,然后打包成exe,给研讨室的每一台一体机装上,属于是做好事不留名了😅。
原理
这个投屏软件的原理其实也是十分简单的,实现需要服务端与客户端。这两个端系统通过Socket进行连接,然后,服务端不停的进行截图操作,将这些截下来的图,转码为二进制流在传输给客户端,客户端在把二进制解码成图片,可能有同学问为什么要转为二进制流🧐,这里是因为一个截图文件过于的大,在传输的过程中,可能会造成阻塞而丢包。嗯,这就是整个过程,但是这里面还是有一些细节的。比如,对截屏的操作使用多线程线程里面,这样在截屏的过程中就不会影响到连接等。当然,这里同学们还可以改一改添加键鼠的响应,再把服务器部署到公网上就可以,实现远程操控了。😎😎
成品展示
这是两个exe文件(图标是别人的照片,打一波码😁)
这图标到时候打包的时候,大家想设成啥样就啥样。
点击后就会有一个对话框
这个涉及到一个方法,到时候讲解
点“是”以后,就会跳转到输入IP和端口号的界面(这个ip就是到时候服务器的区域网IP,由于我们把服务端给打包了,所以端口是固定的8888)。
点击确定就可以连接投屏了。
下图是电脑和Surface的投屏(随便开的一个网页哈,广告就大码了,大伙不要关注细节😅)
刚刚去研讨室试了一下,发现好用,
那就写个readme放那给同学们用吧😀
又是助人为乐的一天😂
代码实现
上面已经讲过原理了,这个小程序也不会太难,实现我们写一下他的客户端吧。
下面主要进行代码的解析,完整代码放置于其后。
客户端
生成询问框
我们看到客户端点击运行后,会有一个小对话框,这个询问框是JOptionPane中的showConfirmDialog()方法生成的
//询问框,showConfirmDialog()方法是展现询问框
int choice = JOptionPane.showConfirmDialog(null, "掌控对方电脑?", "霍格沃茨魔法学院秦皇岛分院", JOptionPane.YES_NO_CANCEL_OPTION);
询问框选择响应
询问框做完后,我们要完成其选择响应,首先是点击“否(NO)”或“取消(Cancel)”按钮就退出程序。
if (choice == JOptionPane.NO_OPTION || choice == JOptionPane.CANCEL_OPTION) {
return;
}
点击确定我们就要进入输入框,输入待连接的服务端的ip和端口
String input = JOptionPane.showInputDialog("请输入你要连接服务器的ip地址及端口号", "127.0.0.1:8888");
showInputDialog()方法中输入的ip和端口号是一个初始值,就同下图
截取输入字符串
在我们拿到服务端的ip和端口号后,我们先要把拿到的一整串字符串(包含:ip和端口)给分开
//获取服务器的主机 substring()方法用以截取字符串
String host = input.substring(0, input.indexOf(":"));
//端口
String port = input.substring(input.indexOf(":") + 1);
与服务端建立联系
分别得到ip和端口后,我们就可以建立其Socket连接服务端了
这里需要注意一点Socket()中需要的端口是Int型,而我们刚刚截下来的是String,我们可以用Integer.parseInt()方法将string包装成int
Socket client = new Socket(host, Integer.parseInt(port));
创建数据输入流
我们传送截图需要使用二进制数据流,这里就需要先写输入流,接收传送来的数据
DataInputStream dataInputStream = new DataInputStream(client.getInputStream());
创建窗口及面板
我们客户端连接到了以后是需要一个窗口,然后再在这个窗口的面板中加入传输过来的截图的,所以我们先创建一个窗口和面板,到时候把收到的截图添加进去就行。当然对于这个窗口我们也有些要求,比如,每个电脑都有不同,我们要让截图的分辨率适应窗口,以及我们为了更全的看图,就需要一个滑动滚轮等。
//创建显示面板
JFrame jFrame = new JFrame();
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setTitle("任意门");
jFrame.setSize(1024,768);
//读取服务端的分辨率
double height = dataInputStream.readDouble();
double width = dataInputStream.readDouble();
Dimension ds = new Dimension((int)width,(int)height);
jFrame.setSize(ds);
//创建面板
JLabel jLabel = new JLabel();
JPanel jPanel = new JPanel();
//设置滚动条
JScrollPane jScrollPane = new JScrollPane(jPanel);
jPanel.setLayout(new FlowLayout());
jPanel.add(jLabel);
jFrame.add(jScrollPane);
jFrame.setVisible(true);
jFrame.setLocationRelativeTo(null);
jFrame.setAlwaysOnTop(true);
获取截图
现在我们就可以获取服务端发送的截图,并将截图粘贴到面板中,首先,我们需要明确的是,我们是要不断地接收这个截图,以确保实时性,同样,服务端那边也会不停的给我们发送图片,这是要放到一个循环里面。
while(true){
//获取流的长度
int len = dataInputStream.readInt();
byte[] imageData = new byte[len];
dataInputStream.readFully(imageData);
//新建一个图片,并把截图数据传递过来
ImageIcon image = new ImageIcon(imageData);
//再把图标放到label面板中
jLabel.setIcon(image);
//重新绘制面板
jFrame.repaint();
}
这个便是客户端的代码,编的时候大家还会发现,需要抛些异常,直接就把父类异常抛出就行了。
服务端
服务端就是核心的功能就是:建立服务器监听,多线程截图以及图片转二进制数据流并流传输。
建立服务器监听
这个是socket里面的内容
//建立服务器监听
ServerSocket ss = new ServerSocket(8888);
System.out.println("正在法力追踪服务器>>>");
Socket clinet = ss.accept();
System.out.println("锁定服务器成功");
建立数据流传输
OutputStream outputStream = clinet.getOutputStream();
//将文件流转换成二进制数据,用于传输
DataOutputStream doc = new DataOutputStream(outputStream);
多线程截图
进行多线程截图的原因在原理处是提到过,这是由于图片文件过于的大,在传输过程中是比较占用带宽的,这是会导致传输层拥塞而丢包。
还是先要将数据传输流定义进来
//多线程截图
class DoorThread extends Thread{
private DataOutputStream dataout;
public DoorThread(DataOutputStream dataout){
this.dataout = dataout;
}
然后,启动线程。
这里我是先说一下窗口截图是如何实现的
窗口截图实现
实现窗口截图,我们需要知道几点:
- 1.我们用什么截图:Robot
- 2.我们截什么图:Rectangle
- 3.我们如何处理截图:JPEGImageEncoder
🆗,我们可以开始实践一下
截图我们要用到一个(小)机器人😕,并获取截图的区域(这个我们要使用Toolkit)
//创建一个机器人,机器人可以帮助我们截取屏幕
Robot robot = new Robot();
//获取屏幕的大小
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension dimension = toolkit.getScreenSize();
然后,我们就可以指定一个截图区域,然后再这个区域内使用Robot截图,再新建一个图片实体用来装载这个截图,再把这个截图做成图标装进Label
while (true) {
//指定分享区域
Rectangle rectangle = new Rectangle(jFrame.getWidth(), 0, dimension.width - jFrame.getWidth(), dimension.height);
//截取指定区域(分享区域)内的屏幕到一张图片中
BufferedImage bufferedImage = robot.createScreenCapture(rectangle);
//在把图片放到一个Label里,如何再把这个label加到jFrame里就行了
ImageLabel.setIcon(new ImageIcon(bufferedImage));
}
上面说了一下怎么截图,现在我们回到这个工程里面,把截图这块给完成了。
和上面几乎是一样的操作,就是我们再这里要进行一步图片的压缩操作,并进行字节流传输
截图操作还是一样的
//截图
BufferedImage bufferedImage = robot.createScreenCapture(rec);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
压缩要用到JPEGImageEncoder,但是有一件比较遗憾的事是这个类,应该是说sun.image 这个类包应该是在jdk7以后就没了,需要使用别的方法代替,为了省事直接用jdk7以下版本😂
//对图片进行压缩处理
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(byteArrayOutputStream);
encoder.encode(bufferedImage);
然后,我们开启字节流传输文件流
//字节流传输的文件流
byte[] data = byteArrayOutputStream.toByteArray();
dataout.writeInt(data.length);
dataout.write(data);
dataout.flush();
Thread.sleep(0);
ok,在把这些操作套在一个线程里面,并在主方法里面启用这个多线程
//启动线程
DoorThread doorThread = new DoorThread(doc);
doorThread.start();
行了,整个程序就好了😮😮😮
完整代码
整个project是这样的
Client类
package cn.edu.neu.Door;
import javax.swing.*;
import java.awt.*;
import java.io.DataInputStream;
import java.net.Socket;
/**
* 任意门的客户端
*/
public class Client {
public static void main(String[] args) {
try {
//询问框,showConfirmDialog()方法是展现询问框
int choice = JOptionPane.showConfirmDialog(null, "掌控对方电脑?", "霍格沃茨魔法学院秦皇岛分院", JOptionPane.YES_NO_CANCEL_OPTION);
//判断点击的按钮是什么 NO_OPTION这个就是一个常量
//如果点击了否
if (choice == JOptionPane.NO_OPTION || choice == JOptionPane.CANCEL_OPTION) {
return;
}
//输入ip地址和端口号
String input = JOptionPane.showInputDialog("请输入你要连接服务器的ip地址及端口号", "127.0.0.1:8888");
//获取服务器的主机 substring()方法用以截取字符串
String host = input.substring(0, input.indexOf(":"));
//端口
String port = input.substring(input.indexOf(":") + 1);
//链接服务器 Integer.parseInt()方法是将string包装成int
Socket client = new Socket(host, Integer.parseInt(port));
//创建输入流
DataInputStream dataInputStream = new DataInputStream(client.getInputStream());
//创建显示面板
JFrame jFrame = new JFrame();
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setTitle("任意门");
jFrame.setSize(1024,768);
//读取服务端的分辨率
double height = dataInputStream.readDouble();
double width = dataInputStream.readDouble();
Dimension ds = new Dimension((int)width,(int)height);
jFrame.setSize(ds);
//创建面板
JLabel jLabel = new JLabel();
JPanel jPanel = new JPanel();
//设置滚动条
JScrollPane jScrollPane = new JScrollPane(jPanel);
jPanel.setLayout(new FlowLayout());
jPanel.add(jLabel);
jFrame.add(jScrollPane);
jFrame.setVisible(true);
jFrame.setLocationRelativeTo(null);
jFrame.setAlwaysOnTop(true);
while(true){
//获取流的长度
int len = dataInputStream.readInt();
byte[] imageData = new byte[len];
dataInputStream.readFully(imageData);
ImageIcon image = new ImageIcon(imageData);
jLabel.setIcon(image);
//重新绘制面板
jFrame.repaint();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
Server类(包括多线程类)
package cn.edu.neu.Door;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 任意门服务类
*/
public class Server {
public static void main(String[] args) {
try{
//建立服务器监听
ServerSocket ss = new ServerSocket(8888);
System.out.println("正在法力追踪服务器>>>");
Socket clinet = ss.accept();
System.out.println("锁定服务器成功");
OutputStream outputStream = clinet.getOutputStream();
//将文件流转换成二进制数据,用于传输
DataOutputStream doc = new DataOutputStream(outputStream);
//启动线程
DoorThread doorThread = new DoorThread(doc);
doorThread.start();
}catch (Exception e){
e.printStackTrace();
}
}
}
//多线程截图
class DoorThread extends Thread{
private DataOutputStream dataout;
public DoorThread(DataOutputStream dataout){
this.dataout = dataout;
}
//开始启动线程
public void run() {
//获取屏幕的大小
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension dimension = toolkit.getScreenSize();
try {
//获取屏幕的分辨率 dataout数据输入流
dataout.writeDouble(dimension.getHeight());
dataout.writeDouble(dimension.getWidth());
dataout.flush(); //刷新
//定义分享区域大小(整个屏幕大小)
Rectangle rec = new Rectangle(dimension);
Robot robot = new Robot();
while(true){
//截图
BufferedImage bufferedImage = robot.createScreenCapture(rec);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//对图片进行压缩处理
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(byteArrayOutputStream);
encoder.encode(bufferedImage);
//字节流传输的文件流
byte[] data = byteArrayOutputStream.toByteArray();
dataout.writeInt(data.length);
dataout.write(data);
dataout.flush();
Thread.sleep(0);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
程序打包
将程序打成jar包
右键点击工程文件夹
点击Open Module Setting
点击Artifacts
点击小加号
在如上图选择
在选择执行的主类(比如客户端,就选Client)
然后,点击OK就行了,再去输出地址找找看。
上面那个exe4j可以忽略,这是或许操作得到的,对了,你们现在也确实要下一个exe4j这个工具可以把jar包转为exe
下载好打开就是这样的
前面就按自己需求填一下
到了这里,就是最初提到的自己定义ico的事了
推荐一个文件格式转换在线工具(好吧你们的png,jpg转为ico😁)
在这里你可以选择程序运行载编的位数,他默认的是32位。
到了最关键的一步了,这里先点击绿色的小加号,把我们刚刚打的jar包添加进去,然后,再在上面选择主类
这里要选择可执行的Java版本,别忘了我们的JPEGImageEncode是要求jdk7以下的,所以这里max版本填7以下
然后,后面按需选择一下,然后到了这一步,先点击运行一下,在保存,就行了😀
怎么样,是挺好用的吧😁