我是靠谱客的博主 灵巧过客,这篇文章主要介绍java多线程下载器,现在分享给大家,希望可以做个参考。

本文主要介绍一个多线程下载器的实现方法,主要应用技术如下:

  1. Http请求;
  2. 线程池-ThreadExecutorPool;
  3. RandomAccessFile;
  4. CountDownLatch;
  5. 原子类

本文下载器的执行流程如下:

  1. 找到网上一个可供下载的链接;
  2. 发送http请求,获取下载文件信息;
  3. 设置http可分片下载,使用多线程分别对各个分片下载;
  4. 使用countDownLatch统计各个线程是否均已下载完毕;
  5. 合并各个分片成一个完整的下载文件,下载流程结束!

下载链接和主启动类

本文选取qq应用程序的下载链接作为实验对象。可以去qq官网,复制一个下载链接。

主启动类如下,其中 download 是我们要调用的多线程下载方法。

复制代码
1
2
3
4
5
6
7
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 >:为保证线程任务有返回结果,使用此种线程实现方式;
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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.*; /** * 下载器 * @author zjl * @date 2022/04/20 */ 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); /** * 下载 * @param url url */ 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<Future> 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(); } } /** * 文件切分 * @param url url * @param futureList 未来的列表 */ public void split(String url, ArrayList<Future> 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<Boolean> future = poolExecutor.submit(downloadTask); // 添加到结果集合中 futureList.add(future); } } /** * 文件合并 * @param fileName 合并的文件名前缀 */ 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; } /** * 删除临时文件 * @param fileName 文件名称 * @return boolean */ 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(*) 的括号里面流或文件的创建后是不需要手动关闭的。
这个任务的主要功能如下:

  1. 发送HTTP请求,请求下载分片后的文件数据;
  2. 将分片结果以临时文件形式保存到本地;
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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; /** * 分块下载任务 * * @author zjl * @date 2022/04/20 */ public class DownloadTask implements Callable<Boolean> { 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; } }

下载信息任务类:DownloadInfoThread

此处要注意本次下载大小 downSize 和 finishedSize 使用原子类实现。本任务类包含如下信息:

  • 文件总大小;
  • 已下载的文件大小;
  • 本次下载大小;
  • 上一次的下载大小;
  • 下载速度、剩余文件大小和剩余下载时间等。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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); } }

常量类

主要记录了一些下载相关的信息

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
/** * 常量类 * @author zjl * @date 2022/04/20 */ 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:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.example.testspring.dudemo.util; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; /** * http工具包 * @author zjl * @date 2022/04/20 */ public class HttpUtils { /** * 获得连接 * @param url url * @return {@link HttpURLConnection} * @throws IOException ioexception */ 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; } /** * 得到http文件名称 * @param url url * @return {@link String} */ public static String getHttpFileName(String url) { int index = url.lastIndexOf("/"); return url.substring(index+1); } /** * 获得分片连接 * @param url url * @param startPos 开始pos * @param endPos 终端pos * @return {@link HttpURLConnection} * @throws IOException ioexception */ 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; } /** * 获取下载文件大小 * @param url * @return long */ public static long getFileSize(String url) throws IOException { return getConnection(url).getContentLength(); } }

FileUtils:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
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

最后

以上就是灵巧过客最近收集整理的关于java多线程下载器的全部内容,更多相关java多线程下载器内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(63)

评论列表共有 0 条评论

立即
投稿
返回
顶部