JAVA使用HttpURLConnection进行多线程文件下载

简介: JAVA是HttpURLConnection进行多线程文件下载

遇到了一个下载文件的问题

在开发中,需要实现一个文件下载的方法,对下载时间有一点要求,对于小文件来说,问题不大,单线程下载既可;

  • 单线程下载文件

首先使用HttpURLConnection获取文件流;
创建RandomAccessFile文件对象,用于写入;
使用 randomAccessFile.write(buffer,0,size);将流转换字节写入文件
另外,可以启动一个单独的线程,记录下载进度;
在HttpURLConnection请求后记录总大小,
在写入文件时记录已下载大小;
使用NumberFormat记录输出百分比;

public boolean downloadFile(String url,String dest_path,int downloadTimeout,boolean downloadResume){

        //检测下载进度
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    NumberFormat numberFormat = NumberFormat.getPercentInstance();
                    numberFormat.setMinimumFractionDigits(2);
                    logger.debug("是否下载完成:{}",downloadFinish);
                    while (!downloadFinish){
                        Thread.sleep(10000);
                        if(totalSize > 0){
                            logger.info("已下载大小{},进度:{},如果长时间不更新,可能是进度汇报现场卡了,不影响下载",
                                    getDownloadSize(),
                                numberFormat.format(getDownloadSize()*1.0/getTotalSize()));
                        }
                    }
                }catch (Throwable e){
                    logger.error("记录下载文件进度出错,{}",e.getMessage(),e);
                    e.printStackTrace();
                }
            }
        }).start();

        logger.debug("开始下载,源文件地址:{},\n 目标地址:{},\n 设置的超时时间为:{}",url,dest_path,downloadTimeout);
        try {
            RandomAccessFile randomAccessFile = null;
            InputStream in = null;
            URL fileUrl = new URL(url);
            HttpURLConnection connection = (HttpURLConnection)fileUrl.openConnection();
            connection.setConnectTimeout(downloadTimeout);
            connection.setRequestMethod("GET");
            File tempFile = new File(dest_path+"_tmp");// 临时文件
            if(!tempFile.getParentFile().exists()){
                tempFile.getParentFile().mkdirs();
            }
            if(downloadResume){
                if(tempFile.exists() && tempFile.isFile()){
                    downloadSize = tempFile.length();
                    startIndex = downloadSize;
                }
                connection.setRequestProperty("Range", "bytes=" + startIndex + "-");
            }else{
                if(tempFile.exists() && tempFile.isFile()){
                    tempFile.delete();
                }
            }
            int staus = connection.getResponseCode();
            totalSize = downloadSize + connection.getContentLengthLong();
            logger.debug("请求状态码:{},文件总大小:{}[{}],本地需要下载的大小:{}",staus,totalSize, totalSize/1024/1024, totalSize - downloadSize);
            if(staus == 200 || staus == 206){
                randomAccessFile = new RandomAccessFile(tempFile,"rwd");
                randomAccessFile.seek(startIndex);
                in = connection.getInputStream();
                byte[] buffer = new byte[2048 * 10];
                int size = 0;
                while ((size = in.read(buffer))!= -1){
                    randomAccessFile.write(buffer,0,size);
                    downloadSize = downloadSize + size;
                }
                randomAccessFile.close();
            }

            in.close();
            File dest = new File(dest_path);
            this.setDownloadFinish(true);
            return  tempFile.renameTo(dest);
        }catch (Throwable e){
            logger.error("下载文件出错,{}",e.getMessage(),e);
        }finally {
            this.setDownloadFinish(true);
        }
        return false;
    }

但对于大文件(超5G)来说,下载时间就会很长,当然这个也需要考虑网络和硬件的关系;但是可以通过多线程的方式下载文件;

  • 多线程下载文件:

    多线程分片下载文件, 获取文件总大小, 分成指定的份数,再启动指定的线程去下载自己的那一份;

    //获取总大小:
    private Long getRemoteFileSize(String remoteFileUrl) throws IOException {
          Long fileSize = 0L ;
          HttpURLConnection httpConnection = (HttpURLConnection) new URL(remoteFileUrl).openConnection();
          httpConnection.setRequestMethod("HEAD");
          Integer responseCode = 0;
          try {
              responseCode = httpConnection.getResponseCode();
          } catch (IOException e) {
              e.printStackTrace();
          }
          if (responseCode >= 400) {
              System.out.println("Web服务器响应错误!请稍后重试");
              return fileSize;
          }
          String sHeader;
          for (Integer i = 1; ; i++) {
              sHeader = httpConnection.getHeaderFieldKey(i);
              if ("Content-Length".equals(sHeader)) {
                  fileSize = Long.parseLong(httpConnection.getHeaderField(sHeader));
                  break;
              }
          }
          return fileSize;
      }

