【Java多线程上手项目】多线程下载器

所用和需要学习的知识点

  • http
  • ScheduledExecutorService生成打印线程,按时间隔输出信息
  • 利用ThreadPoolExecutor线程池进行多线程分片下载
  • 使用原子类保证数据在线程中安全性

利用scanner获取控制台输入

Scanner sc =new Scanner(System.in);

String url=null;
while (true){
    System.out.println("请输入下载地址:");
    Scanner sc =new Scanner(System.in);
    url = sc.next();
    break;
}

我们在控制台获取输入的下载地址。

HttpURLConnection

编写工具类,获取下载HttpURLConnection 和获取下载文件名称

 // 获取http链接
    public static HttpURLConnection getHttpURLConnection(String url) throws IOException {
        URL httpUrl = new URL(url);
        HttpURLConnection httpUrlConnection = (HttpURLConnection)httpUrl.openConnection();
        // 向文件所在服务器 伪造请求信息
        httpUrlConnection.addRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1");
        return httpUrlConnection;
    }

    // 获取文件名称
    public static String getFileName(String url){
        int index = url.lastIndexOf("/");
        String fileName = url.substring(index);
        return fileName;
    }

获取文件IO流进行下载

其中代码中利用到try(){} catch(){},实现了对输入流输出流的自动关闭
flush()可以强制将缓冲区的内容全部写入输出流

其中 bos.flush() 其实可以忽略这个刷新缓冲流。只调用 bis.close() 和 bos.close() 就可以。

进一步使用try(){} catch{} 连关闭流可以省略

IO流

  1. 利用上述的http链接获取输入流
  2. 获取保存路径
  3. 获取输出流,保存文件

实现单线程下载文件

public static void downLoad(String url){
        String fileName = HttpUtils.getFileName(url);
        String savePath = Constant.PATH + fileName;
        HttpURLConnection httpURLConnection = null;
        try {
            httpURLConnection = HttpUtils.getHttpURLConnection(url);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 利用文件IO流进行下载
        try (
                // 执行完自动关闭IO流
                InputStream inputStream = httpURLConnection.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(inputStream);
                FileOutputStream fos = new FileOutputStream(savePath);
                BufferedOutputStream bos = new BufferedOutputStream(fos);

        ){
            int len=-1;
            while ((len = bis.read())!=-1){
                bos.write(len);
            }

        } catch (FileNotFoundException e) {
            System.out.println("下载的文件不存在");
        } catch (Exception e){
            System.out.println("下载失败");
        }
        finally {
            if(httpURLConnection!=null){
                httpURLConnection.disconnect();
            }
        }
    }

可以使用QQ下载链接进行尝试

https://dldir1.qq.com/qqfile/qq/PCQQ9.7.9/QQ9.7.9.29065.exe

Logger工具类

编写logger工具类

使用了Java 可变参数,同时构建了打印主函数print

其次构建 info和error函数实现了对print函数的复用,并涉及可变参数

public class LogUtils {
    public static void info(String msg, Object... args){
        print(msg,"-info-",args);
    }
    public static void error(String msg, Object... args){
        print(msg,"-error-",args);
    }
    
    public static void print(String msg, String level, Object... args){
        // 如果包含额外信息
        if (args!=null && args.length>0){
            msg = msg.replace("{}",(String)args[0]);
        }
        System.out.println(LocalTime.now().format(DateTimeFormatter.ofPattern("hh:mm:ss"))+level+msg);
    }
}

获取打印信息

文件下载的时候最好能够展示出下载的速度,已下载文件大小等信息。这里可以每隔一段时间来获取文件的下载信息,比如间隔1秒获取一次,然后将信息打印到控制台。文件下载是一个独立的线程,另外还需要再开启一个线程来间隔获取文件的信息。java.util.concurrent. ScheduledExecutorService,这个类可以帮助我们来实现此功能。

ScheduledExecutorService

在该类中提供了一些方法可以帮助开发者实现间隔执行的效果,下面列出一些常见的方法及其参数说明。我们可以通过下面方式来获取该类的对象,其中1标识核心线程的数量

ScheduledExecutorService s = Executors.newScheduledThreadPool(1);

schedule方法

该方法是重载的,这两个重载的方法都是有3个形参,只是第一个形参不同。

  • Runnable / Callable 可以传入这两个类型的任务
  • long delay 时间数量
  • TimeUnit unit 时间单位

该方法的作用是让任务按照指定的时间延时执行

public static void main(String[] args) {
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
        ses.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis());
            }
        },2, TimeUnit.SECONDS);
        ses.shutdown();
    }

