java实现大文件并发高效下载

一、概述

这是针对多个大文件并发下载的Java程序。它利用了OkHttp库来进行网络请求,并使用线程池来同时下载多个文件,从而提高下载效率。程序通过遍历预设的文件URL和本地保存路径,创建下载任务并提交给线程池执行。每个下载任务负责下载文件的一部分,通过设置HTTP请求头的Range实现断点续传。下载完成后,程序会对下载文件进行完整性校验,以确保文件没有损坏。这个程序具备高度的扩展性,可以轻松添加更多的文件URL和本地保存路径。

二、具体代码实现

这个是笔者在写下载某音的视频时遇到的一个问题。旨在高效地从网络上下载多个大文件。笔者通过多线程分片断点续传的方式进行下载的。
首先导入必要的库:导入了所需的库文件,包括OkHttp用于网络请求和ProgressBar用于显示下载进度。
官方 miniodemo需要的依赖

  <!-- 官方 miniodemo需要的依赖-->
  <dependency>
        <groupId>me.tongfei</groupId>
      <artifactId>progressbar</artifactId>
        <version>0.7.4</version>
    </dependency>

官方 okhttp需要的依赖

<!-- 官方 okhttp需要的依赖-->
  <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>4.10.0</version>
  </dependency>

具体的Java代码讲解如下:

定义常量: 定义了一些常量,如并发线程数、最大重试次数等。

 private static final int NUM_THREADS = 1000; // 增加并发数

    private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
    private static final OkHttpClient httpClient = new OkHttpClient.Builder()
            .connectionPool(new ConnectionPool(NUM_THREADS, 500, TimeUnit.MINUTES)) // 添加连接池
            .build();

创建OkHttpClient:通过OkHttp库创建了一个自定义的OkHttpClient实例,其中包括连接池的设置,以提高连接效率。

 // 创建HTTP请求
  Request request = new Request.Builder()
          .url(fileUrl)
          .build();

  // 发送HTTP请求并获取响应(同步请求)
  Call call = httpClient.newCall(request);
  Response response = call.execute();

预设文件URL和本地保存路径: 定义了要下载的文件的URL列表和相应的本地保存路径列表,使用者可以根据需要自行添加更多的文件。

 private static final String[] fileUrls = {
            "http://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000c8rkah3c77ua6v8oqskg&line=0&file_id=477344e441dc467f8f2b72f081e241b6&sign=2b2e377cec728f425d9dc0c2a2357a25&is_play_url=1&source=PackSourceEnum_SEARCH&aid=6383",
            "https://v26-web.douyinvod.com/cf617f2ae9e9df284c491b6cfdb0a12b/64c71c93/video/tos/cn/tos-cn-ve-15/ff44e778a37b4113a841bc1b28065af4/?a=6383&amp;ch=11&amp;cr=3&amp;dr=0&amp;lr=all&amp;cd=0%7C0%7C0%7C3&amp;cv=1&amp;br=1791&amp;bt=1791&amp;cs=0&amp;ds=3&amp;ft=bvTKJbQQqUumf7oZPo0OW_EklpPiXziScMVJEawkbfCPD-I&amp;mime_type=video_mp4&amp;qs=0&amp;rc=NmZkN2U4Ozo8NGVnNmdnaUBpM3B5O2Q6ZmZvNzMzNGkzM0AtNjRiMjVeNjUxYzNiL2EvYSNmYy02cjQwZGdgLS1kLTBzcw%3D%3D&amp;l=2023073108590933C4237837B400D5DD56&amp;btag=e00038000&amp;dy_q=1690765150",
            " http://www.douyin.com/aweme/v1/play/?video_id=v0300fg10000c5lgaabc77ufcp8pbsrg&line=0&file_id=4c0b3faff21c4d3eadcac66e2708a4cb&sign=06baaa612983765b083487d7f470b96c&is_play_url=1&source=PackSourceEnum_SEARCH&aid=6383"
            // 添加更多要下载的文件URL
    };

    private static final String[] destinationPaths = {
            "D://" + UUID.randomUUID() + ".mp4",
            "D://" + UUID.randomUUID() + ".mp4",
            "D://" + UUID.randomUUID() + ".mp4"
            // 添加更多文件的本地保存路径
    };

