说明

  主要功能:

    1) 分割文件, 生成下载任务;

    2) 定时任务: 检索需要下载的任务, 利用多线程下载限制下载速度;

    3) 定时任务: 检索可合并的文件, 把n个文件合并为完整的文件.

  GitHub: https://github.com/vergilyn/SpringBootDemo

  代码结构:

    

  

一、获取远程资源ContentLength、FileName

  本来以为很容易, 但如果想较好的得到contentLength、fileName其实很麻烦,主要要看download-url是怎么样的. 大致有3种:

  1) download-url: www.xxx.com/xxxx.exe,这种是最简单的.直接通过HttpURLConnection.getContentLength()就可以获取到, FileName则直接解析download-url(或从Content-Disposition中解析得到fileName).

  2) download-url: www.xxx.com/download.html?fileId=xxx, 这个实际响应的和1)一样, 只是无法直接解析download-url得到fileName, 只能从Content-Disposition中解析得到fileName.

  3) download-url跟2)类似, 但会"重定向"或"响应"一个真实下载地址, 那么就需要具体分析.

  

二、分割下载文件

  原意: 把一个大文件分割成n个小文件, 分别下载这n个小文件. 尽可能减少需要重新下载的大小. 其实就是想要"断点下载"(或称"断点续传");

  但是, 后面想了下这种"分块"感觉好蠢.更理想的实现思路可能是:

  直接往完整文件file.exe.tmp写,每次启动下载的时候读取这个file.exe.tmp的size,请求下载的Range就是bytes={size}-{contentLength}.

  代码说明: 生成n个下载任务, 保存每个下载任务的Range: bytes={beginOffse}-{endOffset}

 private void createSplitFile(CompleteFileBean fileBean){
String key = ConstantUtils.keyBlock(fileBean.getId());
String fileId = fileBean.getId();
String fileName = fileBean.getFileName();
String url = fileBean.getDownloadUrl();
long contentLength = fileBean.getContentLength(); BlockFileBean block;
List<String> blocks = new ArrayList<>(); if(contentLength <= ConstantUtils.UNIT_SIZE){
block = new BlockFileBean(fileId, getBlockName(fileName, 1), url, 0, contentLength );
blocks.add(JSON.toJSONString(block));
}else{
long begin = 0;
int index = 1;
while(begin < contentLength){
long end = begin + ConstantUtils.UNIT_SIZE <= contentLength ? begin + ConstantUtils.UNIT_SIZE : contentLength;
block = new BlockFileBean(fileId, getBlockName(fileName, index++), url, begin, end );
blocks.add(JSON.toJSONString(block));
begin += ConstantUtils.UNIT_SIZE;
}
} if(blocks.size() > 0){
// 模拟保存数据库: 生成每个小块的下载任务, 待定时器读取任务下载
redisTemplate.opsForList().rightPushAll(key, blocks);
// 保存需要执行下载的任务, 实际应用中是通过sql得到.
redisTemplate.opsForList().rightPushAll(ConstantUtils.keyDownloadTask(), key);
}
}

三、多线程下载

  线程池、线程的知识请自行baidu/google;(我也不是很了解啊 >.<!)

  实际中我只特别去了解了下:ArrayBlockingQueueCallerRunsPolicy, 根据我的理解(不一定对): 只有CallerRunsPolicy比较适用, 但当ArrayBlockingQueue等待队列达到满值时并且有新任务A-TASK进来时,CallerRunsPolicy会强制中断当前主线程去执行这个新任务A-TASK, 见:https://www.cnblogs.com/lic309/p/4564507.html.

  这是否意味着我可能有"某块"下到一半被强制中断了?虽然这下载任务并未被标记成已下载完, 但如果有大量这种中断操作, 意味着会重新去下载这部分数据.(这也反映出另外中"断点下载"思路可能更好)

  所以, 实际中我把任务等待队列设置成一定比总任务数大. 因为实际中我每天只执行一次下载定时任务, 每次只下载700个小块(即700条下载任务), 所以ArrayBlockingQueue我设置的800. 并且我没有保留核心线程

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
6,
30,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.allowCoreThreadTimeOut(true);

  分块下载, 只需用到Http请求的Range: bytes={beginOffse}-{endOffset}.

  至于哪种"下载"写法更好, 并未有太多的深究, 所以不知道具体那种"下载"的写法会更好, 但看到很多都是RandomAccessFile实现的:

  @Override