scheduleAtFixedRate方法

该方法的作用是按照指定的时间延时执行,并且每隔一段时间再继续执行

  • Runnable command 执行的任务
  • long initialDelay 延时的时间数量
  • long period 间隔的时间数量

l TimeUnit unit 时间单位

倘若在执行任务的时候,耗时超过了间隔时间,则任务执行结束之后直接再次执行,而不是再等待间隔时间执行。 意思就是执行任务的时间 与 间隔的时间相比,这里间隔的时间包含执行任务的时间

scheduleWithFixedDelay方法

该方法的作用是按照指定的时间延时执行,并且每隔一段时间再继续执行

  • Runnable command 执行的任务
  • long initialDelay 延时的时间数量
  • long period 间隔的时间数量
  • TimeUnit unit 时间单位

在执行任务的时候,无论耗时多久,任务执行结束之后都会等待间隔时间之后再继续下次任务。

构建下载信息类

首先构建下载信息类实现Runnable接口,然后我们利用scheduleWithFixedDelay新建该进程方法,每间隔一秒执行run()方法更新并显示下载信息。

主要是存储文件下载信息,同时由于我们使用字节流进行下载,我们如果使用常见的MB显示,记得进行单位转换Constant.MB=1024d * 1024d

package com.yqyang.down;

import com.yqyang.constant.Constant;

public class DownLoadInfo implements Runnable{
    // 文件总大小
    double fileSize;

    // 剩余文件大小
    double lastFileSize;

    // 当前下载的总大小 多个线程 使用volatile强制从主内存读取
    volatile double downSize;



    // 前1s下载的总大小
    double predownSize;

    public DownLoadInfo(double fileSize) {
        this.fileSize = fileSize;
        lastFileSize = fileSize;
    }

    @Override
    public void run() {
        // 下载使用的是字节流 单位换算 同时为了方便打印转化为字符串
        String fileSizeMB = String.format("%.2f",fileSize / Constant.MB);

        //  1s 内下载文件大小
        double secondDownSize= downSize -predownSize;

        predownSize = downSize;
        // 1s 下载速度 kb/s 同时为了方便打印转化为字符串
        String downSpeed = String.format("%.2f",secondDownSize / 1024d);;

        // 剩余下载大小 同时为了方便打印转化为字符串
        lastFileSize = (fileSize - downSize);
        String lastFileSizeMB = String.format("%.2f",lastFileSize/Constant.MB);

        // 剩余时间
        String lastTime = String.format("%.0f",lastFileSize/secondDownSize);
        String downInfo = String.format("剩余文件大小%sMB 文件大小 %sMB 下载速度 %sKB/s 剩余时间%ss",lastFileSizeMB,fileSizeMB,downSpeed,lastTime);
        // 打印下载信息
        System.out.print("\r");
        System.out.print(downInfo);

    }
}


  • 实现Runnable接口
  • 对于会在多个线程使用的变量,使用volatile声明,使其强制从内存读取
  • 使用 System.out.print("\r"); 不会换行,刷新当前显示字符串

更新DownLoad

我们需要更新DownLoad代码

首先我们可以先获取,要保存的地方是否存在文件,并且与要下载的文件大小是否相同,

编写FileUtils

package com.yqyang.utils;

import java.io.File;

public class FileUtils {
    public static double getLocalFileSize(String path){
        File file = new File(path);
        // 首先看文件是否存在 其次看是不是文件, 是的话返回文件长度
        return file.exists() && file.isFile() ? file.length() : 0;
    }
}

