1.Java 的屏幕广播(基于UDP),2.多线程下载器

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<count; i++) {
                 * 如果是最后一个包,就将剩下的所有数据都发送出去
                if(i == count-1) {
                    buffer = new byte[len%(60*1024)+16];
                }
                 * 写入时间戳
                System.arraycopy(Utils.long2Byte(time), 0,  buffer, 0, 8);
                 * 写入分割的数量
                System.arraycopy(Utils.int2Byte(count), 0, buffer, 8, 4);
                 * 写入当前的编号,从0开始
                System.arraycopy(Utils.int2Byte(i), 0, buffer, 12, 4);
                 * 写入image内容
                System.arraycopy(src, i*60*1024, buffer, 16, buffer.length-16);
                
                System.out.println("i = " + (i) + ", buffer.length = " + buffer.length);
                
                 * 发送数据,组装成数据报包
                DatagramPacket pack = new DatagramPacket(buffer, buffer.length);
                 * 设置发送地址、端口
                pack.setSocketAddress(new InetSocketAddress("localhost", 9999));
                
                socket.send(pack);
                System.out.println("发送一个数据报包");
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

接收者,较广播方复杂一些

是用JFrame建立的界面,用于展示收到的屏幕截图
对上面的数据进行反解,取出各个小包中的内容数据,合成一个大的数据byte[],再解压缩成图片数据,最后转换成图片显示再界面中

在对数据报包合成时,使用Map来存储,时间戳作为key,value是TreeMap<Integer, PackInfo>,每个小包解析为一个PackInfo对象,Integer是存放包的编号,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<Long, HashMap<Integer, PackInfo>> 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<Long, HashMap<Integer,PackInfo>>();
                            HashMap<Integer,PackInfo> value = new HashMap<Integer,PackInfo>();
                            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<Long, HashMap<Integer, PackInfo>> map) throws Exception {
        Iterator<Entry<Long, HashMap<Integer,PackInfo>>> it = map.entrySet().iterator();
        while(it.hasNext()) {
            Entry<Long, HashMap<Integer,PackInfo>> 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<packinfo.getCount(); i++) {
                    PackInfo p = entry.getValue().get(i);
                    baos.write(p.getData());
                }
                baos.close();
                
                 * 解压缩数据
                byte[] b = unCompressImage(baos.toByteArray());
                
                System.out.println("未解压缩的数据长度 = " + baos.toByteArray().length);
                System.out.println("解压缩后的图片长度 = " + b.length);
                
                 * 设置图片到面板中
                BufferedImage image = ImageIO.read(new ByteArrayInputStream(b));
                ImageIcon i = new ImageIcon(image);
                label.setIcon(i);
                
                repaint();
                System.out.println("设置一张图片");
                 * 将设置过后的image数据从map中移除
                it.remove();
            }
        }
        System.out.println("字典长度 = " + map.size());
    }

     * 对合成的数据进行解压缩,返回解压的byte[]
    private byte[] unCompressImage(byte[] b) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(b);
        ZipInputStream zis = new ZipInputStream(bais);
        zis.getNextEntry();
        byte[] data = zis.readAllBytes();
        zis.close();
        bais.close();
        return data;
    }
}

 * 存放包的数据的实例类
class PackInfo {
    private long time;  *  包的时间戳
    private int count;  * 分割的包数量
    private int num;    *  当前的包编号
    private byte[] data;  * 包中存放的数据
    
    public long getTime() {
        return time;
    }
    public void setTime(long time) {
        this.time = time;
    }
    public int getCount() {
        return count;
    }
    public void setCount(int count) {
        this.count = count;
    }
    public int getNum() {
        return num;
    }
    public void setNum(int num) {
        this.num = num;
    }
    public byte[] getData() {
        return data;
    }
    public void setData(byte[] data) {
        this.data = data;
    }
}

程序的效果如下图

接受者启动的初始画面,后台线程正在等待接收数据


启动广播者,不断的在发送数据包

可以看到面板中的实时屏幕广播了



多线程下载器

使用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<Download> lists = downloader.keepDownload();

                        System.out.println("文件总大小:" + downloader.fileSize);
                        
                         *  3.给总进度条设置最大值
                        this.bar.setMaximum(downloader.fileSize);
                        
                         *  4.按照线程数量动态添加进度条
                        List<JProgressBar> bars = addBar(lists);

                         *  5.开始下载
                        downloader.startDownload(bars);

                    }else {  * 新的下载
                        System.out.println("开始新的下载");
                        
                         *  2.取得初始化列表对象
                        List<Download> lists = downloader.prepareDownload();

                         *  3.将基本数据存入一个properties文件中
                        saveDownloadInfo(lists, downloader.fileSize);

                        System.out.println("文件总大小:" + downloader.fileSize);

                         *  4.给进度条设置最大值
                        this.bar.setMaximum(downloader.fileSize);

                         *  5.按照线程数量动态添加进度条
                        List<JProgressBar> 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<Download> 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<JProgressBar> addBar(List<Download> lists) {
        List<JProgressBar> bars = new ArrayList<JProgressBar>();
        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<Download> 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<Download> prepareDownload() {
        try {
             * 每个线程下载的基本数量,最后一个线程下载的量要另外计算
            int block = fileSize/count;
            
            lists = new ArrayList<Download>();
            for(int i=0; i<count; i++) {
                Download download = new Download();
                if(i != count-1) {  * 不是最后一个线程,下载量就是block
                    download.setSize(block);
                }else {  * 最后一个线程,下载量是总量减去之前线程的下载量
                    download.setSize(fileSize-(count-1)*block);
                }
                download.setUrl(url);
                download.setSavePath(savePath);
                download.setIndex(i);        * 设置线程编号
                download.setStart(i*block);  * 设置线程的开始下载位置
                
                lists.add(download);
            }
            return lists;
        } catch(Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
     * 断点续传的,取出上次下载的线程储存的数据文件,读取到Download中,构成list返回
    public List<Download> keepDownload(){
        lists = new ArrayList<Download>();
        
        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<count; i++) {
            Download d = new Download();
            d.setUrl(url);
            d.setSavePath(savePath);
            d.setStart(Integer.parseInt(downui.p.getProperty("Thread." + i + ".start")));
            d.setIndex(i);
            d.setSize(Integer.parseInt(downui.p.getProperty("Thread." + i + ".size")));
            lists.add(d);
        }
        return lists;
    }

     * 启动所有线程开始下载
    public void startDownload(List<JProgressBar> 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<count; i++) {
            int size = Integer.parseInt(downui.p.getProperty("Thread." + i + ".size"));
            if(size > 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 + "]";
    }
}

暂停下载



关闭程序之后再店家开始下载,可以实现断点续传



下载完成
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容