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

一、概述

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

二、具体代码实现

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

  
  <dependency>
        <groupId>me.tongfeigroupId>
      <artifactId>progressbarartifactId>
        <version>0.7.4version>
    dependency>

官方 okhttp需要的依赖


  <dependency>
      <groupId>com.squareup.okhttp3groupId>
      <artifactId>okhttpartifactId>
      <version>4.10.0version>
  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&ch=11&cr=3&dr=0&lr=all&cd=0%7C0%7C0%7C3&cv=1&br=1791&bt=1791&cs=0&ds=3&ft=bvTKJbQQqUumf7oZPo0OW_EklpPiXziScMVJEawkbfCPD-I&mime_type=video_mp4&qs=0&rc=NmZkN2U4Ozo8NGVnNmdnaUBpM3B5O2Q6ZmZvNzMzNGkzM0AtNjRiMjVeNjUxYzNiL2EvYSNmYy02cjQwZGdgLS1kLTBzcw%3D%3D&l=2023073108590933C4237837B400D5DD56&btag=e00038000&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() {
            // 初始化重新下载标志和重试次数
            boolean downloadComplete = false;
            int retryCount = 0;
            while (!downloadComplete && 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&ch=11&cr=3&dr=0&lr=all&cd=0%7C0%7C0%7C3&cv=1&br=1791&bt=1791&cs=0&ds=3&ft=bvTKJbQQqUumf7oZPo0OW_EklpPiXziScMVJEawkbfCPD-I&mime_type=video_mp4&qs=0&rc=NmZkN2U4Ozo8NGVnNmdnaUBpM3B5O2Q6ZmZvNzMzNGkzM0AtNjRiMjVeNjUxYzNiL2EvYSNmYy02cjQwZGdgLS1kLTBzcw%3D%3D&l=2023073108590933C4237837B400D5DD56&btag=e00038000&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() {
            // 初始化重新下载标志和重试次数
            boolean downloadComplete = false;
            int retryCount = 0;
            while (!downloadComplete && 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();
    }


}

你可能感兴趣的:(java,java,开发语言)