添加线程,打印下载信息

 public static void downLoad(String url){
        String fileName = HttpUtils.getFileName(url);
        String savePath = Constant.PATH + '/' +fileName;
        HttpURLConnection httpURLConnection = null;
        DownLoadInfo downLoadInfo;
        // 获取本地文件大小
        double localFileSize = FileUtils.getLocalFileSize(savePath);
        try {
            httpURLConnection = HttpUtils.getHttpURLConnection(url);
            
            // 
            long contentLength = httpURLConnection.getContentLengthLong();
            String temp= String.valueOf(contentLength);

            // 本地是否已经曾经下载完成
            if (localFileSize*Math.pow(10, temp.length())>=contentLength) {
                LogUtils.info("文件已下载");
                return;
            }
            // 构建下载信息类
            downLoadInfo=new DownLoadInfo(contentLength);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


        // 新建打印信息线程 记得关闭
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
		// 设置任务和执行时间
        scheduledExecutorService.scheduleWithFixedDelay(downLoadInfo, 1, 1, TimeUnit.SECONDS);
        try (
                // 执行完自动关闭IO流
                InputStream inputStream = httpURLConnection.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(inputStream);
                FileOutputStream fos = new FileOutputStream(savePath);
                BufferedOutputStream bos = new BufferedOutputStream(fos);

        ){
            byte[] buff = new byte[1024*100];
            int len=-1;
            while ((len = bis.read(buff))!=-1){
                bos.write(buff,0,len);
                // ====     添加关键下载信息     ====
                downLoadInfo.downSize+=len;
            }

        } catch (FileNotFoundException e) {
            LogUtils.error("未找到{}文件", fileName);
        } catch (Exception e){
            LogUtils.error("下载失败");
        }
        finally {
            if(httpURLConnection!=null){
                httpURLConnection.disconnect();
            }
            // 关闭
            scheduledExecutorService.shutdownNow();
        }
    }/

线程池-ThreadPoolExecutor

线程在创建,销毁的过程中会消耗一些资源,为了节省这些开销,jdk添加了线程池。线程池节省了开销,提高了线程使用的效率。阿里巴巴开发文档中建议在编写多线程程序的时候使用线程池

ThreadPoolExecutor 构造方法

  • corePoolSize
    • 线程池中核心线程的数量
  • maximumPoolSize
    • 线程池中最大线程的数量,是核心线程数量和非核心线程数量之和
  • keepAliveTime
    • 非核心线程空闲的生存时间
  • unit
    • keepAliveTime的生存时间单位
  • workQueue
    • 当没有空闲的线程时,新的任务会加入到workQueue中排队等待
  • threadFactory
    • 线程工厂,用于创建线程
  • handler
    • 拒绝策略,当任务太多无法处理时的拒绝策略

线程池工作状态

在这里插入图片描述

对于怎么安排非核心线程,和阻塞队列有关

比如使用ArrayBlockingQueue, 核心线程满之后,会先进阻塞队列,阻塞队列满之后会调用非核心线程

线程池的状态

线程池中有5个状态,分别是:

· RUNNING

创建线程池之后的状态是RUNNING

SHUTDOWN

该状态下,线程池就不会接收新任务,但会处理阻塞队列剩余任务,相对温和。

STOP

该状态下会中断正在执行的任务,并抛弃阻塞队列任务,相对暴力。

· TIDYING

任务全部执行完毕,活动线程为 0 即将进入终止

· TERMINATED

线程池终止

线程池的关闭

线程池使用完毕之后需要进行关闭,提供了以下两种方法进行关闭

  • shutdown()

该方法执行后,线程池状态变为 SHUTDOWN,不会接收新任务,但是会执行完已提交的任务,此方法不会阻塞调用线程的执行。

  • shutdownNow()

该方法执行后,线程池状态变为 STOP,不会接收新任务,会将队列中的任务返回,并用 interrupt 的方式中断正在执行的任务。

工作队列

jdk中提供的一些工作队列workQueue

  • SynchronousQueue

直接提交队列

·* ArrayBlockingQueue

有界队列,可以指定容量

· LinkedBlockingDeque

无界队列

· PriorityBlockingQueue

优先任务队列,可以根据任务优先级顺序执行任务

    public static void main(String[] args) {
        // 新建线程池
        ThreadPoolExecutor executor=new ThreadPoolExecutor(3, 4,10,  TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
        // 设置任务
        Runnable r=()->{
            System.out.println(Thread.currentThread().getName());
        };
        int i=5;
        // 打印线程池信息
        System.out.println(executor);
        for (int i1 = 0; i1 < i; i1++) {
            // 线程执行任务
            executor.execute(r);
        }
        System.out.println(executor);
    }

使用线程池实现多线程分片下载

完善HttpUtils方法-分片下载

重写getHttpURLConnection方法, 实现分片下载

下载url 中startPos - endPos 字节的数据,如果endPos不写表示从startPos下载到最后。

// https://dldir1.qq.com/qqfile/qq/PCQQ9.7.9/QQ9.7.9.29065.exe

    // 获取http链接
    public static HttpURLConnection getHttpURLConnection(String url) throws IOException {
        URL httpUrl = new URL(url);
        HttpURLConnection httpUrlConnection = (HttpURLConnection)httpUrl.openConnection();
        // 向文件所在服务器 伪造请求信息
        httpUrlConnection.addRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1");
        return httpUrlConnection;
    }

/*     获取分片下载链接 重写方法
    *
    * */
    public static HttpURLConnection getHttpURLConnection(String url,long startPos,long endPos) throws IOException {
        // 获取链接
        HttpURLConnection httpURLConnection = getHttpURLConnection(url);
        if (endPos==0)
            // 最后一个分片
            httpURLConnection.setRequestProperty("RANGE","bytes="+startPos+"-");
        else
            httpURLConnection.setRequestProperty("RANGE","bytes="+startPos+"-"+endPos);
        return httpURLConnection;
    }

完善分片下载任务

因为我们要实现分片下载,所以我们需要用到多线程下载,要构建线程任务。

这里我们实现Callable接口,因为Callable接口可以有返回值,但是Runnable接口没有返回值
countDownLatch,是一个线程安全的累减器,保证每个线程下载完分片后在继续执行。

public class DownLoadTask implements Callable<Boolean> {
    public DownLoadTask(String url, long startPos, long endPos, int part, CountDownLatch countDownLatch) {
        this.url = url;
        this.startPos = startPos;
        this.endPos = endPos;
        this.part = part;
        this.countDownLatch=countDownLatch;
    }
    private CountDownLatch countDownLatch;
    private String url;
    // 下载起始位置
    private long startPos;
    // 下载结束位置
    private long endPos;
    // 分片索引
    private int part;
    @Override
    public Boolean call() throws IOException {
        // 获取文件名字
        String fileName = HttpUtils.getFileName(url);
        // 分块的文件名
        String saveName = Constant.PATH +"/"+fileName+".temp"+part;
        // 获取分块下载的链接
        HttpURLConnection httpURLConnection=HttpUtils.getHttpURLConnection(url, startPos, endPos);

        System.out.println(Thread.currentThread()+String.valueOf(startPos)+"-"+ String.valueOf(endPos));
        try(    // 实现自动关闭
                InputStream is = httpURLConnection.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(is);
                RandomAccessFile rw = new RandomAccessFile(saveName, "rw");
        ){
            int len=-1;
            byte[] buff = new byte[1024 * 100];
            while ((len=bis.read(buff))!=-1){
                rw.write(buff,0, len);
                DownLoadInfo.downSize.add(len);
            }

        }catch (FileNotFoundException e){
            LogUtils.error("下载文件不存在");
            return false;
        }catch (Exception e){
            LogUtils.error("出错了");
            return false;
        }finally {
            countDownLatch.countDown();
            // 记得关闭啊
            if (httpURLConnection!=null)
                httpURLConnection.disconnect();
        }



        return true;
    }

实现分片下载

其次创建线程池对象

public  ThreadPoolExecutor executor=new ThreadPoolExecutor(Constant.THREAD_NUM, Constant.THREAD_NUM,1,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));

很明显我们这里面没有非核心线程。

对文件进行分片,并调用任务

        // 文件切分
    public  void split(String url, long contentLength){
        int size = (int) (contentLength / Constant.THREAD_NUM);
        for (int i = 0; i < Constant.THREAD_NUM; i++) {
            long startPos = i * size;
            long endPos;
            if (i==Constant.THREAD_NUM-1) endPos=0;
            else endPos = startPos + size;
            if (i!=0) startPos+=1;
            DownLoadTask downLoadTask = new DownLoadTask(url, startPos, endPos, i, countDownLatch);
            executor.submit(downLoadTask);
        }
    }

利用原子类 LongAdder

此时打印信息是有错误的,这里必须保证变量downSize的安全


public class DownLoadInfo implements Runnable{
    // 文件总大小
    double fileSize;




    // 当前下载的总大小 多个线程  使用volatile强制从主内存读取
    static volatile LongAdder downSize=new LongAdder();



    // 前1s下载的总大小
    double predownSize;

    public DownLoadInfo(double fileSize) {
        this.fileSize = fileSize;

    }

    @Override
    public void run() {
        // 下载使用的是字节流 单位换算 同时为了方便打印转化为字符串
        String fileSizeMB = String.format("%.2f",fileSize / Constant.MB);

        //  1s 内下载文件大小
        double secondDownSize= downSize.doubleValue() -predownSize;

        predownSize = downSize.doubleValue();
        // 1s 下载速度 kb/s 同时为了方便打印转化为字符串
        String downSpeed = String.format("%.2f",secondDownSize / 1024d);;

        // 剩余下载大小 同时为了方便打印转化为字符串
        double lastFileSize = (fileSize - downSize.doubleValue());
        String lastFileSizeMB = String.format("%.2f",lastFileSize/Constant.MB);

        // 剩余时间
        String lastTime = String.format("%.0f",lastFileSize/secondDownSize);
        String downInfo = String.format("剩余文件大小%sMB 文件大小 %sMB 下载速度 %sKB/s 剩余时间%ss",lastFileSizeMB,fileSizeMB,downSpeed,lastTime);
        System.out.print("\r");
        System.out.print(downInfo);

    }
}


合并分片文件并删除缓存

    public Boolean mergeFile(String fileName){
        LogUtils.info("正在合并文件");
        try (RandomAccessFile accessFile = new RandomAccessFile(fileName,"rw")) {
            for (int i = 0; i < Constant.THREAD_NUM; i++) {
                int len=-1;
                try ( BufferedInputStream bis=new BufferedInputStream(new FileInputStream(fileName+".temp"+i));){
                    byte[] buff = new byte[1024 * 100];
                    while ((len=bis.read(buff))!=-1){
                        accessFile.write(buff);
                    }
                }

            }

        } catch (FileNotFoundException e) {
            return false;

        } catch (IOException e) {
            return false;
        }
        LogUtils.info("合并文件完成");
        return true;

    }

    public Boolean deleteTempFile(String fileName){
        for (int i = 0; i < Constant.THREAD_NUM; i++) {
            File file=new File(fileName+".temp"+i);
            file.delete();
        }
        return true;
    }

DownLoad

 public  void downLoad(String url){
        String fileName = HttpUtils.getFileName(url);
        String savePath = Constant.PATH + '/' +fileName;
        HttpURLConnection httpURLConnection = null;
        DownLoadInfo downLoadInfo;
        // 获取本地文件大小
        double localFileSize = FileUtils.getLocalFileSize(savePath);
        long contentLength;
        try {
            httpURLConnection = HttpUtils.getHttpURLConnection(url);
            contentLength = httpURLConnection.getContentLengthLong();
            String temp= String.valueOf(contentLength);

            // 查看文件是否已经下载完成
            if (localFileSize*Math.pow(10, temp.length())>=contentLength) {
                LogUtils.info("文件已下载");
                return;
            }
            // 构建下载信息类
            downLoadInfo=new DownLoadInfo(contentLength);
            // 设置信息线程池每隔一秒执行一次
            scheduledExecutorService.scheduleWithFixedDelay(downLoadInfo, 1, 1, TimeUnit.SECONDS);


            split(url, contentLength);

            // 等待所有线程下载完毕
            countDownLatch.await();

            System.out.print("/r");
            System.out.println("所有文件下载完成");

            mergeFile(savePath);
            deleteTempFile(savePath);



        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if(scheduledExecutorService!=null) scheduledExecutorService.shutdown();
            if(executor!=null) executor.shutdown();
        }

    }

完整代码

package com.yqyang;

import com.yqyang.down.DownLoad;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        String url=null;
        while (true){
            System.out.println("请输入下载地址:");
            Scanner sc =new Scanner(System.in);
            url = sc.next();
            break;
        }

        DownLoad downLoad = new DownLoad();
        downLoad.downLoad(url);


    }

}

