Java 的屏幕广播(基于UDP)
Java的屏幕广播,是基于UDP协议的,user datagram protocal 用户数据报协议,无连接,无顺序,不安全,但是作为发送实时数据还是十分常用的。
整个难点在于要字节制定协议,由于UDP的一个包最大不能超过64K,而一帧屏幕截图(1366*768)是肯定超过64K的,所以我们需要对所截出来的image进行分割发送。
假设我们将一张屏幕截图分割成若干个包发送出去,要构成屏幕广播,就需要不断的截图发包,接收端就肯定需要知道哪几个包是同一张图片,而在这几个包中哪个包是图片的哪一部分,便于恢复成一张完整的图片。
如此就需要一个协议来规定各个包之间的关系。
在下面的程序中,每张图片之间我用时间戳来区别,图片中分割的各个部分用编号来确定,还要加上每张图片所分割的数量,就构造了一个数据报包。
首先的代码是一个工具类,将byte转换成long、int,还有反转
package Boardcast;
* 用于存放byte转换成long,int的工具代码
public class Utils {
public static byte[] long2Byte(long number) {
byte[] b = new byte[8];
for(int i=0; i<8; i++) {
b[i] = (byte) (number>>((8-i-1)*8));
}
return b;
}
* 从offset开始往后的8个字节转换为long数据
public static long byte2Long(byte[] b, int offset) {
long end = 0;
for(int i=0; i<8; i++) {
end = end | ((long)(b[i+offset] & 0xff)<<((8-i-1)*8));
* 其中的long类型转换一定要加上,如果没加上结果就成了int类型
}
return end;
}
public static byte[] int2Byte(int number) {
byte[] b = new byte[4];
b[0] = (byte) (number >> 24);
b[1] = (byte) (number >> 16);
b[2] = (byte) (number >> 8);
b[3] = (byte)number;
return b;
}
public static int byte2Int(byte[] b, int offset) {
int i3 = (b[0+offset] & 0xFF)<< 24;
int i2 = (b[1+offset] & 0xFF)<< 16;
int i1 = (b[2+offset] & 0xFF)<< 8;
int i0 = b[3+offset] & 0xFF;
return i3 | i2 | i1 | i0;
}
}
下面是广播者,发送截屏后过压缩,将压缩后的数据分割后进行发送
package Boardcast;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
* 广播者,屏幕截图后转换成byte[]格式,过压缩,分割成一个个60K左右的小包发送出去
* 对于同一帧的图片分割出来的小包,有着相同的时间戳,同样的包数量,不同的编号,用于区分
public class Boardcaster {
private DatagramSocket socket;
private Robot robot;
private Rectangle rect;
public static void main(String[] args) {
Boardcaster bc = new Boardcaster();
System.out.println("开始发送数据。。。。");
bc.start();
}
public void start() {
try {
socket = new DatagramSocket(8888);
* 实例化一个Robot,用于抓图
robot = new Robot();
* 设置所抓图片的位置,长宽
rect = new Rectangle(0, 0, 1366, 768);
while(true) {
popDatagramPacket(socket);
System.out.println("发送一帧图片");
}
} catch(Exception e) {
e.printStackTrace();
}
}
* 抓图
public BufferedImage getPrintScreen() {
BufferedImage image = robot.createScreenCapture(rect);
return image;
}
* 抓图,压缩,分割发送
private void popDatagramPacket(DatagramSocket socket) {
try {
* 抓图
BufferedImage image = getPrintScreen();
* 将图写入baos
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", baos);
System.out.println("未压缩的byte[]大小 = " + baos.toByteArray().length);
* 过压缩
byte[] buffer = compressImage(baos.toByteArray());
System.out.println("过压缩的byte[]大小 = " + buffer.length);
* 分割图片并发送出去
cutImageByteAndPost(buffer, socket);
} catch (Exception e) {
e.printStackTrace();
}
}
* 将image的byte[]过压缩,返回压缩后的byte[]
private byte[] compressImage(byte[] buffer) throws Exception {
* 新建一个baos,用于存放转压缩后的byte[]数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos);
ZipEntry entry = new ZipEntry("image.jpg");
* 放入条目
zos.putNextEntry(entry);
* 写入数据
zos.write(buffer);
zos.close();
baos.close();
return baos.toByteArray();
}
* 将数据的byte[]分割发送出去,同一帧的图片分割成的小包,有着相同的时间戳,同样的包数量,不同的编号
* 每一个小包,开始8个字节存放时间戳,接下来四个字节存放本帧图片所分割出来的包数量,再放入四个字节的编号
public void cutImageByteAndPost(byte[] src, DatagramSocket socket) {
try {
int len = src.length;
* 分割的包数量
int count = len/60/1024;
if(len > count*60*1024) {
count++;
}
System.out.println("len = " + len + ", count = " + count);
byte[] buffer = new byte[60*1024+8+4+4];
long time = System.nanoTime();
* 发count数量的包
for(int i=0; i
接收者,较广播方复杂一些
是用JFrame建立的界面,用于展示收到的屏幕截图
对上面的数据进行反解,取出各个小包中的内容数据,合成一个大的数据byte[],再解压缩成图片数据,最后转换成图片显示再界面中
在对数据报包合成时,使用Map来存储,时间戳作为key,value是TreeMap
package Boardcast;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipInputStream;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
* 接收屏幕广播界面
public class BoradcastScreenClientUI extends JFrame{
private static final long serialVersionUID = -580341982722536152L;
* label用于放置image
private JLabel label;
public static void main(String[] args) {
BoradcastScreenClientUI bsClient = new BoradcastScreenClientUI();
}
public BoradcastScreenClientUI() {
init();
getData();
}
* 初始化面板设置,设置一个label,放置image
public void init() {
this.setTitle("屏幕广播");
this.setBounds(270, 80, 1366, 768);
try {
BufferedImage image = ImageIO.read(new File("E://TestCase//day20//demo.jpg"));
ImageIcon icon = new ImageIcon(image);
label = new JLabel(icon);
label.setBounds(25, 25, 1366, 768);
} catch(Exception e) {
e.printStackTrace();
}
this.add(label);
* 监听窗口关闭事件,关闭窗口的同时停止程序
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.out.println("关闭窗口");
System.exit(-1);
}
});
* 设置面板可见
this.setVisible(true);
}
* 启动一个线程监听端口,接收数据,解析成image并放入面板中显示
public void getData() {
Thread t = new Thread() {
private DatagramSocket receiver;
@Override
public void run() {
try {
receiver = new DatagramSocket(9999);
byte[] buffer = new byte[64*1024];
DatagramPacket pack = new DatagramPacket(buffer, buffer.length);
System.out.println("等待接收数据");
* 用Map来存放image的各个包数据,Long是接收到数据包的时间戳,Integer是包的编号,
* PackInfo中存放了解析出来的包数据、包数量、包编号、时间戳
Map> map = null;
* key用于存放接收的上一个包数据的时间戳
long key = 0;
while(true) {
receiver.receive(pack);
System.out.println("收到一个数据报包 " + pack.getLength());
* 将收到的数据包解析成数据对象
PackInfo packinfo = parsePackInfo(pack);
* 如果接收的的数据时间戳大于之前的包的时间戳,则丢弃之前的map,新建一个map,放入新的数据
if(packinfo.getTime() > key) {
map = new HashMap>();
HashMap value = new HashMap();
value.put(packinfo.getNum(), packinfo);
map.put(packinfo.getTime(), value);
* 更新key的值
key = packinfo.getTime();
}else if(packinfo.getTime() == key) { * 如果时间戳相等则将包数据放入map中
map.get(key).put(packinfo.getNum(), packinfo);
* 检测是否够合成一张图片了,条件是map.value.size等于包的数量
checkMap(map);
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
};
* 作为守护线程,并启动
t.setDaemon(true);
t.start();
}
* 解析一个包的数据,并放入对象中返回
private PackInfo parsePackInfo(DatagramPacket pack) {
* 新建一个包数据对象存放数据
PackInfo packinfo = new PackInfo();
byte[] buffer = pack.getData();
* 取出时间戳,0-7的字节
long time = Utils.byte2Long(buffer, 0);
packinfo.setTime(time);
* 取出分割数量,8-11的字节
int count = Utils.byte2Int(buffer, 8);
packinfo.setCount(count);
* 取出当前编号,12-15的字节
int num = Utils.byte2Int(buffer, 12);
packinfo.setNum(num);
* 当前被压缩后的数据
byte[] data = new byte[pack.getLength()-16];
System.arraycopy(buffer, 16, data, 0, data.length);
packinfo.setData(data);
System.out.println(time + ", " + count + ", " + num + ", " + data.length);
return packinfo;
}
* 检测放入的小包是否等于包的总数,等于则合成一张图片
public void checkMap(Map> map) throws Exception {
Iterator>> it = map.entrySet().iterator();
while(it.hasNext()) {
Entry> entry = it.next();
PackInfo packinfo = entry.getValue().entrySet().iterator().next().getValue();
* 检测是否集齐了所有小包,集齐了就合成一帧图片
if(packinfo.getCount() == entry.getValue().size()) {
System.out.println("满了,合成图片");
* 将各个小包合成图片
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for(int i=0; i
程序的效果如下图
接受者启动的初始画面,后台线程正在等待接收数据
启动广播者,不断的在发送数据包
可以看到面板中的实时屏幕广播了
多线程下载器
使用java的多线程从服务器下载文件,我的服务器是在本地用tomcat搭建的
可以实现暂停下载和断点续传,断点续传的数据文件是用Properties来处理的。
每个线程下载后都会更新下载的数据文件来保存下载信息,使得在停止下载后可以续传。
package multidownload;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
* 使用多线程从服务器下载文件
public class DownloadUI extends JFrame implements ActionListener {
private static final long serialVersionUID = 5521199214537722822L;
public JTextField textUrl; * 下载的URL输入框
public JTextField textPath; * 保存路径输入框
public JTextField textThreadCount; * 线程数量
private JButton buttonStart;
private JButton buttonStop;
public JProgressBar bar; * 总进度条
public boolean flag = false; * 是否暂停下载标志位
public String ProfilePath; * 下载线程保存的数据文件路径
public Properties p; * 下载线程保存的数据文件
public static void main(String[] args) {
DownloadUI multiDown = new DownloadUI();
}
public DownloadUI() {
init();
}
private void init() {
this.setTitle("多线程下载");
this.setBounds(270, 80, 800, 600);
this.setLayout(null);
Font fontText = new Font("宋体", Font.ITALIC, 20);
Font fontLab = new Font("宋体", Font.BOLD, 27);
JLabel labSrcPath = new JLabel("URL地址");
labSrcPath.setFont(fontLab);
labSrcPath.setBounds(30, 30, 120, 60);
textUrl = new JTextField();
textUrl.setFont(fontText);
textUrl.setBounds(150, 30, 400, 60);
textUrl.setText("http://localhost:8000//demo.avi");
this.add(labSrcPath);
this.add(textUrl);
JLabel labAimPath = new JLabel("保存路径");
labAimPath.setFont(fontLab);
labAimPath.setBounds(20, 130, 150, 60);
textPath = new JTextField();
textPath.setFont(fontText);
textPath.setBounds(150, 130, 400, 60);
textPath.setText("E:\\TestCase\\day21\\demo.avi");
this.add(labAimPath);
this.add(textPath);
JLabel labThreadCount = new JLabel("线程数量");
labThreadCount.setFont(fontLab);
labThreadCount.setBounds(20, 230, 150, 60);
textThreadCount = new JTextField();
textThreadCount.setFont(fontLab);
textThreadCount.setBounds(150, 230, 400, 60);
textThreadCount.setText("3");
* 开始下载按钮
buttonStart = new JButton("开始下载");
buttonStart.setBounds(100, 320, 100, 40);
buttonStart.addActionListener(this);
* 停止下载按钮
buttonStop = new JButton("停止下载");
buttonStop.setBounds(250, 320, 100, 40);
buttonStop.addActionListener(this);
this.add(labThreadCount);
this.add(textThreadCount);
this.add(buttonStart);
this.add(buttonStop);
* 添加窗口事件处理程序,使用适配器
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.out.println("关闭窗口");
System.exit(-1);
}
});
* 进度条,作为总量进度条
bar = new JProgressBar();
bar.setBounds(50, 380, 700, 50);
this.add(bar);
this.setVisible(true);
}
* 对按钮的事件监听和处理
@Override
public void actionPerformed(ActionEvent e) {
try {
Downloader downloader;
if (e.getSource() == buttonStart) { * 要判断是新的下载还是断点续传
System.out.println("点击开始按钮");
* 获取面板上的数据
String url = textUrl.getText();
String savePath = textPath.getText();
int count = Integer.parseInt(textThreadCount.getText());
System.out.println("URL地址:" + url + ", 保存路径:" + savePath + ", 线程数:" + count);
* 为了可以断点续传,需要存储传输的各个线程节点的信息,线程的开始位置,传输数量等
* 构造数据文件的路径,和保存的文件路径在同一个位置,命名的也相同,后缀名为.properties
ProfilePath = savePath.substring(0, savePath.lastIndexOf(".")) + ".properties";
* flag是暂停的标志,检测是否是已经开启过线程下载了
if(flag) {
System.out.println("下载已暂停,请点击继续下载");
}else {
File file = new File(ProfilePath);
* 1.初始化下载器对象
downloader = new Downloader(url, savePath, count, this);
if (file.exists()) { * 断点续传
System.out.println("断点续传");
* 是断点续传的话就肯定会存在同名的数据文件,读取传输的量并继续下载
p = new Properties();
p.load(new FileInputStream(ProfilePath));
* 2.取得初始化列表对象
List lists = downloader.keepDownload();
System.out.println("文件总大小:" + downloader.fileSize);
* 3.给总进度条设置最大值
this.bar.setMaximum(downloader.fileSize);
* 4.按照线程数量动态添加进度条
List bars = addBar(lists);
* 5.开始下载
downloader.startDownload(bars);
}else { * 新的下载
System.out.println("开始新的下载");
* 2.取得初始化列表对象
List lists = downloader.prepareDownload();
* 3.将基本数据存入一个properties文件中
saveDownloadInfo(lists, downloader.fileSize);
System.out.println("文件总大小:" + downloader.fileSize);
* 4.给进度条设置最大值
this.bar.setMaximum(downloader.fileSize);
* 5.按照线程数量动态添加进度条
List bars = addBar(lists);
* 6.开始下载
downloader.startDownload(bars);
}
}
} else if (e.getSource() == buttonStop) { * 检测是否停止
System.out.println("点击停止按钮");
this.flag = !this.flag;
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
* 保存下载文件的下载数据信息
private void saveDownloadInfo(List lists, int fileSize) {
try {
p = new Properties();
* 设置公共的url,savePath,线程数量,文件总大小
p.setProperty("Thread.url", lists.get(0).getUrl());
p.setProperty("Thread.savePath", lists.get(0).getSavePath());
p.setProperty("Thread.count", lists.size() + "");
p.setProperty("Thread.fileSize", fileSize + "");
* 为每一个线程设置基本数据,下载的位置,还需要下载的size大小,下载的总数amount,线程编号index
for (Download d : lists) {
p.setProperty("Thread." + d.getIndex() + ".start", d.getStart() + "");
p.setProperty("Thread." + d.getIndex() + ".size", d.getSize() + "");
p.setProperty("Thread." + d.getIndex() + ".amount", d.getSize() + "");
p.setProperty("Thread." + d.getIndex() + ".index", d.getIndex() + "");
}
p.store(new FileOutputStream(ProfilePath), "Thread basic information for remember");
System.out.println("保存基本数据");
} catch (Exception e) {
e.printStackTrace();
System.out.println("保存初始化数据出错");
}
}
* 按照线程个数动态添加进度条
private List addBar(List lists) {
List bars = new ArrayList();
bar.setValue(0);
* allAmount是为总进度条设置的
int allAmount = 0;
for (Download download : lists) {
JProgressBar b = new JProgressBar();
b.setBounds(50, 420 + (download.getIndex() + 1) * 30, 700, 25);
int amount = Integer.parseInt(p.getProperty("Thread." + download.getIndex() + ".amount"));
b.setMaximum(amount);
b.setValue(amount - download.getSize());
allAmount = allAmount + amount - download.getSize();
this.add(b);
bars.add(b);
}
this.bar.setValue(allAmount);
this.repaint();
return bars;
}
}
package multidownload;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JProgressBar;
* 这是一个下载器,启动下载线程
public class Downloader {
private String url; * 下载的url
private String savePath; * 本地保存路径
private int count; * 线程数量
private List lists; * 下载线程数的info列表
public int fileSize; * 要下载的文件大小
private DownloadUI downui; * 对应的就是DownloadUI传过来的对象
* 初始化
public Downloader(String url, String savePath, int count, DownloadUI downui) throws Exception {
this.url = url;
this.savePath = savePath;
this.count = count;
this.downui = downui;
URL u = new URL(url);
URLConnection conn = u.openConnection();
fileSize = conn.getContentLength();
}
* 下载前的初始化工作
public List prepareDownload() {
try {
* 每个线程下载的基本数量,最后一个线程下载的量要另外计算
int block = fileSize/count;
lists = new ArrayList();
for(int i=0; i keepDownload(){
lists = new ArrayList();
int count = Integer.parseInt(downui.p.getProperty("Thread.count"));
String url = downui.p.getProperty("Thread.url");
String savePath = downui.p.getProperty("Thread.savePath");
downui.textThreadCount.setText(count+"");
downui.textUrl.setText(url);
downui.textPath.setText(savePath);
for(int i=0; i bars) {
for(Download download : lists) {
System.out.println(download);
JProgressBar b = bars.get(download.getIndex());
new DownloadThread(download, b, downui).start();
}
}
}
* 下载线程,每个线程下载的位置信息从download对象中取得
* 每个线程操作的Properties对象都是同一个,如果不是同一个就会有数据丢失的风险
class DownloadThread extends Thread{
private Download download;
private JProgressBar b;
private DownloadUI downui;
public DownloadThread(Download download, JProgressBar b, DownloadUI downui){
this.download = download;
this.b = b;
this.downui = downui;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " download = " + download);
try {
* 连接
URL url = new URL(download.getUrl());
URLConnection conn = url.openConnection();
* 设置请求头信息
conn.setRequestProperty("Range", "bytes=" + download.getStart() + "-" + (download.getStart()+download.getSize()));
System.out.println(Thread.currentThread().getName() + "bytes=" + download.getStart() + "-" + (download.getStart()+download.getSize()));
InputStream in = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile(download.getSavePath(), "rw");
raf.seek(download.getStart());
int len = -1;
byte[] buffer = new byte[1024];
while((len = in.read(buffer)) != -1) {
* 检测是否停止下载,不要使用while(downui.flag);这句来堵塞进程,效果不好
while(downui.flag) {
Thread.sleep(200);
};
raf.write(buffer, 0, len);
b.setValue(b.getValue()+len);
synchronized (downui) {
downui.bar.setValue(downui.bar.getValue()+len);
downui.p.setProperty("Thread."+download.getIndex()+".start", Integer.parseInt(downui.p.getProperty("Thread."+download.getIndex()+".start"))+len + "");
downui.p.setProperty("Thread."+download.getIndex()+".size", Integer.parseInt(downui.p.getProperty("Thread."+download.getIndex()+".size"))-len + "");
downui.p.store(new FileOutputStream(downui.ProfilePath), "");
}
}
raf.close();
in.close();
* 检测文件是否下载结束了,结束后删除线程数据文件(不过是删除失败的)
if(isEnd()) {
System.out.println(Thread.currentThread().getName() + " 文件下载结束了");
System.out.println("删除数据文件");
downui.p = null;
File f = new File(downui.ProfilePath);
if(f.delete()) {
System.out.println("删除成功");
}else {
System.out.println("删除失败");
}
}
} catch(Exception e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + "线程下载出错");
}
}
* 检测所有线程是否下载结束了,注意:Properties是线程安全的,其内部使用了synchronized
private boolean isEnd() {
int count = Integer.parseInt(downui.p.getProperty("Thread.count"));
for(int i=0; i 0) {
return false;
}
}
return true;
}
}
* 储存单个线程的下载信息
class Download{
private String url;
private String savePath;
private int start;
private int size;
private int index;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getSavePath() {
return savePath;
}
public void setSavePath(String savePath) {
this.savePath = savePath;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
@Override
public String toString() {
return "Download [url=" + url + ", savePath=" + savePath + ", start=" + start + ", size=" + size + ", index="
+ index + "]";
}
}
暂停下载
关闭程序之后再店家开始下载,可以实现断点续传
下载完成