概述
多线程下载器
- 最近学习了多线程相关知识,通过一个小项目对所学知识梳理,做一个综合的运用。
项目介绍
- 该项目主要是使用HttpURLConection发起HTTP请求,再结合IO流和多线程对文件进行一个切分下载,最后合并。
项目演示
项目目录结构
代码
项目入口类,需要传入下载地址,或者在控制台输入
/**
* @author wym
* @description 主类 https://dldir1.qq.com/qqfile/qq/PCQQ9.5.6/QQ9.5.6.28129.exe
* @date 2022年01月21 14:20
*/
public class Main {
public static void main(String[] args) {
//下载地址
String url = null;
if (args == null || args.length == 0) {
while (url == null) {
LogUtils.info("请输入下载地址");
Scanner scanner = new Scanner(System.in);
url = scanner.next();
}
}else {
url = args[0];
}
Downloader downloader = new Downloader();
downloader.download(url);
}
}
通过项目入口类我们可以发现,整个项目的细节都在Downloader这个类中,要想弄清楚Downloader类中的细节,我们先把系统工具好好看一看
HttpUtils,主要通过这个工具类获取HTTP请求对象,获取所下载文件的相关信息,如:文件大小、文件名字、分块下载等。
/**
* @author wym
* @description Http工具类
* @date 2022年01月21 14:29
*/
public class HttpUtils {
/**
* 获取下载文件大小
* @param url
* @return
* @throws IOException
*/
public static long getHttpFileContentLength(String url) throws IOException {
int contentLength;
HttpURLConnection httpURLConnection = null;
try {
httpURLConnection = getHttpURLConnection(url);
contentLength = httpURLConnection.getContentLength();
} finally {
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
return contentLength;
}
/**
* 分块下载
* @param url 下载地址
* @param startPos 下载文件起始位置
* @param endPos 下载文件结束位置
* @return
*/
public static HttpURLConnection getHttpURLConnection(String url, long startPos, long endPos) throws IOException {
HttpURLConnection httpURLConnection = getHttpURLConnection(url);
LogUtils.info("下载的区间是:{}-{}",startPos,endPos);
if (endPos != 0) {
httpURLConnection.setRequestProperty("RANGE","bytes=" + startPos + "-" + endPos);
}else {
httpURLConnection.setRequestProperty("RANGE","bytes=" + startPos + "-");
}
return httpURLConnection;
}
/**
* 获取HttpURLConnection链接对象
* @param url 下载地址
* @return 返回HttpURLConnection对象
*/
public static HttpURLConnection getHttpURLConnection(String url) throws IOException {
URL httpUrl = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
//向文件所在的服务器发送标识信息
httpURLConnection.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 httpURLConnection;
}
/**
* 获取下载问文件的名字
* @param url
* @return
*/
public static String getHttpFileName(String url){
return url.substring(url.lastIndexOf("/") + 1);
}
}
FileUtils中的getFileContentLength方法,主要是用来判断该文件有没有重复下载
/**
* @author wym
* @description 文件工具类
* @date 2022年01月21 16:11
*/
public class FileUtils {
/**
* 获取本地文件的大小
*/
public static long getFileContentLength(String path){
File file = new File(path);
return file.exists() && file.isFile() ? file.length() : 0;
}
}
LogUtils 自定义日志工具类,提供了统一的日志管理,方便阅读。
public class LogUtils {
public static void info(String msg,Object... args){
print(msg,"-info-",args);
}
public static void error(String msg,Object... args){
print(msg,"-error-",args);
}
private static void print(String msg,String level,Object... args){
if (args != null && args.length > 0) {
msg = String.format(msg.replace("{}","%s"),args);
}
String name = Thread.currentThread().getName();;
System.out.println(LocalTime.now().format(DateTimeFormatter.ofPattern("hh:mm:ss")) + " " +
name + level + msg);
}
}
Downloader实现细节
- scheduledExecutorService线程池,是用来打印实时的下载信息,比如下载速度什么的。
- poolExecutor线程池,是用来进行分块下载的,将文件分为多个小块,多个线程并发下载。
- 根据阿里巴巴代码规范手册,最好使用原生方法创建线程池,我这里演示了两种创建方法
public class Downloader {
private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
private final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(Constant.THREAD_NUM,Constant.THREAD_NUM,
0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(Constant.THREAD_NUM));
private final CountDownLatch countDownLatch = new CountDownLatch(Constant.THREAD_NUM);
public void download(String url){
//获取文件名
String httpFileName = HttpUtils.getHttpFileName(url);
//文件下载路径
httpFileName = Constant.PATH + httpFileName;
//获取本地文件大小
long localFileLength = FileUtils.getFileContentLength(httpFileName);
HttpURLConnection httpURLConnection = null;
DownloadInfoThread downloadInfoThread = null;
//获取连接对象
try {
httpURLConnection = HttpUtils.getHttpURLConnection(url);
//获取下载文件的总大小
int contentLength = httpURLConnection.getContentLength();
//文件是否已下载过
if (localFileLength >= contentLength) {
LogUtils.info("{}已下载完毕,无需重新下载",httpFileName);
return;
}
//创建获取下载信息的任务对象
downloadInfoThread = new DownloadInfoThread(contentLength);
//将任务交给线程池执行,每隔一秒执行一次
scheduledExecutorService.scheduleAtFixedRate(downloadInfoThread,1,1, TimeUnit.SECONDS);
//切分对象
List<Future<Boolean>> list = new ArrayList<>();
spilt(url,list);
countDownLatch.await();
//合并文件
if (merge(httpFileName)){
clearTemp(httpFileName);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
System.out.print("r");
System.out.print("下载完成");
//关闭对象
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
//关闭线程池
scheduledExecutorService.shutdownNow();
poolExecutor.shutdown();
}
}
/**
* 文件切分
* @param url
* @param futureList
*/
public void spilt(String url, List<Future<Boolean>> futureList){
try {
//获取下载文件大小
long contentLength = HttpUtils.getHttpFileContentLength(url);
//计算切分后的文件大小
long size = contentLength / 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++;
}
//创建任务
DownloaderTask downloaderTask = new DownloaderTask(url, startPos, endPos, i,countDownLatch);
//提交任务
Future<Boolean> submit = poolExecutor.submit(downloaderTask);
futureList.add(submit);
}
}catch (IOException e){
e.printStackTrace();
}
}
/**
* 文件合并
* @param fileName
* @return
*/
public boolean merge(String fileName){
LogUtils.info("开始合并文件{}",fileName);
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;
}
return true;
}
/**
* 清除临时文件
* @param fileName
* @return
*/
public boolean clearTemp(String fileName){
for (int i = 0; i < Constant.THREAD_NUM; i++) {
File file = new File(fileName + ".temp" + i);
file.delete();
}
return true;
}
}
DownloaderTask提交给线程池的任务,也就是分块任务
/**
* @author wym
* @description 分块下载任务
* @date 2022年01月22 14:26
*/
public class DownloaderTask implements Callable<Boolean> {
private String url;
private long startPos;
private long endPos;
//分块的块号
private int part;
private CountDownLatch countDownLatch;
public DownloaderTask(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 httpURLConnection = HttpUtils.getHttpURLConnection(url, startPos, endPos);
try (
InputStream inputStream = httpURLConnection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
RandomAccessFile accessFile = new RandomAccessFile(httpFileName, "rw");
){
byte[] buffer = new byte[Constant.BYTE_SIZE];
int len = -1;
//循环读取数据
while ((len = bis.read(buffer)) != -1) {
//1秒内下载数据之和
DownloadInfoThread.downSize.add(len);
accessFile.write(buffer,0,len);
}
}catch (FileNotFoundException e){
LogUtils.error("下载文件不存在{}",url);
return false;
}catch (Exception e){
LogUtils.error("下载出现异常");
return false;
}finally {
httpURLConnection.disconnect();
countDownLatch.countDown();
}
return true;
}
}
DownloadInfo显示下载信息:
- 已下载 168.61mb/170.75mb,速度 2320kb/s,剩余时间 0.9s
/**
* @author wym
* @description 展示下载信息的线程
* @date 2022年01月21 15:51
*/
public class DownloadInfoThread implements Runnable{
//下载文件总大小
private long httpFileContentLength;
//本地已下载文件的大小
public static LongAdder finishedSize = new LongAdder();
//本次累计下载的大小
public static volatile LongAdder downSize = new LongAdder();
//前一次下载的大小
public double prevSize;
public DownloadInfoThread(long httpFileContentLength) {
this.httpFileContentLength = httpFileContentLength;
}
@Override
public void run() {
//计算文件总大小 单位:mb
String httpFileSize = String.format("%.2f",httpFileContentLength / Constant.MB);
//计算每秒下载速度kb
int speed = (int)((downSize.doubleValue() - prevSize) / 1024d);
prevSize = downSize.doubleValue();
//剩余文件的大小
double remainSize = httpFileContentLength - finishedSize.doubleValue() - downSize.doubleValue();
//计算剩余时间
String remainTime = String.format("%.1f", remainSize / 1024d / speed);
if ("Infinity".equalsIgnoreCase(remainTime)) {
remainTime = "-";
}
//已下载大小
String currentFileSize = String.format("%.2f",(downSize.doubleValue() - finishedSize.doubleValue()) / Constant.MB);
String downInfo = String.format("已下载 %smb/%smb,速度 %skb/s,剩余时间 %ss",
currentFileSize,httpFileSize,speed,remainTime);
System.out.print("r");
System.out.print(downInfo);
}
}
常量类,便于修改
/**
* @author wym
* @description 常量类
* @date 2022年01月21 14:42
*/
public class Constant {
public static final String PATH = "下载文件的存放地址,本地地址";
public static final double MB = 1024d * 1024d;
public static final int BYTE_SIZE = 1024 * 100;
//线程数量
public static final int THREAD_NUM = 5;
}
最后
以上就是无奈花生为你收集整理的多线程项目实战——多线程下载器的全部内容,希望文章能够帮你解决多线程项目实战——多线程下载器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复