com.yyq.constant Constant.java

package com.yqyang.constant;

import java.util.Stack;

public class Constant {
    public static final String PATH="D:\\WorkSpace\\JavaWorkSpace\\downLoader\\down";
    public static final double MB = (1024d * 1024d);
    public static final int THREAD_NUM = 5;
}

com.yyq.down DownLoad.java

package com.yqyang.down;

import com.yqyang.constant.Constant;
import com.yqyang.utils.FileUtils;
import com.yqyang.utils.HttpUtils;
import com.yqyang.utils.LogUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.concurrent.*;

public class DownLoad {
    // 新建打印信息线程池 记得关闭
    public CountDownLatch countDownLatch=new CountDownLatch(Constant.THREAD_NUM);
    public  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
    // 新建线程池
    public  ThreadPoolExecutor executor=new ThreadPoolExecutor(Constant.THREAD_NUM, Constant.THREAD_NUM,1,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));
    public  void downLoad(String url){
        String fileName = HttpUtils.getFileName(url);
        String savePath = Constant.PATH + '/' +fileName;
        HttpURLConnection httpURLConnection = null;
        DownLoadInfo downLoadInfo;
        // 获取本地文件大小
        double localFileSize = FileUtils.getLocalFileSize(savePath);
        long contentLength;
        try {
            httpURLConnection = HttpUtils.getHttpURLConnection(url);
            contentLength = httpURLConnection.getContentLengthLong();
            String temp= String.valueOf(contentLength);

            // 查看文件是否已经下载完成
            if (localFileSize*Math.pow(10, temp.length())>=contentLength) {
                LogUtils.info("文件已下载");
                return;
            }
            // 构建下载信息类
            downLoadInfo=new DownLoadInfo(contentLength);
            // 设置信息线程池每隔一秒执行一次
            scheduledExecutorService.scheduleWithFixedDelay(downLoadInfo, 1, 1, TimeUnit.SECONDS);


            split(url, contentLength);

            // 等待所有线程下载完毕
            countDownLatch.await();

            System.out.print("/r");
            System.out.println("所有文件下载完成");

            mergeFile(savePath);
            deleteTempFile(savePath);



        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if(scheduledExecutorService!=null) scheduledExecutorService.shutdown();
            if(executor!=null) executor.shutdown();
        }

    }
        // 文件切分
    public  void split(String url, long contentLength){
        int size = (int) (contentLength / Constant.THREAD_NUM);
        for (int i = 0; i < Constant.THREAD_NUM; i++) {
            long startPos = i * size;
            long endPos;
            if (i==Constant.THREAD_NUM-1) endPos=0;
            else endPos = startPos + size;
            if (i!=0) startPos+=1;
            DownLoadTask downLoadTask = new DownLoadTask(url, startPos, endPos, i, countDownLatch);
            executor.submit(downLoadTask);
        }
    }

    // 文件合并
    public Boolean mergeFile(String fileName){
        LogUtils.info("正在合并文件");
        try (RandomAccessFile accessFile = new RandomAccessFile(fileName,"rw")) {
            int len=-1;
            for (int i = 0; i < Constant.THREAD_NUM; i++) {
                try ( BufferedInputStream bis=new BufferedInputStream(new FileInputStream(fileName+".temp"+i));){
                    byte[] buff = new byte[1024 * 100];
                    while ((len=bis.read(buff))!=-1){
                        accessFile.write(buff,0, len);
                    }
                }

            }

        } catch (FileNotFoundException e) {
            return false;

        } catch (IOException e) {
            return false;
        }
        LogUtils.info("合并文件完成");
        return true;

    }

    public Boolean deleteTempFile(String fileName){
        for (int i = 0; i < Constant.THREAD_NUM; i++) {
            File file=new File(fileName+".temp"+i);
            file.delete();
        }
        return true;
    }

}