分片:

 CountDownLatch countDownLatch = new CountDownLatch(poolLength);
List<FileDownloadRunnable> downloadRunnables = new ArrayList<>();

        Long partSize = length / poolLength;
        for (int i = 1; i <= poolLength; i++) {
            //start = i * length / poolLength;
            //long end = (i + 1) * length / poolLength - 1;
            // 每一个线程下载的开始位置
            Long start = (i - 1) * partSize;
            // 每一个线程下载的开始位置
            Long end = start + partSize - 1;
            if (i == poolLength) {
                //最后一个线程下载的长度稍微长一点
                end = length;
            }
            //System.out.println(start+"---------------"+end);
            FileDownloadRunnable fileDownloadRunnable =
                    new FileDownloadRunnable(url, dest_path, start, end,countDownLatch);
            downloadRunnables.add(fileDownloadRunnable);
            Thread thread = new Thread(fileDownloadRunnable);
            thread.setName("下载线程"+i);
            thread.start();
        }
         countDownLatch.await(); //等待主线程完成
        for (FileDownloadRunnable downloadRunnable:downloadRunnables) {
            boolean downloadFinish = downloadRunnable.isDownloadFinish();
            logger.debug("多个线程是否都下载完成:"+downloadRunnable.isDownloadFinish());
            if(!downloadFinish){
                throw  new RuntimeException("链接文件失败,无法落地");
            }
        }
        //FileDownloadRunnable.mergeFiles(dest_path,poolLength,false);
        long endTime = System.currentTimeMillis();
        logger.debug("文件下载完成,总耗时:{}s",(endTime - startTime) / 1000);
        return true;

下载:

public class FileDownloadRunnable implements Runnable{

    //下载路径
    private String urlLocation;
    //下载完存放路径
    private String filePath;
    //文件开始地址
    public Long start;
    //文件结束地址
    public Long end;

    private CountDownLatch countDownLatch;

    ThreadLocal<Long> startTime = new ThreadLocal<>();

    NumberFormat numberFormat = NumberFormat.getPercentInstance();
    Long allSize = 1L;

    private boolean downloadFinish = false;


    public FileDownloadRunnable(String urlLocation, String filePath, Long start, Long end, CountDownLatch countDownLatch) {
        this.urlLocation = urlLocation;
        this.filePath = filePath;
        this.start = start;
        this.end = end;
        this.countDownLatch = countDownLatch;
        numberFormat.setMinimumFractionDigits(2);
        allSize = end-start;
    }

    @Override
    public void run() {
        InputStream is = null;
        //FileOutputStream out = null;
        RandomAccessFile out = null;
        try {
            File file = new File(filePath);
            if(!file.getParentFile().exists()){
                file.getParentFile().mkdirs();
            }
            startTime.set(System.currentTimeMillis());
            System.out.println(start+"-"+end);
            //获取下载的部分
            URL url = new URL(urlLocation);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setReadTimeout(6000);
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
            conn.setRequestProperty("Connection", "Keep-Alive");
            //conn.setRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1");
            //conn.connect();
            int statusCode = conn.getResponseCode();
            long length = conn.getContentLengthLong();
            System.out.println(Thread.currentThread().getName() +":请求状态码:"+statusCode+",请求到的大小:"+length+"="+length/1024/1024+"M");
            is = conn.getInputStream();
            //System.out.println("getInputStream().available():"+is.available());
            //BufferedInputStream bis = new BufferedInputStream(is);
            //out = new FileOutputStream(filePath);
            out = new RandomAccessFile(file, "rw");
            out.seek(start);
            //System.out.println("out.seek:"+start);
            byte[] bytes = new byte[2048 * 10];
            int l = 0;
            //int totalSize = 0;
            while ((l = is.read(bytes))!=-1){
                //totalSize += l;
                out.write(bytes,0,l);
            }
            //bis.close();
            out.close();
            is.close();
            long time = System.currentTimeMillis()  - startTime.get();
            downloadFinish = true; //下载完成标识
            System.out.println(Thread.currentThread().getName()+"完成下载!共耗时(秒):" + time/1000);
        } catch (Throwable e) {
            e.printStackTrace();
            throw new RuntimeException(Thread.currentThread().getName()+"下载失败",e);
        } finally {
            countDownLatch.countDown();
        }

    }
 public boolean isDownloadFinish() {
        return downloadFinish;
    }

未解决的问题:

下载时遇到一个问题,挂载盘和本地盘下载结果不一致;不知道是不是挂载盘方式的问题?

相关文章
|
15天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
72 17
|
26天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
11天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
28天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
28天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
28天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
55 3
|
28天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
157 2
|
1月前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
52 6
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
8月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。