本文主要介绍一个多线程下载器的实现方法,主要应用技术如下:
- Http请求;
- 线程池-ThreadExecutorPool;
- RandomAccessFile;
- CountDownLatch;
- 原子类
本文下载器的执行流程如下:
- 找到网上一个可供下载的链接;
- 发送http请求,获取下载文件信息;
- 设置http可分片下载,使用多线程分别对各个分片下载;
- 使用countDownLatch统计各个线程是否均已下载完毕;
- 合并各个分片成一个完整的下载文件,下载流程结束!
本文选取qq应用程序的下载链接作为实验对象。可以去qq官网,复制一个下载链接。
主启动类如下,其中 download 是我们要调用的多线程下载方法。
public class Main {
public static void main(String[] args) {
String url = "https://dldir1.qq.com/qqfile/qq/PCQQ9.5.9/QQ9.5.9.28650.exe";
new Downloader().download(url);
}
}
下载类 Downloader
下载类主要包含如下几种操作:
- 具体的下载方法: download;
- 文件分片下载实现:split;
- 文件合并操作:merge;
- 移除生成的临时分片文件: removeTmpFile;
包括的操作对象如下:
- ThreadPoolExecutor poolExecutor: 分片下载任务的线程池;
- ScheduledExecutorService executorService:下载任务状态信息的线程池;
- CountDownLatch countDownLatch :保证合并操作之前,下载操作全部完成;
- RandomAccessFile accessFile:分片文件,同时提供流的读写方法;
- Callable< T >:为保证线程任务有返回结果,使用此种线程实现方式;
package com.example.testspring.dudemo.core;
import com.example.testspring.dudemo.constant.Constant;
import com.example.testspring.dudemo.util.FileUtils;
import com.example.testspring.dudemo.util.HttpUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.concurrent.*;
public class Downloader {
// 监听下载信息的线程池
public ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 创建线程池对象
public ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(Constant.THREAD_NUM,Constant.THREAD_NUM,0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(Constant.THREAD_NUM));
// CountDownLatch 保证合并之前分片都下载完毕
CountDownLatch countDownLatch = new CountDownLatch(Constant.THREAD_NUM);
public void download(String url) {
// 获取文件名
String fileName = HttpUtils.getHttpFileName(url);
// 设置文件下载路径
fileName = Constant.PATH + fileName;
// 获取文件大小
long localFileSize = FileUtils.getFileSize(fileName);
// 下载运行信息线程类
DownloadInfoThread downloadInfoThread = null;
// HTTP连接对象
HttpURLConnection connection = null;
try{
// 建立连接
connection = HttpUtils.getConnection(url);
// 获取下载文件的总大小
int totalLength = connection.getContentLength();
// 保证本文件未下载过
if(localFileSize >= totalLength) {
System.out.println("该文件已经下载过");
return;
}
// 创建获取下载信息的任务对象
downloadInfoThread = new DownloadInfoThread(totalLength);
// 获取下载状态,创建一个每秒执行一次的线程,捕获当前下载状态(大小、速度)
executorService.scheduleAtFixedRate(downloadInfoThread,1,1, TimeUnit.SECONDS);
}catch (IOException e) {
e.printStackTrace();
}
// 保证连接存在
if(connection == null) {
System.out.println("获取连接失败");
return;
}
// 括号内代码会自动关闭
try {
// 切分任务
ArrayList list = new ArrayList<>();
split(url,list);
// 保证多个线程的分片数据下载完毕
countDownLatch.await();
// 合并文件
if(merge(fileName)) {
removeTmpFile(fileName);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
System.out.print("r");
System.out.print("下载完成");
// 关闭连接
connection.disconnect();
executorService.shutdownNow();
// 关闭线程池
poolExecutor.shutdown();
}
}
public void split(String url, ArrayList futureList) throws IOException {
// 获取下载文件大小
long fileSize = HttpUtils.getFileSize(url);
// 计算切分后的文件大小
long size = fileSize / Constant.THREAD_NUM;
for (int i = 0; i < Constant.THREAD_NUM; i++) {
// 计算下载起始位置
long startPos = i * size;
long endPos;
if(i == Constant.THREAD_NUM - 1) {
// 下载的最后一块
endPos = 0;
}else {
endPos = startPos + size;
}
// 如果不是第一块,那么起始位置+1
if(startPos != 0) {
startPos++;
}
// 创建任务对象
DownloadTask downloadTask = new DownloadTask(url, startPos, endPos,i,countDownLatch);
// 提交任务到线程池
Future future = poolExecutor.submit(downloadTask);
// 添加到结果集合中
futureList.add(future);
}
}
public boolean merge(String fileName) {
System.out.print("r");
System.out.println("开始合并文件");
byte[] buffer = new byte[Constant.BYTE_SIZE];
int len = -1;
try(RandomAccessFile accessFile = new RandomAccessFile(fileName,"rw")){
for (int i = 0; i < Constant.THREAD_NUM; i++) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName + ".temp" + i))){
while((len = bis.read(buffer)) != -1) {
accessFile.write(buffer,0,len);
}
}
}
}catch (Exception e) {
e.printStackTrace();
return false;
}
System.out.println("文件合并完毕!");
return true;
}
public boolean removeTmpFile(String fileName) {
for (int i = 0; i < Constant.THREAD_NUM; i++) {
File file = new File(fileName + ".temp" + i);
file.delete();
}
return true;
}
}
下载任务 DownloadTask实现
有个有趣的点,可能你们会知道,try(*) 的括号里面流或文件的创建后是不需要手动关闭的。
这个任务的主要功能如下:
- 发送HTTP请求,请求下载分片后的文件数据;
- 将分片结果以临时文件形式保存到本地;
package com.example.testspring.dudemo.core; import com.example.testspring.dudemo.constant.Constant; import com.example.testspring.dudemo.util.HttpUtils; import java.io.BufferedInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; public class DownloadTask implements Callable下载信息任务类:DownloadInfoThread{ private String url; // 起始位置 private long startPos; // 结束位置 private long endPos; // 标识当前是哪一部分 private int part; private CountDownLatch countDownLatch; public DownloadTask(String url, long startPos, long endPos, int part,CountDownLatch countDownLatch) { this.url = url; this.startPos = startPos; this.endPos = endPos; this.part = part; this.countDownLatch = countDownLatch; } @Override public Boolean call() throws Exception { // 获取文件名 String httpFileName = HttpUtils.getHttpFileName(url); // 分块的文件名 httpFileName = httpFileName + ".temp" + part; // 下载路径 httpFileName = Constant.PATH + httpFileName; // 获取分块下载的链接 HttpURLConnection connection = HttpUtils.getConnection(url, startPos, endPos); try ( InputStream input = connection.getInputStream(); BufferedInputStream bis = new BufferedInputStream(input); RandomAccessFile accessFile = new RandomAccessFile(httpFileName,"rw"); ){ byte[] bytes = new byte[Constant.BYTE_SIZE]; int len = -1; while((len = bis.read(bytes)) != -1) { accessFile.write(bytes,0,len); // 1s内下载数据之和 DownloadInfoThread.downSize.add(len); } } catch (Exception e) { e.printStackTrace(); return false; } finally { countDownLatch.countDown(); connection.disconnect(); } return true; } }
此处要注意本次下载大小 downSize 和 finishedSize 使用原子类实现。本任务类包含如下信息:
- 文件总大小;
- 已下载的文件大小;
- 本次下载大小;
- 上一次的下载大小;
- 下载速度、剩余文件大小和剩余下载时间等。
package com.example.testspring.dudemo.core;
import com.example.testspring.dudemo.constant.Constant;
import java.util.concurrent.atomic.LongAdder;
public class DownloadInfoThread implements Runnable{
private long fileSize;
public DownloadInfoThread(long fileSize) {
this.fileSize = fileSize;
}
private static LongAdder finishedSize = new LongAdder();
public static volatile LongAdder downSize = new LongAdder();
private double preSize;
@Override
public void run() {
// 计算文件总大小 单位mb
String httpFileSize = String.format("%.2f",fileSize/ Constant.MB);
// 计算每秒下载速度 单位kb/s
int speed = (int) ((downSize.longValue() - preSize) / 1024d);
preSize = downSize.longValue();
// 剩余文件大小
double remainSize = fileSize - finishedSize.longValue() - downSize.longValue();
// 计算剩余时间
String remainTime = String.format("%.1f",remainSize / 1024d / speed);
if ("Infinity".equalsIgnoreCase(remainTime)) {
remainTime = "-";
}
// 计算已经下载大小
String currentFileSize = String.format("%.2f",(downSize.longValue() - finishedSize.longValue()) / Constant.MB);
String downloadInfo = String.format("已下载 %smb/%smb,速度 %skb/s,剩余时间 %ss", currentFileSize, httpFileSize, speed, remainTime);
System.out.print("r");
System.out.print(downloadInfo);
}
}
常量类
主要记录了一些下载相关的信息
public class Constant {
public static final double MB = 1024d * 1024d;
public static final String PATH = "C:\Users\DELL\Desktop\";
public static final int BYTE_SIZE = 100*1024;
public static final int THREAD_NUM = 5;
}
工具类
主要获取文件信息和建立http连接信息。
HttpUtils:
package com.example.testspring.dudemo.util;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpUtils {
public static HttpURLConnection getConnection(String url) throws IOException {
URL httpUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection)httpUrl.openConnection();
// 向文件所在服务器发送标识信息
connection.setRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko)Chrome/14.0.835.163 Safari/535.1");
return connection;
}
public static String getHttpFileName(String url) {
int index = url.lastIndexOf("/");
return url.substring(index+1);
}
public static HttpURLConnection getConnection(String url, long startPos,long endPos) throws IOException {
HttpURLConnection connection = getConnection(url);
System.out.println("下载的分片区间是" + startPos + "-" + endPos);
if(endPos != 0) {
// bytes = 100-200
connection.setRequestProperty("RANGE","bytes="+startPos+"-"+endPos);
}else {
// 只有 - 会下载到结尾的所有数据
connection.setRequestProperty("RANGE","bytes="+startPos+"-");
}
return connection;
}
public static long getFileSize(String url) throws IOException {
return getConnection(url).getContentLength();
}
}
FileUtils:
package com.example.testspring.dudemo.util;
import java.io.File;
public class FileUtils {
public static long getFileSize(String path) {
File file = new File(path);
return file.exists() && file.isFile() ? file.length() : 0;
}
}
运行结果
代码有很多改进和值得思考的地方,案例场景虽然并不复杂,但是胜在应用技术全面,可作为其他场景下的基础demo!
参考链接:
https://www.iqiyi.com/v_1ykiuvgozfw.html?vfrm=pcw_playpage&vfrmblk=D&vfrmrst=80521_listbox_positive#curid=7040308833907200_e5e0f6c7a8a786310017870b9526bacd