com.yyq.down DownLoadInfo.java

package com.yqyang.down;

import com.yqyang.constant.Constant;

import java.util.concurrent.atomic.LongAdder;

public class DownLoadInfo implements Runnable{
    // 文件总大小
    double fileSize;




    // 当前下载的总大小 多个线程 使用volatile强制从主内存读取
    static volatile LongAdder downSize=new LongAdder();



    // 前1s下载的总大小
    double predownSize;

    public DownLoadInfo(double fileSize) {
        this.fileSize = fileSize;

    }

    @Override
    public void run() {
        // 下载使用的是字节流 单位换算 同时为了方便打印转化为字符串
        String fileSizeMB = String.format("%.2f",fileSize / Constant.MB);

        //  1s 内下载文件大小
        double secondDownSize= downSize.doubleValue() -predownSize;

        predownSize = downSize.doubleValue();
        // 1s 下载速度 kb/s 同时为了方便打印转化为字符串
        String downSpeed = String.format("%.2f",secondDownSize / 1024d);;

        // 剩余下载大小 同时为了方便打印转化为字符串
        double lastFileSize = (fileSize - downSize.doubleValue());
        String lastFileSizeMB = String.format("%.2f",lastFileSize/Constant.MB);

        // 剩余时间
        String lastTime = String.format("%.0f",lastFileSize/secondDownSize);
        String downInfo = String.format("剩余文件大小%sMB 文件大小 %sMB 下载速度 %sKB/s 剩余时间%ss",lastFileSizeMB,fileSizeMB,downSpeed,lastTime);
        System.out.print("\r");
        System.out.print(downInfo);

    }
}

