Java implementiert den gleichzeitigen und effizienten Download großer Dateien

I. Übersicht

Dies ist ein Java-Programm zum gleichzeitigen Herunterladen mehrerer großer Dateien. Es nutzt die OkHttp-Bibliothek für Netzwerkanfragen und nutzt einen Thread-Pool, um mehrere Dateien gleichzeitig herunterzuladen, wodurch die Download-Effizienz verbessert wird. Das Programm erstellt eine Download-Aufgabe und sendet sie zur Ausführung an den Thread-Pool, indem es die voreingestellte Datei-URL und den lokalen Speicherpfad durchläuft. Jede Download-Aufgabe ist für das Herunterladen eines Teils der Datei verantwortlich, und die Wiederaufnahme des Haltepunkts wird durch Festlegen des Bereichs des HTTP-Anforderungsheaders realisiert. Nachdem der Download abgeschlossen ist, überprüft das Programm die Integrität der heruntergeladenen Datei, um sicherzustellen, dass die Datei nicht beschädigt ist. Dieses Programm ist hoch erweiterbar und es können problemlos weitere Datei-URLs und lokale Speicherpfade hinzugefügt werden.

Zweitens die spezifische Code-Implementierung

Dies ist ein Problem, auf das der Autor beim Schreiben und Herunterladen eines Videos mit einer bestimmten Audiodatei gestoßen ist. Entwickelt, um mehrere große Dateien effizient aus dem Internet herunterzuladen. Der Autor lädt mittels Multithread-Segment-Haltepunkt-Lebenslauf herunter.
Importieren Sie zunächst die erforderlichen Bibliotheken : Importieren Sie die erforderlichen Bibliotheksdateien, einschließlich OkHttp für Netzwerkanfragen und ProgressBar zur Anzeige des Download-Fortschritts.
Von der offiziellen Miniodemo geforderte Abhängigkeiten

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

Die vom offiziellen okhttp geforderten Abhängigkeiten

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

Der spezifische Java-Code wird wie folgt erklärt:

Konstanten definieren: Definieren Sie einige Konstanten, z. B. die Anzahl gleichzeitiger Threads, die maximale Anzahl von Wiederholungsversuchen usw.

 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 erstellen : Erstellen Sie über die OkHttp-Bibliothek eine benutzerdefinierte OkHttpClient-Instanz, einschließlich Verbindungspooleinstellungen, um die Verbindungseffizienz zu verbessern.

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

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

Standard-Datei-URL und lokaler Speicherpfad: Definiert die URL-Liste der herunterzuladenden Dateien und die entsprechende lokale Speicherpfadliste, und Benutzer können bei Bedarf weitere Dateien hinzufügen.

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

Hauptfunktion: In der Hauptfunktion wird ein Thread-Pool erstellt und alle Datei-URLs durchlaufen. Erstellen Sie für jede Datei eine Download-Aufgabe und senden Sie sie zur Ausführung an den Thread-Pool.

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

Dateimethode herunterladen (downloadFile): Diese Methode empfängt die URL und den lokalen Pfad der Datei und initiiert eine HTTP-Anfrage, um die Dateidaten abzurufen. Wenn die Antwort erfolgreich ist, berechnet es den verbleibenden herunterzuladenden Teil basierend auf der Dateigröße und dem heruntergeladenen Teil und weist die Download-Aufgabe mehreren Threads im Thread-Pool zu.


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

Download-Aufgabenklasse (DownloadTask): Dies ist eine interne Klasse, die die Runnable-Schnittstelle implementiert und die Download-Aufgabe einer einzelnen Datei darstellt. Jede Download-Aufgabe ist für das Herunterladen eines Teils der Datei verantwortlich und realisiert die Wiederaufnahme des Haltepunkts durch Festlegen des Bereichsfelds im HTTP-Anforderungsheader. Wenn die Download-Aufgabe ausgeführt wird, werden die Dateidaten nach mehreren Versuchen in die lokale Datei geschrieben.

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

        }
    }

Methode zur Überprüfung der Dateiintegrität (checkFileIntegrity): Diese Methode prüft die Integrität der heruntergeladenen Datei, um sicherzustellen, dass die Datei nicht beschädigt oder manipuliert ist. Es berechnet den Hash der Datei und vergleicht ihn mit dem erwarteten Hash.

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

Methode zum Berechnen des Datei-Hash-Werts (calculateFileHash): Diese Methode verwendet MessageDigest, um den Hash-Wert der Datei für die anschließende Überprüfung der Dateiintegrität zu berechnen. Es liest die Bytes des Dateiinhalts, führt eine Hash-Berechnung durch und gibt schließlich einen Hash-Wert-String im Hexadezimalformat zurück.

// 计算文件的哈希值
    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. Spezifische und vollständige Code-Implementierung

Dieses Programm nutzt die OkHttp-Bibliothek und Multithreading, um die effiziente gleichzeitige Download-Funktion mehrerer Dateien zu realisieren und gleichzeitig die Integrität der heruntergeladenen Dateien sicherzustellen. Mit einer angemessenen Thread-Verwaltung und Dateiaufteilungsstrategie können die Download-Geschwindigkeit und Effizienz maximiert werden.

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


}

Ich denke du magst

Origin blog.csdn.net/weixin_43114209/article/details/132202112
Empfohlen
Rangfolge