基于Java的高并发多线程分片断点下载
基于Java的高并发多线程分片断点下载
首先直接看测试情况:
单线程下载72MB文件
![]()
7线程并发分片下载72MB文件:
![]()
下载效率提高2-3倍,当然以上测试结果还和设备CPU核心数、网络带宽息息相关。
一、原理
分片下载主要核心来自于HTTP/1.1中的一个header:Range,主要作用是允许用户请求网络资源中的部分片段。基于此功能,我们可以结合Java多线程来开发一个多线程分片断点下载的辅助类,具体实现流程见文章剩下内容。
二、源代码
下面看一下全部源代码
import com.sccl.autojob.util.id.SystemClock;
import lombok.AccessLevel;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Description 文件下载辅助类,支持分段并发下载,支持断点下载
* @Author Huang Yongxiang
* @Date 2022/05/27 15:56
*/
@Slf4j
public class FileDownloadHelper {
/**
* 文件元数据
*/
private FileMetaData fileMetaData;
/**
* 请求方式
*/
private String way;
/**
* 连接超时时长:ms
*/
private int connectTimeout;
/**
* 读取数据超时时长:ms
*/
private int readTimeout;
/**
* 分片数目
*/
private int splitCount;
/**
* 完整数据的连接对象
*/
private HttpURLConnection connection;
private FileSplitFetchTask[] fetchTasks;
private FileDownloadHelper() {
}
public static Builder builder() {
return new Builder();
}
private long getLength() {
try {
if (fileMetaData.length != -1) {
return fileMetaData.length;
}
HttpURLConnection connection = getConnection();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
long length = Long.parseLong(connection.getHeaderField("Content-Length"));
this.fileMetaData.length = length;
return length;
}
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
private HttpURLConnection getConnection() {
try {
if (connection == null) {
HttpURLConnection connection = (HttpURLConnection) new URL(fileMetaData.url).openConnection();
connection.setReadTimeout(readTimeout);
connection.setConnectTimeout(connectTimeout);
connection.setRequestMethod(way);
this.connection = connection;
return connection;
}
} catch (Exception e) {
log.error("获取连接时发生异常:{}", e.getMessage());
}
return connection;
}
private HttpURLConnection getSplitConnection(long startPos, long endPos) {
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) new URL(fileMetaData.url).openConnection();
connection.setReadTimeout(readTimeout);
connection.setConnectTimeout(connectTimeout);
connection.setRequestMethod(way);
String prop = "bytes=" + startPos + "-" + endPos;
log.info("分片参数:{}", prop);
connection.setRequestProperty("RANGE", prop);
} catch (IOException e) {
e.printStackTrace();
}
return connection;
}
private InputStream getInputStreamFromConnection(HttpURLConnection connection) {
try {
return connection.getInputStream();
} catch (IOException e) {
log.error("获取输入流时发生异常:{}", e.getMessage());
}
return null;
}
public FileMetaData getFileMetaData() {
return fileMetaData;
}
private FileSplitFetchTask[] createFileSplitFetchTask() {
FileSplitFetchTask[] fetchTasks = new FileSplitFetchTask[splitCount];
long length = getLength();
if (length == -1) {
log.error("文件大小未知,服务创建分片任务失败");
return fetchTasks;
} else if (length == -2) {
log.error("文件:{}不存在", fileMetaData.name);
return fetchTasks;
}
int lastEndPos = 0;
int startPos = 0;
int endPos = 0;
double averageLength = (length * 1.0) / splitCount;
for (int i = 0; i < splitCount; i++) {
if (lastEndPos != 0) {
startPos = lastEndPos + 1;
}
endPos = startPos + (int) Math.ceil(averageLength);
fetchTasks[i] = new FileSplitFetchTask(startPos, endPos, i);
lastEndPos = endPos;
}
return fetchTasks;
}
public InputStream getInputStream() {
return fileMetaData.getInputStream();
}
public boolean write() {
try {
if (fileMetaData.content != null && fileMetaData.content.size() > 0 && !StringUtils.isEmpty(fileMetaData.path)) {
File file = new File(fileMetaData.path + File.separator + fileMetaData.name);
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(fileMetaData.getContentAsByteArray());
outputStream.flush();
outputStream.close();
return true;
} else {
log.error("没有指定写入路径,写入失败");
return false;
}
} catch (Exception e) {
log.error("写入时发生异常:{}", e.getMessage());
e.printStackTrace();
}
return false;
}
public void stopDownload() {
if (this.fetchTasks != null) {
for (FileSplitFetchTask task : fetchTasks) {
task.stop();
}
}
}
public void continueDownload() {
if (this.fetchTasks != null) {
for (FileSplitFetchTask task : fetchTasks) {
task.goOn();
}
}
}
public void download() {
int totalCount = 0;
try {
//构建分片任务对象
FileSplitFetchTask[] fetchTasks = createFileSplitFetchTask();
this.fetchTasks = fetchTasks;
List<FutureTask<Integer>> futureTasks = new ArrayList<>();
for (int i = 0; i < fetchTasks.length; i++) {
FutureTask<Integer> futureTask = new FutureTask<>(fetchTasks[i]);
futureTasks.add(futureTask);
Thread thread = new Thread(futureTask);
thread.setName(String.valueOf(i));
//启动下载
thread.start();
}
//阻塞等待所有线程下载完
for (FutureTask<Integer> future : futureTasks) {
totalCount += future.get();
}
//拼接内容
for (FileSplitFetchTask task : fetchTasks) {
byte[] cache = task.getContent();
for (byte b : cache) {
fileMetaData.content.add(b);
}
}
//写出
if (!StringUtils.isEmpty(fileMetaData.path) && write()) {
log.info("写出成功,路径:{}", fileMetaData.path + File.separator + fileMetaData.name);
}
} catch (Exception e) {
e.printStackTrace();
log.error("下载过程发生异常:{}", e.getMessage());
return;
}
if (totalCount != fileMetaData.length) {
log.warn("下载字节数:{}与实际字节数:{}不匹配", totalCount, fileMetaData.length);
} else {
//log.info("下载成功");
}
}
public void clear() {
this.fileMetaData = null;
connection.disconnect();
connection = null;
System.gc();
}
/**
* 下载文件的元数据
*/
@Setter(AccessLevel.PRIVATE)
private static class FileMetaData {
/**
* 地址
*/
private String url;
/**
* 长度
*/
private long length = -1;
/**
* 写入路径
*/
private String path;
/**
* 文件名,包含后缀
*/
private String name;
/**
* 后缀,不包含.
*/
private String suffix;
/**
* 内容,二进制字列表
*/
private List<Byte> content;
private byte[] arrayContent;
/**
* 文件的输入流
*/
private InputStream inputStream;
public byte[] getContentAsByteArray() {
if (arrayContent != null) {
return arrayContent;
}
if (content != null) {
int i = 0;
byte[] holder = new byte[content.size()];
for (Byte bt : content) {
holder[i++] = bt;
}
arrayContent = holder;
return holder;
}
return new byte[]{};
}
public InputStream getInputStream() {
if (inputStream == null) {
inputStream = new ByteArrayInputStream(getContentAsByteArray());
}
return inputStream;
}
}
@Setter
@Accessors(chain = true)
public static class Builder {
/**
* 地址
*/
private String url;
/**
* 写入路径
*/
private String path;
/**
* 文件名,包含后缀
*/
private String name;
/**
* 请求方式
*/
private String way = "get";
/**
* 连接超时时长:ms
*/
private int connectTimeout = 5000;
/**
* 读取数据超时时长:ms
*/
private int readTimeout = 5000;
/**
* 是否允许分片下载
*/
private boolean allowSplitDownload = true;
/**
* 分片数目
*/
private int splitCount = 3;
public Builder setConnectTimeout(int connectTimeout, TimeUnit unit) {
this.connectTimeout = (int) unit.toMillis(connectTimeout);
return this;
}
public Builder setReadTimeout(int readTimeout, TimeUnit unit) {
this.readTimeout = (int) unit.toMillis(readTimeout);
return this;
}
public FileDownloadHelper build() {
if (!check()) {
throw new IllegalArgumentException("错误参数,请检查");
}
FileDownloadHelper fileDownloadHelper = new FileDownloadHelper();
FileMetaData fileMetaData = new FileMetaData();
if (StringUtils.isEmpty(name)) {
int namePos = url.trim().lastIndexOf("/");
name = url.substring(namePos + 1);
}
fileMetaData.setName(name);
int pos = fileMetaData.name.lastIndexOf(".");
if (pos != -1) {
fileMetaData.setSuffix(fileMetaData.name.substring(pos));
}
fileMetaData.setPath(path);
fileMetaData.setUrl(url.trim());
fileMetaData.content = new ArrayList<>();
if (way.trim().equalsIgnoreCase("get") || way.trim().equalsIgnoreCase("post")) {
fileDownloadHelper.way = way.trim().toUpperCase();
}
fileDownloadHelper.connectTimeout = connectTimeout;
fileDownloadHelper.readTimeout = readTimeout;
fileDownloadHelper.fileMetaData = fileMetaData;
fileDownloadHelper.splitCount = allowSplitDownload ? splitCount : 1;
return fileDownloadHelper;
}
private boolean check() {
boolean flag = url.lastIndexOf("/") != -1 || url.lastIndexOf(File.separator) != -1;
return !StringUtils.isEmpty(url) && flag && splitCount > 0;
}
}
private class FileSplitFetchTask implements Callable<Integer> {
/**
* 开始索引
*/
private final long startPos;
/**
* 终止索引
*/
private final long endPos;
/**
* 开始标志
*/
private boolean isStart = false;
/**
* 结束标志
*/
private boolean isOver = false;
/**
* 暂停标志
*/
private AtomicBoolean isStop = new AtomicBoolean(false);
/**
* 线程号
*/
private final int threadId;
/**
* 内容
*/
private final byte[] content;
/**
* 分片请求
*/
private final HttpURLConnection splitConnection;
public FileSplitFetchTask(long startPos, long endPos, int threadId) {
if (endPos < startPos) {
throw new IllegalArgumentException("终止索引不得小于起始索引");
}
this.startPos = startPos;
this.endPos = endPos;
this.threadId = threadId;
this.content = new byte[(int) (endPos - startPos + 1)];
this.splitConnection = getSplitConnection(startPos, endPos);
}
public byte[] getContent() {
if (isOver) {
return content;
} else {
log.error("线程:{}正在拉取,无法获取", threadId);
return null;
}
}
public void stop() {
log.info("线程:{}已暂停下载", threadId);
this.isStop.set(true);
}
public void goOn() {
log.info("线程:{}已继续下载", threadId);
this.isStop.set(false);
}
public boolean isStart() {
return isStart;
}
public boolean isOver() {
return isOver;
}
public boolean isStop() {
return isStop.get();
}
@Override
public Integer call() throws Exception {
long start = SystemClock.now();
log.info("线程:{}下载开始", threadId);
InputStream inputStream = getInputStreamFromConnection(splitConnection);
if (inputStream == null) {
throw new NullPointerException("线程:" + threadId + "下载失败,输入流为空");
}
int cache;
try {
isStart = true;
int pos = 0;
while (!isStop.get()) {
cache = inputStream.read();
if (cache != -1) {
content[pos++] = (byte) cache;
} else {
break;
}
}
log.info("线程:{}已下载完,共计:{}字节,共计用时:{}ms", threadId, pos, SystemClock.now() - start);
isStop.set(true);
isOver = true;
inputStream.close();
return pos;
} catch (IOException e) {
e.printStackTrace();
}
return 0;
}
}
}
三、源码讲解
模块说明
源码包含三个内部类,分别是FileMetaData、Builder、FileSplitFetchTask,FileMetaData是下载文件的元数据描述,包含地址、长度、写入路径、文件名、后缀以及文件的二进制内容和输入流;Builder是构建者模式中的构建者角色,用于构建FileDownloadHelper对象;FileSplitFetchTask是下载任务对象,供线程执行。
下载逻辑
用户启动下载时先访问接口获取文件长度,然后根据文件长度和分片长度创建好每个分片HttpUrlConnection对象,继而创建FileSplitFetchTask对象。一切就绪后创建线程执行每个FileSplitFetchTask,主线程异步阻塞等待子线程下载完成,并对下载后的字节数据进行组合。下载时是单个一个字节一个字节的下载,主线程可以操作FileSplitFetchTask对象,进行暂停和恢复操作。下载完成后如果构建时指定了路径,文件将会直接写入到指定路径。
四、使用
对于要下载的网络资源首先应该看其是否支持范围请求,具体方法是请求该网络资源所在URL地址,然后看返回的HTTP响应的头部是否包含请求头Accept-Ranges,并且如果该请求头的值不是none,则说明该资源支持范围请求,如下,Content-Length是该资源的完成大小。
Accept-Ranges: bytes
Content-Length: 146515
public static void main(String[] args) {
long start = SystemClock.now();
AtomicBoolean over = new AtomicBoolean(false);
String[] urls = new String[]{"https://avatar-1309914555.cos.ap-chengdu.myqcloud.com/UU-4.27.0.exe"};
for (String url : urls) {
FileDownloadHelper downloadHelper =
FileDownloadHelper.builder().setUrl(url).setWay("get").setAllowSplitDownload(true).setSplitCount(10).setConnectTimeout(5, TimeUnit.SECONDS).setReadTimeout(60, TimeUnit.SECONDS).build();
downloadHelper.download();
}
System.out.println("下载完成,总计用时:" + (SystemClock.now() - start) + "ms");
}
以上代码是一个使用示列,示列网络资源是网易的mumu加速器,存放在腾讯云的对象存储中。使用时使用构建者模式配置相关参数,创建对象,然后直接调用download方法即可开始下载,更多细节可以阅读源码。
![]()
基于Java的高并发多线程分片断点下载的更多相关文章
- java处理高并发高负载类网站的优化方法
java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据) 一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF ...
- [转]java处理高并发高负载类网站的优化方法
本文转自:http://www.cnblogs.com/pengyongjun/p/3406210.html java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,ja ...
- Java基础】并发 - 多线程
Java基础]并发 - 多线程 分类: Java2014-05-03 23:56 275人阅读 评论(0) 收藏 举报 Java 目录(?)[+] 介绍 Java多线程 多线程任务执行 大多数 ...
- 基于RTKLIB构建高并发通信测试工具
1. RTKLIB基础动态库生成 RTKLIB是全球导航卫星系统GNSS(global navigation satellite system)的标准&精密定位开源程序包,由日本东京海洋大学的 ...
- Java架构-高并发的解决实战总结方案
Java架构-高并发的解决实战总结方案 1.应用和静态资源分离 刚开始的时候应用和静态资源是保存在一起的,当并发量达到一定程度的时候就需要将静态资源保存到专门的服务器中,静态资源主要包括图片.视频.j ...
- java之高并发与多线程
进程和线程的区别和联系 从资源占用,切换效率,通信方式等方面解答 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元:而把传统的进程称为重型进程(H ...
- java系统高并发解决方案-转
转载博客地址:http://blog.csdn.net/zxl333/article/details/8685157 一个小型的网站,比如个人网站,可以使用最简单的html静态页面就实现了,配合一些图 ...
- java系统高并发解决方案(转载)
转载博客地址:http://blog.csdn.net/zxl333/article/details/8454319 转载博客地址:http://blog.csdn.net/zxl333/articl ...
- java系统高并发解决方案(转载收藏)
一个小型的网站,比如个人网站,可以使用最简单的html静态页面就实现了,配合一些图片达到美化效果,所有的页面均存放在一个目录下,这样的网站对系统架构.性能的要求都很简单,随着互联网业务的不断丰富,网站 ...
- JAVA的高并发基础认知 二
一.JAVA高级并发 1.5JDK之后引入高级并发特性,大多数的特性在java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发 ...
随机推荐
- zabbix企业监控
第一节.系统初始化 1.前期环境 主机名 IP地址 操作系统 备注 zabbix-10 192.168.2.10 CentOS Linux release 7.4 zabbix服务端 agent-15 ...
- 华为路由器vrrp(虚拟路由器冗余协议)基本配置命令
vrrp(虚拟路由器冗余协议)基本配置 int g0/0/0 vrrp vrid 1 virtual-ip 172.16.1.254 创建VRRP备份组,备份组号为1,配置虚拟IP为172.16.1. ...
- uoj221【NOI2016】循环之美
前面部分比较简单,就是无脑化式子,简单点讲好了. 首先肯定在\((x,y)=1\)时才考虑这个分数,要求纯循环的话,不妨猜猜结论,就是y必须和K互质.所以答案是\(\sum_{i=1}^n \sum_ ...
- IDEA插件MyBatisCodeHelper-Pro的破解与使用
0.前言 本文中的IDEA版本是2020.3,使用的插件版本是MyBatisCodeHelper-Pro 2.8.9,3.0+版本目前没找到激活的方式 和本文插件类似的还有mybatisX,但我不喜欢 ...
- Resilience4J通过yml设置circuitBreaker
介绍 Resilience4j是一个轻量级.易于使用的容错库,其灵感来自Netflix Hystrix,但专为Java 8和函数式编程设计. springcloud2020升级以后Hystrix被官方 ...
- 【日志系统】Loki日志监控 - 入门初体验
使用Grafana+Loki+Promtail入门级部署分布式日志系统(windows环境) 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放 ...
- Java:既然有了synchronized,为什么还要提供Lock?
摘要:在Java中提供了synchronized关键字来保证只有一个线程能够访问同步代码块.既然已经提供了synchronized关键字,那为何在Java的SDK包中,还会提供Lock接口呢?这是不是 ...
- ES6 学习笔记(十一)迭代器和生成器函数
1.前言 JavaScript提供了许多的方法来获取数组或者对象中的某个元素或者属性(迭代).从以前的for循环到之后的filter.map再到后来的for...in和for...of的迭代机制.只要 ...
- 【笔记】入门DP(Ⅱ)
0X00 P1433 吃奶酪 状压 \(DP\),把经过的点压缩成01串.若第 \(i\) 位为 \(0\) 表示未到达,为 \(1\) 则表示已到达. 用 \(f[i][j]\) 表示以 \(i\) ...
- Python中Print方法
1 number1 = int(input("请输入第一个数:")) 2 number2 = int(input("请输入第二个数:")) 3 4 # 方法一: ...