基于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;
}
} }

三、源码讲解

模块说明

源码包含三个内部类,分别是FileMetaDataBuilderFileSplitFetchTaskFileMetaData是下载文件的元数据描述,包含地址、长度、写入路径、文件名、后缀以及文件的二进制内容和输入流;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的高并发多线程分片断点下载的更多相关文章

  1. java处理高并发高负载类网站的优化方法

    java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据) 一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF ...

  2. [转]java处理高并发高负载类网站的优化方法

    本文转自:http://www.cnblogs.com/pengyongjun/p/3406210.html java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,ja ...

  3. Java基础】并发 - 多线程

    Java基础]并发 - 多线程 分类: Java2014-05-03 23:56 275人阅读 评论(0) 收藏 举报 Java   目录(?)[+]   介绍 Java多线程 多线程任务执行 大多数 ...

  4. 基于RTKLIB构建高并发通信测试工具

    1. RTKLIB基础动态库生成 RTKLIB是全球导航卫星系统GNSS(global navigation satellite system)的标准&精密定位开源程序包,由日本东京海洋大学的 ...

  5. Java架构-高并发的解决实战总结方案

    Java架构-高并发的解决实战总结方案 1.应用和静态资源分离 刚开始的时候应用和静态资源是保存在一起的,当并发量达到一定程度的时候就需要将静态资源保存到专门的服务器中,静态资源主要包括图片.视频.j ...

  6. java之高并发与多线程

    进程和线程的区别和联系 从资源占用,切换效率,通信方式等方面解答 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元:而把传统的进程称为重型进程(H ...

  7. java系统高并发解决方案-转

    转载博客地址:http://blog.csdn.net/zxl333/article/details/8685157 一个小型的网站,比如个人网站,可以使用最简单的html静态页面就实现了,配合一些图 ...

  8. java系统高并发解决方案(转载)

    转载博客地址:http://blog.csdn.net/zxl333/article/details/8454319 转载博客地址:http://blog.csdn.net/zxl333/articl ...

  9. java系统高并发解决方案(转载收藏)

    一个小型的网站,比如个人网站,可以使用最简单的html静态页面就实现了,配合一些图片达到美化效果,所有的页面均存放在一个目录下,这样的网站对系统架构.性能的要求都很简单,随着互联网业务的不断丰富,网站 ...

  10. JAVA的高并发基础认知 二

    一.JAVA高级并发 1.5JDK之后引入高级并发特性,大多数的特性在java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发 ...

随机推荐

  1. spark之交集并集差集拉链

    spark之交集并集差集拉链 def main(args: Array[String]): Unit = { val sparkConf = new SparkConf().setMaster(&qu ...

  2. Python模拟服务端

    本机服务端 import socket # 获取到socket sk = socket.socket() # 获取到地址 ip 和 端口号 address = ('127.0.0.1', 8001) ...

  3. 【C++】GoogleTest进阶之gMock

    gMock是什么 当我们去写测试时,有些测试对象很单纯简单,例如一个函数完全不依赖于其他的对象,那么就只需要验证其输入输出是否符合预期即可. 但是如果测试对象很复杂或者依赖于其他的对象呢?例如一个函数 ...

  4. 齐博x1会员中心如何加标签

    点击查看大图 轻松几步,你可以做会员中心的界面 这是调用文章的 代码如下:会员中心的标签跟前台使用方法是一模一样的, 关键之处就是多了一项动态参数 union="uid" 在以往, ...

  5. 在Rocky8中安装VMware Workstation 的方法

    在Rocky8中安装VMware Workstation 的方法 1.Rocky必须是图形界面 2.下载wmware workstation(下载地址:https://www.vmware.com/i ...

  6. fltp备份文件后统计验证

    上一篇(https://www.cnblogs.com/jying/p/16805821.html)记录了自己在centos使用lftp备份文件的过程,本篇记录自己对备份后的文件与源文件目录的对比统计 ...

  7. Python的几种lambda排序方法

    1.对单个变量进行排序 #lst = [[5,8],[5,3],[3,1]] lst.sort(key = lambda x : x[1]) #lst = [[3,1],[5,8],[5,3]] 以元 ...

  8. Window10开机键盘映射

    一.映射工具 1.github地址 https://github.com/susam/uncap 2.映射方式 (1)CapsLock映射成ESC键 uncap 0x1b:0x14 (2)CapsLo ...

  9. Pycharm下载与使用及python的基础数据类型

    1.Pycharm编辑器 1.1.下载地址 https://www.jetbrains.com/pycharm/ 1.2.Pycharm编辑器下载 1.根据自己的操作系统选择相对应的下载方式 2.尽量 ...

  10. devexpress 中advBandedGridView内容自动换行和调整自适应行高

    首先是自动换行,可以创建一个repositoryItemMemoEdit 并绑定到需要换行的列中 再设置一下repositoryItemMemoEdit高度自适应,这样子就完成了自动换行了 repos ...