public void run() {
byte[] buffer = new byte[1024]; // 缓冲区大小
long totalSize = block.getEndOffset() - block.getBeginOffset();
long begin = System.currentTimeMillis();
InputStream is = null;
RandomAccessFile os = null;
try {
URLConnection conn = new URL(block.getDownloadUrl()).openConnection();
// -1: 因为bytes=0-499, 表示contentLength=500.
conn.setRequestProperty(HttpHeaders.RANGE, "bytes=" + block.getBeginOffset() + "-" + (block.getEndOffset() - 1));
conn.setDoOutput(true); is = conn.getInputStream(); File file = new File(tempPath + File.separator + block.getBlockFileName());
os = new RandomAccessFile(file, "rw"); int len;
while((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
} os.close(); } catch (IOException e) {
e.printStackTrace();
System.out.println(block.getBlockFileName() + " download error: " + e.getMessage());
return; // 注意要return
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
long end = System.currentTimeMillis() ;
// 简单计算下载速度, 我把连接时间也算在内了
double speed = totalSize / 1024D / (end - begin + 1) * 1000D; // +1: 避免0
System.out.println(block.getBlockFileName() + " aver-speed: " + speed + " kb/s"); // FIXME: 实际中需要更新表BlockFileBean的信息, 标记分块已下载完成, 记录平均下载速度、下载完成时间等需要的信息
// (省略)更新表BlockFileBean
}

四、限制下载速度

  看了下网上说的如何现在下载速度, 思路:

  假设下载速度上限是m(kb/s), 发送n个字节的理论耗时: n / 1024 / m (kb/s); 然而实际耗时 t(s), 那么则线程需要休眠 n / 1024 / m - t;  

  我也只是看到都是用这种方式来限速, 但我怎么觉得"很蠢", (个人理解)这种实现其实实际下载速度还是满速, 而且会频繁的存在线程的调度.

public class SpeedLimit {
private final Long speed;
// 已下载大小
private Long writeSize = 0L;
private long beginTime;
private long endTime; public SpeedLimit(Long speed, long beginTime) {
this.speed = speed;
this.beginTime = beginTime;
this.endTime = beginTime;
} public void updateWrite(int size){
this.writeSize += size;
} public void updateEndTime(long endTime) {
this.endTime = endTime;
} public Long getTotalSize() {
return totalSize;
} public Long getSpeed() {
return speed;
} public Long getWriteSize() {
return writeSize;
} public long getBeginTime() {
return beginTime;
} public long getEndTime() {
return endTime;
}
}
    @Override
public void run() {
byte[] buffer = new byte[1024]; // 缓冲区大小
long totalSize = block.getEndOffset() - block.getBeginOffset();
long begin = System.currentTimeMillis();
InputStream is = null;
RandomAccessFile os = null;
try {
// FIXME: 对下载(对文件操作)并没有太多了解, 所以不知道具体那种"下载"的写法会更好, 但看到很多都是RandomAccessFile实现的.
URLConnection conn = new URL(block.getDownloadUrl()).openConnection();
// -1: 因为bytes=0-499, 表示contentLength=500.
conn.setRequestProperty(HttpHeaders.RANGE, "bytes=" + block.getBeginOffset() + "-" + (block.getEndOffset() - 1));
conn.setDoOutput(true); is = conn.getInputStream(); File file = new File(tempPath + File.separator + block.getBlockFileName());
os = new RandomAccessFile(file, "rw"); int len;
// 是否限制下载速度
if(ConstantUtils.IS_LIMIT_SPEED){ // 限制下载速度 /* 思路:
* 假设下载速度上限是m(kb/s), 发送n个字节的理论耗时: n / 1024 / m; 然而实际耗时 t(s), 那么则需要休眠 n / 1024 / m - t;
*/
// 需要注意: System.currentTimeMillis(), 可能多次得到的时间相同, 详见其API说明.
SpeedLimit sl = new SpeedLimit(ConstantUtils.DOWNLOAD_SPEED, System.currentTimeMillis()); while((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len); sl.updateWrite(len);
sl.updateEndTime(System.currentTimeMillis()); long timeConsuming = sl.getEndTime() - sl.getBeginTime() + 1; // +1: 避免0 // 当前平均下载速度: kb/s, 实际中可以直接把 b/ms 约等于 kb/ms (减少单位转换逻辑)
double currSpeed = sl.getWriteSize() / 1024D / timeConsuming * 1000D;
if(currSpeed > sl.getSpeed()){ // 当前下载速度超过限制速度
// 休眠时长 = 理论限速时常 - 实耗时常;
double sleep = sl.getWriteSize() / 1024D / sl.getSpeed() * 1000D - timeConsuming;
if(sleep > 0){
try {
Thread.sleep((long) sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} }
}else{
while((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} os.close(); } catch (IOException e) {
e.printStackTrace();
System.out.println(block.getBlockFileName() + " download error: " + e.getMessage());
return; // 注意要return
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
long end = System.currentTimeMillis() ;
// 简单计算下载速度, 我把连接时间也算在内了
double speed = totalSize / 1024D / (end - begin + 1) * 1000D; // +1: 避免0
System.out.println(block.getBlockFileName() + " aver-speed: " + speed + " kb/s"); // FIXME: 实际中需要更新表BlockFileBean的信息, 标记分块已下载完成, 记录平均下载速度、下载完成时间等需要的信息
// (省略)更新表BlockFileBean
}

五、合并文件

  需要注意:

  1) 合并文件的顺序;

  2) stream一定要关闭;

  3) 不要把一个大文件读取到内存中.

  我乱七八糟写了(或看到)以下4种写法,并没去深究哪种更理想.可能比较推荐的RandomAccessFile或者channelTransfer的形式.

  (以下代码中的stream并不一定都关闭了, 可以检查一遍)

public class FileMergeUtil {

    /**
* 利用FileChannel.write()合并文件
*
* @param dest 最终文件保存完整路径
* @param files 注意排序
* @param capacity {@link ByteBuffer#allocate(int)}
* @see <a href="http://blog.csdn.net/skiof007/article/details/51072885">http://blog.csdn.net/skiof007/article/details/51072885<a/>
* @see <a href="http://blog.csdn.net/seebetpro/article/details/49184305">ByteBuffer.allocate()与ByteBuffer.allocateDirect()方法的区别<a/>
*/
public static void channelWrite(String dest, File[] files, int capacity) {
capacity = capacity <= 0 ? 1024 : capacity;
FileChannel outChannel = null;
FileChannel inChannel = null;
FileOutputStream os = null;
FileInputStream is = null;
try {
os = new FileOutputStream(dest);
outChannel = os.getChannel();
for (File file : files) {
is = new FileInputStream(file);
inChannel = is.getChannel();
ByteBuffer bb = ByteBuffer.allocate(capacity);
while (inChannel.read(bb) != -1) {
bb.flip();
outChannel.write(bb);
bb.clear();
}
inChannel.close();
is.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
try {
if (outChannel != null) {
outChannel.close();
}
if (inChannel != null) {
inChannel.close();
}
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
} /**
* 利用FileChannel.transferFrom()合并文件
* @param dest 最终文件保存完整路径
* @param files 注意排序
* @see <a href="http://blog.csdn.net/tobacco5648/article/details/52958046">http://blog.csdn.net/tobacco5648/article/details/52958046</a>
*/
public static void channelTransfer(String dest, File[] files) {
FileChannel outChannel = null;
FileChannel inChannel = null;
FileOutputStream os = null;
FileInputStream is = null;
try {
os = new FileOutputStream(dest);
outChannel = os.getChannel();
for (File file : files) {
is = new FileInputStream(file);
inChannel = is.getChannel();
outChannel.transferFrom(inChannel, outChannel.size(), inChannel.size()); inChannel.close();
is.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
try {
if (outChannel != null) {
outChannel.close();
}
if (inChannel != null) {
inChannel.close();
}
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
} }
} /**
* 利用apache common-IO, {@link IOUtils#copyLarge(Reader, Writer, char[])}.
* <p>看实现代码, 不就是普通write()? 没发现又什么特别的优化, 所以感觉此方式性能/效率可能并不好.</p>
* @param dest
* @param files
* @param buffer
*/
public static void apache(String dest, File[] files, int buffer){
OutputStream os = null;
try {
byte[] buf = new byte[buffer];
os = new FileOutputStream(dest);
for (File file : files) {
InputStream is = new FileInputStream(file);
IOUtils.copyLarge(is, os, buf);
is.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} /**
* 利用randomAccessFile合并文件.
* <pre>虽然用了RandomAccessFile, 但还是普通的write(), 未了解其性能....<pre/>
* @param dest
* @param files
* @param buffer
*/
public static void randomAccessFile(String dest, List<File> files, int buffer){
RandomAccessFile in = null;
try {
in = new RandomAccessFile(dest, "rw");
in.setLength(0);
in.seek(0); byte[] bytes = new byte[buffer]; int len = -1;
for (File file : files) {
RandomAccessFile out = new RandomAccessFile(file, "r");
while((len = out.read(bytes)) != -1) {
in.write(bytes, 0, len);
}
out.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} }
}

【daily】文件分割限速下载,及合并分割文件的更多相关文章

  1. PHP文件可限速下载代码

    <?php include("DBDA.class.php"); $db = new DBDA(); $bs = $_SERVER["QUERY_STRING&qu ...

  2. asp.net从服务器(指定文件夹)下载任意格式的文件到本地

    一.我需要从服务器下载ppt文件到本地 protected void Btn_DownPPT_Click(object sender, EventArgs e)        {            ...

  3. 2013第38周日Java文件上传下载收集思考

    2013第38周日Java文件上传&下载收集思考 感觉文件上传及下载操作很常用,之前简单搜集过一些东西,没有及时学习总结,现在基本没啥印象了,今天就再次学习下,记录下自己目前知识背景下对该类问 ...

  4. JS弹出下载对话框以及实现常见文件类型的下载

    写在前面 JS要实现下载功能,一般都是这么几个过程:生成下载的URL,动态创建一个A标签,并将其href指向生成的URL,然后触发A标签的单击事件,这样就会弹出下载对话框,从而实现了一个下载的功能. ...

  5. 阿里云负载均衡SLB的文件上传下载问题解决

    Nfs同步文件夹配置 问题描述 : javaweb应用部署到云服务器上时,当服务器配置了SLB负载均衡的时候,多台服务器就会造成文件上传下载获取不到文件的错误, 解决办法有:1.hdfs  2.搭建f ...

  6. JavaWeb 文件上传下载

    1. 文件上传下载概述 1.1. 什么是文件上传下载 所谓文件上传下载就是将本地文件上传到服务器端,从服务器端下载文件到本地的过程.例如目前网站需要上传头像.上传下载图片或网盘等功能都是利用文件上传下 ...

  7. javaEE(14)_文件上传下载

    一.文件上传概述 1.实现web开发中的文件上传功能,需完成如下二步操作: •在web页面中添加上传输入项•在servlet中读取上传文件的数据,并保存到本地硬盘中. 2.如何在web页面中添加上传输 ...

  8. java中的文件上传下载

    java中文件上传下载原理 学习内容 文件上传下载原理 底层代码实现文件上传下载 SmartUpload组件 Struts2实现文件上传下载 富文本编辑器文件上传下载 扩展及延伸 学习本门课程需要掌握 ...

  9. 转载:JavaWeb 文件上传下载

    转自:https://www.cnblogs.com/aaron911/p/7797877.html 1. 文件上传下载概述 1.1. 什么是文件上传下载 所谓文件上传下载就是将本地文件上传到服务器端 ...

随机推荐

  1. Basic Thought / Data Structure: 前缀和 Prefix Sum

    Intro: 在OI中,前缀和是一种泛用性很高的数据结构,也是非常重要的优化思想 Function: 求静态区间和 模板题:输入序列\(a_{1..n}\),对于每一个输入的二元组\((l,r)\), ...

  2. mysql--->innodb引擎什么时候表锁什么时候行锁?

    mysql innodb引擎什么时候表锁什么时候行锁? InnoDB基于索引的行锁 InnoDB行锁是通过索引上的索引项来实现的,这一点MySQL与Oracle不同,后者是通过在数据中对相应数据行加锁 ...

  3. 阿里云Redis性能测试结果(1个集合存300万数据,查询能几秒返回结果)

    现状: 1.买了一台主从的阿里云Redis,内存就1GB. 2.查询了阿里云的帮助,没有找到性能相关的说明, 有的也是4GB版本的并发性能 3.提工单问客服 一个集合里有300万数据,单次查询性能大概 ...

  4. C++标准模板库(STL)学习笔记

    C++标准模板库(STL) 一.vector(变长数组) 1.使用vector #include <vector> using namespace std; 2.vector的定义 vec ...

  5. Codeforces 1197E Count The Rectangles(树状数组+扫描线)

    题意: 给你n条平行于坐标轴的线,问你能组成多少个矩形,坐标绝对值均小于5000 保证线之间不会重合或者退化 思路: 从下到上扫描每一条纵坐标为y的水平的线,然后扫描所有竖直的线并标记与它相交的线,保 ...

  6. requests的post提交form-data; boundary=????

    提交这种用boundary分隔的表单数据时,有两种方法,一种是以传入files参数,另一种是传入data参数,data参数需要自己用boundary来分隔为指定的形式,而files参数则以元组的形式传 ...

  7. 《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第二节:画矩形

    有了上一节画线的基础,画矩形的各种边线就特别好理解了,所以,本节在矩形边线上,就不做过多的讲解了,关注一下画“随机矩形”的具体实现就好.与画线相比较,画矩形稍微复杂的一点就是在于它多了很多填充的样式. ...

  8. 动手学习Pytorch(6)--卷积神经网络基础

    卷积神经网络基础 本节我们介绍卷积神经网络的基础概念,主要是卷积层和池化层,并解释填充.步幅.输入通道和输出通道的含义.   二维卷积层 本节介绍的是最常见的二维卷积层,常用于处理图像数据.   二维 ...

  9. 使用sass语法生成自己的css的样式库

    前言 先说一下 sass 和 scss的区别 sass 是一种缩进语法(即没有花括号和分号,只使用换行 缩进的方式去区别子元素,PS:这是我个人的理解) scss 是css-like语法  (它的语法 ...

  10. vue学习(三)完善模板页(bootstrap+AdminLTE)

    1.配置index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...