com.yyq.down DownLoadTask.java

package com.yqyang.down;

import com.yqyang.constant.Constant;
import com.yqyang.utils.HttpUtils;
import com.yqyang.utils.LogUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;

public class DownLoadTask implements Callable<Boolean> {
    public DownLoadTask(String url, long startPos, long endPos, int part, CountDownLatch countDownLatch) {
        this.url = url;
        this.startPos = startPos;
        this.endPos = endPos;
        this.part = part;
        this.countDownLatch=countDownLatch;
    }
    private CountDownLatch countDownLatch;
    private String url;
    // 下载起始位置
    private long startPos;
    // 下载结束位置
    private long endPos;
    // 分片索引
    private int part;
    @Override
    public Boolean call() throws IOException {
        // 获取文件名字
        String fileName = HttpUtils.getFileName(url);
        // 分块的文件名
        String saveName = Constant.PATH +"/"+fileName+".temp"+part;
        // 获取分块下载的链接
        HttpURLConnection httpURLConnection=HttpUtils.getHttpURLConnection(url, startPos, endPos);

        System.out.println(Thread.currentThread()+String.valueOf(startPos)+"-"+ String.valueOf(endPos));
        try(    // 实现自动关闭
                InputStream is = httpURLConnection.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(is);
                RandomAccessFile rw = new RandomAccessFile(saveName, "rw");
        ){
            int len=-1;
            byte[] buff = new byte[1024 * 100];
            while ((len=bis.read(buff))!=-1){
                rw.write(buff,0, len);
                DownLoadInfo.downSize.add(len);
            }

        }catch (FileNotFoundException e){
            LogUtils.error("下载文件不存在");
            return false;
        }catch (Exception e){
            LogUtils.error("出错了");
            return false;
        }finally {
            countDownLatch.countDown();
            // 记得关闭啊
            if (httpURLConnection!=null)
                httpURLConnection.disconnect();
        }



        return true;
    }
}

