Java implémente le téléchargement simultané et efficace de fichiers volumineux

I. Aperçu

Il s'agit d'un programme Java pour le téléchargement simultané de plusieurs fichiers volumineux. Il utilise la bibliothèque OkHttp pour les requêtes réseau et utilise un pool de threads pour télécharger plusieurs fichiers en même temps, améliorant ainsi l'efficacité du téléchargement. Le programme crée une tâche de téléchargement et la soumet au pool de threads pour exécution en parcourant l'URL du fichier prédéfini et le chemin de stockage local. Chaque tâche de téléchargement est responsable du téléchargement d'une partie du fichier, et la reprise du point d'arrêt est réalisée en définissant la plage de l'en-tête de requête HTTP. Une fois le téléchargement terminé, le programme vérifiera l'intégrité du fichier téléchargé pour s'assurer que le fichier n'est pas endommagé. Ce programme est hautement extensible, et plus d'URL de fichiers et de chemins de sauvegarde locaux peuvent être facilement ajoutés.

Deux, implémentation de code spécifique

C'est un problème que l'auteur a rencontré lors de l'écriture et du téléchargement d'une vidéo d'un certain audio. Conçu pour télécharger efficacement plusieurs fichiers volumineux à partir du Web. L'auteur télécharge au moyen d'un résumé de point d'arrêt de segment multi-thread.
Importez d'abord les bibliothèques nécessaires : importez les fichiers de bibliothèque requis, y compris OkHttp pour les requêtes réseau et ProgressBar pour afficher la progression du téléchargement.
Dépendances requises par le miniodemo officiel

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

Les dépendances requises par l'officiel okhttp

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

Le code Java spécifique est expliqué comme suit :

Définir des constantes : définissez certaines constantes, telles que le nombre de threads simultanés, le nombre maximal de tentatives, etc.

 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();

Créer OkHttpClient : créez une instance OkHttpClient personnalisée via la bibliothèque OkHttp, y compris les paramètres du pool de connexions pour améliorer l'efficacité de la connexion.

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

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

URL de fichier par défaut et chemin de stockage local : définit la liste d'URL des fichiers à télécharger et la liste de chemin de stockage local correspondante, et les utilisateurs peuvent ajouter d'autres fichiers si nécessaire.

 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"
            // 添加更多文件的本地保存路径
    };

Fonction principale : dans la fonction principale, un pool de threads est créé et toutes les URL de fichiers sont parcourues. Créez une tâche de téléchargement pour chaque fichier et soumettez-la au pool de threads pour exécution.

 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("所有文件下载完成!");
    }

Méthode de téléchargement de fichier (downloadFile) : cette méthode reçoit l'URL et le chemin d'accès local du fichier, et lance une requête HTTP pour obtenir les données du fichier. Si la réponse réussit, il calcule la partie restante à télécharger en fonction de la taille du fichier et de la partie téléchargée, et attribue la tâche de téléchargement à plusieurs threads dans le pool de threads.


    // 下载文件的方法
    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();
        }
    }

Classe de tâche de téléchargement (DownloadTask) : il s'agit d'une classe interne qui implémente l'interface Runnable, représentant la tâche de téléchargement d'un seul fichier. Chaque tâche de téléchargement est responsable du téléchargement d'une partie du fichier et réalise la reprise du point d'arrêt en définissant le champ Range dans l'en-tête de la requête HTTP. Lorsque la tâche de téléchargement est exécutée, elle écrira les données du fichier dans le fichier local après plusieurs tentatives.

   // 下载任务类
     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);
            }

        }
    }

Méthode de vérification de l'intégrité du fichier (checkFileIntegrity) : cette méthode vérifie l'intégrité du fichier téléchargé pour s'assurer que le fichier n'est pas endommagé ou falsifié. Il calcule le hachage du fichier et le compare au hachage attendu.

  // 校验文件完整性
    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);
    }

Méthode de calcul de la valeur de hachage du fichier (calculateFileHash) : cette méthode utilise MessageDigest pour calculer la valeur de hachage du fichier pour une vérification ultérieure de l'intégrité du fichier. Il lit les octets du contenu du fichier, effectue un calcul de hachage et renvoie enfin une chaîne de valeur de hachage au format hexadécimal.

// 计算文件的哈希值
    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);
    }

3. Implémentation de code spécifique et complète

Ce programme utilise la bibliothèque OkHttp et le multi-threading pour réaliser la fonction efficace de téléchargement simultané de plusieurs fichiers, et en même temps assurer l'intégrité des fichiers téléchargés. Avec une gestion raisonnable des threads et une stratégie de fractionnement des fichiers, la vitesse et l'efficacité du téléchargement peuvent être maximisées.

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() {
    
    
            // 初始化重新下载标志和重试次数
            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();
    }


}

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43114209/article/details/132202112
conseillé
Classement