这是针对多个大文件并发下载的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();
}
}