工具类
com.yqyang.utils FileUtils .java

package com.yqyang.utils;

import java.io.File;

public class FileUtils {
    public static double getLocalFileSize(String path){
        File file = new File(path);
        return file.exists() && file.isFile() ? file.length() : 0;
    }
}

com.yqyang.utils HttpUtils .java

package com.yqyang.utils;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class HttpUtils {
    // https://dldir1.qq.com/qqfile/qq/PCQQ9.7.9/QQ9.7.9.29065.exe

    // 获取http链接
    public static HttpURLConnection getHttpURLConnection(String url) throws IOException {
        URL httpUrl = new URL(url);
        HttpURLConnection httpUrlConnection = (HttpURLConnection)httpUrl.openConnection();
        // 向文件所在服务器 伪造请求信息
        httpUrlConnection.addRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1");
        return httpUrlConnection;
    }


    /*
    *  获取文件名称
    *
    * */
    public static String getFileName(String url){
        int index = url.lastIndexOf("/");
        String fileName = url.substring(index+1);
        return fileName;
    }

    /*     获取分片下载链接 重写方法
    *
    * */
    public static HttpURLConnection getHttpURLConnection(String url,long startPos,long endPos) throws IOException {
        // 获取链接
        HttpURLConnection httpURLConnection = getHttpURLConnection(url);
        if (endPos==0)
            // 最后一个分片
            httpURLConnection.setRequestProperty("RANGE","bytes="+startPos+"-");
        else
            httpURLConnection.setRequestProperty("RANGE","bytes="+startPos+"-"+endPos);
        return httpURLConnection;
    }

}

com.yqyang.utils LogUtils .java

package com.yqyang.utils;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class LogUtils {
    public static void info(String msg, Object... args){
        print(msg,"-info-",args);
    }
    public static void error(String msg, Object... args){
        print(msg,"-error-",args);
    }

    public static void print(String msg, String level, Object... args){
        // 如果包含额外信息
        if (args!=null && args.length>0){
            msg = msg.replace("{}",(String)args[0]);
        }
        System.out.println(LocalTime.now().format(DateTimeFormatter.ofPattern("hh:mm:ss"))+level+msg);
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值