主函数: 在主函数中,创建了一个线程池,并遍历所有文件URL。为每个文件创建一个下载任务并提交给线程池执行。

 public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

        // 遍历所有文件URL,为每个文件创建一个下载任务,并提交给线程池执行
        for (int i = 0; i < fileUrls.length; i++) {
            String fileUrl = fileUrls[i];
            String destinationPath = destinationPaths[i];

            executor.execute(() -> {
                try {
                    downloadFile(fileUrl, destinationPath);
                    System.out.println("文件下载成功:" + destinationPath);
                } catch (IOException e) {
                    System.out.println("文件下载失败:" + destinationPath + ",原因:" + e.getMessage());
                }
            });
        }

        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有文件下载完成!");
    }

下载文件方法(downloadFile): 此方法接收文件的URL和本地路径,发起HTTP请求以获取文件数据。如果响应成功,它会根据文件大小和已下载的部分,计算出需要下载的剩余部分,并将下载任务分配给线程池中的多个线程。


    // 下载文件的方法
    public static void downloadFile(String fileUrl, String destinationPath) throws IOException {
        // 创建HTTP请求
        Request request = new Request.Builder()
                .url(fileUrl)
                .build();

        // 发送HTTP请求并获取响应(同步请求)
        Call call = httpClient.newCall(request);
        Response response = call.execute();

        // 如果响应不成功,抛出异常
        if (!response.isSuccessful()) {
            throw new IOException("服务器返回错误:" + response.code());
        }

        // 获取文件的大小
        long fileSize = response.body().contentLength();

        // 检查目标文件是否存在,如果已经下载过一部分,则继续下载
        File destinationFile = new File(destinationPath);
        long downloadedFileSize = destinationFile.exists() ? destinationFile.length() : 0;

        // 计算剩余的字节数
        long remainingBytes = fileSize - downloadedFileSize;
        // 计算每个线程应下载的字节数
        long chunkSize = remainingBytes / NUM_THREADS;

        // 创建一个新的线程池,用于下载单个文件的多个部分
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

        for (int i = 0; i < NUM_THREADS; i++) {
            long startRange = fileSize - remainingBytes;
            long endRange = startRange + chunkSize - 1;
            if (i == NUM_THREADS - 1) {
                endRange = fileSize - 1;
            }

            // 创建下载任务并提交给线程池执行
            executor.execute(new DownloadTask(fileUrl, destinationPath, startRange, endRange, fileSize));

            remainingBytes -= chunkSize;
        }

        // 关闭线程池并等待所有下载任务完成
        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 校验文件完整性
        try {
            if (checkFileIntegrity(destinationPath)) {
                System.out.println("文件完整性校验通过:" + destinationPath);
            } else {
                System.out.println("文件完整性校验失败:" + destinationPath);
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

下载任务类(DownloadTask): 这是一个实现了Runnable接口的内部类,代表单个文件的下载任务。每个下载任务负责下载文件的一部分,它通过设置HTTP请求头中的Range字段实现断点续传。下载任务执行时,它会在多次尝试后,将文件数据写入本地文件。

   // 下载任务类
     static class DownloadTask implements Runnable {
        private final String fileUrl;
        private final String destinationPath;
        private final long startRange;
        private final long endRange;
        private final long fileSize;

        public DownloadTask(String fileUrl, String destinationPath, long startRange, long endRange, long fileSize) {
            this.fileUrl = fileUrl;
            this.destinationPath = destinationPath;
            this.startRange = startRange;
            this.endRange = endRange;
            this.fileSize = fileSize;
        }

        @Override
        public void run() {
            // 初始化重试次数
            int retryCount = 0;
            while (retryCount < MAX_RETRY_COUNT) {
                try {
                    // 创建HTTP请求,并设置请求头Range来实现断点续传
                    Request request = new Request.Builder()
                            .url(fileUrl)
                            .header("Range", "bytes=" + startRange + "-" + endRange)
                            .build();

                    // 发送HTTP请求并获取响应(同步请求)
                    Call call = httpClient.newCall(request);
                    Response  response = call.execute();

                    // 如果响应不成功,抛出异常
                    if (!response.isSuccessful()) {
                        throw new IOException("服务器返回错误:" + response.code());
                    }

                    // 创建随机访问文件对象,用于将下载的数据写入文件指定的位置
                    RandomAccessFile output = new RandomAccessFile(destinationPath, "rw");
                    output.seek(startRange);
                    byte[] buffer = new byte[1024 * 1024*2];
                    int bytesRead;
                    try (ProgressBar progressBar = new ProgressBar("下载进度", endRange - startRange + 1)) {
                        // 循环读取响应的数据,并写入文件
                        while ((bytesRead = response.body().byteStream().read(buffer)) != -1) {
                            output.write(buffer, 0, bytesRead);
                            progressBar.stepBy(bytesRead);
                        }
                    }

                    // 关闭文件和响应
                    output.close();
                    response.close();

                    break;
                } catch (IOException e) {
                    // 出现异常,进行重新下载
                    e.printStackTrace();
                    System.out.println("文件下载异常,进行重新下载...");
                    retryCount++;
                }
            }

            // 如果下载重试次数达到最大值仍然失败,则打印失败信息
            if (retryCount >= MAX_RETRY_COUNT) {
                System.out.println("文件下载失败:" + destinationPath);
            }

        }
    }

校验文件完整性方法(checkFileIntegrity): 这个方法对下载完成的文件进行完整性校验,以确保文件没有损坏或篡改。它计算文件的哈希值,并将其与预期的哈希值进行比较。

  // 校验文件完整性
    public static boolean checkFileIntegrity(String filePath) throws NoSuchAlgorithmException, IOException {
       
        String expectedHash = calculateFileHash(Paths.get(filePath), "MD5");
        String actualHash = calculateFileHash(Paths.get(filePath), "MD5");

        return expectedHash.equals(actualHash);
    }

计算文件哈希值方法(calculateFileHash): 此方法利用MessageDigest来计算文件的哈希值,以便后续校验文件完整性。它通过读取文件内容的字节,并进行哈希计算,最终返回十六进制格式的哈希值字符串。

// 计算文件的哈希值
    public static String calculateFileHash(Path filePath, String algorithm) throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance(algorithm);
        try (FileInputStream fis = new FileInputStream(filePath.toFile())) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
        }
        byte[] hashBytes = md.digest();
        return bytesToHex(hashBytes);
    }

三、具体完整代码实现

此程序使用OkHttp库和多线程的方式,实现了高效的多文件并发下载功能,同时也确保下载文件的完整性。通过合理的线程管理和文件分割策略,可以最大程度地提高下载速度和效率。

package cn.konne.konneim.download;


import me.tongfei.progressbar.ProgressBar;
import okhttp3.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MultiFileDownloader {
    private static final int NUM_THREADS = 1000; // 增加并发数

    private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
    private static final OkHttpClient httpClient = new OkHttpClient.Builder()
            .connectionPool(new ConnectionPool(NUM_THREADS, 500, TimeUnit.MINUTES)) // 添加连接池
            .build();

    private static final String[] fileUrls = {
            "http://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000c8rkah3c77ua6v8oqskg&line=0&file_id=477344e441dc467f8f2b72f081e241b6&sign=2b2e377cec728f425d9dc0c2a2357a25&is_play_url=1&source=PackSourceEnum_SEARCH&aid=6383",
            "https://v26-web.douyinvod.com/cf617f2ae9e9df284c491b6cfdb0a12b/64c71c93/video/tos/cn/tos-cn-ve-15/ff44e778a37b4113a841bc1b28065af4/?a=6383&amp;ch=11&amp;cr=3&amp;dr=0&amp;lr=all&amp;cd=0%7C0%7C0%7C3&amp;cv=1&amp;br=1791&amp;bt=1791&amp;cs=0&amp;ds=3&amp;ft=bvTKJbQQqUumf7oZPo0OW_EklpPiXziScMVJEawkbfCPD-I&amp;mime_type=video_mp4&amp;qs=0&amp;rc=NmZkN2U4Ozo8NGVnNmdnaUBpM3B5O2Q6ZmZvNzMzNGkzM0AtNjRiMjVeNjUxYzNiL2EvYSNmYy02cjQwZGdgLS1kLTBzcw%3D%3D&amp;l=2023073108590933C4237837B400D5DD56&amp;btag=e00038000&amp;dy_q=1690765150",
            " http://www.douyin.com/aweme/v1/play/?video_id=v0300fg10000c5lgaabc77ufcp8pbsrg&line=0&file_id=4c0b3faff21c4d3eadcac66e2708a4cb&sign=06baaa612983765b083487d7f470b96c&is_play_url=1&source=PackSourceEnum_SEARCH&aid=6383"
            // 添加更多要下载的文件URL
    };

    private static final String[] destinationPaths = {
            "D://" + UUID.randomUUID() + ".mp4",
            "D://" + UUID.randomUUID() + ".mp4",
            "D://" + UUID.randomUUID() + ".mp4"
            // 添加更多文件的本地保存路径
    };

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

        // 遍历所有文件URL,为每个文件创建一个下载任务,并提交给线程池执行
        for (int i = 0; i < fileUrls.length; i++) {
            String fileUrl = fileUrls[i];
            String destinationPath = destinationPaths[i];

            executor.execute(() -> {
                try {
                    downloadFile(fileUrl, destinationPath);
                    System.out.println("文件下载成功:" + destinationPath);
                } catch (IOException e) {
                    System.out.println("文件下载失败:" + destinationPath + ",原因:" + e.getMessage());
                }
            });
        }

        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有文件下载完成!");
    }

    // 下载文件的方法
    public static void downloadFile(String fileUrl, String destinationPath) throws IOException {
        // 创建HTTP请求
        Request request = new Request.Builder()
                .url(fileUrl)
                .build();

        // 发送HTTP请求并获取响应(同步请求)
        Call call = httpClient.newCall(request);
        Response response = call.execute();

        // 如果响应不成功,抛出异常
        if (!response.isSuccessful()) {
            throw new IOException("服务器返回错误:" + response.code());
        }

        // 获取文件的大小
        long fileSize = response.body().contentLength();

        // 检查目标文件是否存在,如果已经下载过一部分,则继续下载
        File destinationFile = new File(destinationPath);
        long downloadedFileSize = destinationFile.exists() ? destinationFile.length() : 0;

        // 计算剩余的字节数
        long remainingBytes = fileSize - downloadedFileSize;
        // 计算每个线程应下载的字节数
        long chunkSize = remainingBytes / NUM_THREADS;

        // 创建一个新的线程池,用于下载单个文件的多个部分
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

        for (int i = 0; i < NUM_THREADS; i++) {
            long startRange = fileSize - remainingBytes;
            long endRange = startRange + chunkSize - 1;
            if (i == NUM_THREADS - 1) {
                endRange = fileSize - 1;
            }

            // 创建下载任务并提交给线程池执行
            executor.execute(new DownloadTask(fileUrl, destinationPath, startRange, endRange, fileSize));

            remainingBytes -= chunkSize;
        }

        // 关闭线程池并等待所有下载任务完成
        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 校验文件完整性
        try {
            if (checkFileIntegrity(destinationPath)) {
                System.out.println("文件完整性校验通过:" + destinationPath);
            } else {
                System.out.println("文件完整性校验失败:" + destinationPath);
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    // 下载任务类
     static class DownloadTask implements Runnable {
        private final String fileUrl;
        private final String destinationPath;
        private final long startRange;
        private final long endRange;
        private final long fileSize;

        public DownloadTask(String fileUrl, String destinationPath, long startRange, long endRange, long fileSize) {
            this.fileUrl = fileUrl;
            this.destinationPath = destinationPath;
            this.startRange = startRange;
            this.endRange = endRange;
            this.fileSize = fileSize;
        }

        @Override
        public void run() {
            // 初始化重新重试次数
            int retryCount = 0;
            while (retryCount < MAX_RETRY_COUNT) {
                try {
                    // 创建HTTP请求,并设置请求头Range来实现断点续传
                    Request request = new Request.Builder()
                            .url(fileUrl)
                            .header("Range", "bytes=" + startRange + "-" + endRange)
                            .build();

                    // 发送HTTP请求并获取响应(同步请求)
                    Call call = httpClient.newCall(request);
                    Response  response = call.execute();

                    // 如果响应不成功,抛出异常
                    if (!response.isSuccessful()) {
                        throw new IOException("服务器返回错误:" + response.code());
                    }

                    // 创建随机访问文件对象,用于将下载的数据写入文件指定的位置
                    RandomAccessFile output = new RandomAccessFile(destinationPath, "rw");
                    output.seek(startRange);
                    byte[] buffer = new byte[1024 * 1024*2];
                    int bytesRead;
                    try (ProgressBar progressBar = new ProgressBar("下载进度", endRange - startRange + 1)) {
                        // 循环读取响应的数据,并写入文件
                        while ((bytesRead = response.body().byteStream().read(buffer)) != -1) {
                            output.write(buffer, 0, bytesRead);
                            progressBar.stepBy(bytesRead);
                        }
                    }

                    // 关闭文件和响应
                    output.close();
                    response.close();

                    break;
                } catch (IOException e) {
                    // 出现异常,进行重新下载
                    e.printStackTrace();
                    System.out.println("文件下载异常,进行重新下载...");
                    retryCount++;
                }
            }

            // 如果下载重试次数达到最大值仍然失败,则打印失败信息
            if (retryCount >= MAX_RETRY_COUNT) {
                System.out.println("文件下载失败:" + destinationPath);
            }

        }
    }

    // 校验文件完整性
    public static boolean checkFileIntegrity(String filePath) throws NoSuchAlgorithmException, IOException {
        String expectedHash = calculateFileHash(Paths.get(filePath), "MD5");
        String actualHash = calculateFileHash(Paths.get(filePath), "MD5");

        return expectedHash.equals(actualHash);
    }

    // 计算文件的哈希值
    public static String calculateFileHash(Path filePath, String algorithm) throws IOException, NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance(algorithm);
        try (FileInputStream fis = new FileInputStream(filePath.toFile())) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
        }
        byte[] hashBytes = md.digest();
        return bytesToHex(hashBytes);
    }

    // 将字节数组转换为十六进制字符串
    private static String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }


}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
1.得到服务器下载文件的大小,然后在本地设置一个临时文件和服务器端文件大小一致 a)获得访问网络地址 b)通过URL对象的openConnection()方法打开连接,返回一个连接对象 c)设置请求头 i.setRequestMethod ii.setConnectTimeout iii.setReadTimeout d)判断是否响应成功 e)获取文件长度(getContentLength()) f)随机访问文件的读取与写入RandomAccessFile(file, mode) g)设置临时文件与服务器文件大小一致(setLength()) h)关闭临时文件 2.计算出每个线程下载的大小(开始位置,结束位置) a)计算出每个线程下载的大小 b)for循环,计算出每个线程的开始、结束位置 c)最后一个线程处理 3.每创建好一次就要开启线程下载 a)构造方法 b)通过URL对象的openConnection()方法打开连接,返回一个连接对象 c)设置请求头 i.setRequestMethod ii.setConnectTimeout d)判断是否响应成功(206) e)获取每个线程返回的流对象 f)随机访问文件的读取与写入RandomAccessFile(file, mode) g)指定开始位置 h)循环读取 i.保存每个线程下载位置 ii.记录每次下载位置 iii.关闭临时记录位置文件 iv.随机本地文件写入 v.记录已下载大小 i)关闭临时文件 j)关闭输入流 4.为了杀死线程还能继续下载的情况下,从本地文件上读取已经下载文件的开始位置 a)创建保存记录结束位置的文件 b)读取文件 c)将流转换为字符 d)获取记录位置 e)把记录位置赋给开始位置 5.当你的n个线程都下载完毕的时候我进行删除记录下载位置的缓存文件 a)线程下载完就减去 b)当没有正在运行的线程时切文件存在时删除文件

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hello.Reader

请我喝杯咖啡吧😊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值