多线程下载,以及断点续传
- 效果图
- 多线程下载的原理
- 需要解决的几个问题
- 理清思路
- 简单封装核心代码(有修改,加入了线程池,具体看源码)
- 使用方法
- 源码下载地址
1.效果图
2.多线程下载的原理
通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。可以想象,如果用户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。
假设服务器的带宽为20M/s,服务器上有很多电影资源,现在有三位同学都想要下载 小泽.avi 这部电影,现在三位同学都在下载,所以每位同学的速度应该为1/3 * 20M/s = 6.7M/s ,但是 小泽.avi 这部电影的大小有 2G左右,这时王五同学可能有点赶时间,等不及,下的这么慢,所以他就使用他所学的多线程的知识多开了几个线程,结果他最先下完。
这次可以看到分给每个线程的带宽为1/5 * 20M/s = 4M/s,但是后面三个线程都是王五同学的,这时王五同学的带宽其实为 12M/s ,没错,王五同学成功运用多线程知识解决了下载慢的问题。(神不知鬼不觉)
看到这里我们可以知道,影响用户带宽的因素:
①服务器的带宽
②线程数
3.需要解决的几个问题
- 问题1:怎么在一个文件里面写数据的时候按照指定的位置写(因为每个线程的下载区间需要不一样,不然数据会覆盖,导致文件下不全)
- 问题2:如何去获取要下载的文件大小(因为怕下载中途需要下载其他东西,导致本次需要下载的文件内存不足,所以需要先预留一个和要下载的文件大小一样大的空间)
- 问题3:计算每个子线程的下载区间(因为每个线程的下载区间肯定不一样,不然怎么加快速度呢)
- 问题4:如何实现断点续传?
第一个问题的解决办法:
借助RandomAccessFile 随机文件访问类的 seek(long offset)方法,这个方法可以把文件的写入位置移动至offset。
第二个问题的解决办法:
我们可以使用HttpURLConnection 对象的 getContentLength() 方法得到你当前请求文件的大小。
第三个问题的解决办法:
假设下载的文件大小为10B(0-9,数组下标从0开始),线程数为3,那么
线程0的下载区间应该是: 0—2
线程1的下载区间应该是: 3—5
线程2的下载区间应该是: 6—9
每个线程下载文件的大小 = 文件长度 / 线程数 (最后一个线程除外,因为可能不能均分)
那么i线程的下载开始位置: i*每个线程下载文件的大小
i线程的下载结束位置: (i+1)*每个线程下载文件的大小 - 1
最后一个线程的结束位置为:文件长度 - 1
第四个问题的解决办法:
因为有时我们在下载下到一半的时候突然停电了,等来电时我们应该接到上次下载的地方继续下载。如何实现呢??
我们可以把每个线程下载的进度都存在一个文件里,等来电时我们先去检索有没有进度文件,如果有,说明上次下载过,但没下完,就将次进度取出来继续下载。不过线程下载的开始位置应该是 原来的开始位置+上次的进度,为了用户体验,我们应该在线程全部下载完成之后将保存的下载进度文件删除
4.理清思路
①请求网络得到需要下载的文件的大小,并生成一个和原文件一样大小的文件(先占空间)(响应码为200)
②确定每个线程的下载区间(最后一个线程的结束位置应该单独考虑)
③先查看有没有进度文件,有则从上次进度开始下载,没有则请求网络获取需要下载区间的数据,并生成下载进度文件以便断点续传。(记住请求的数据不是所有数据,而是各个线程它需要下载的那部分区间,响应码为206)
注:不是所有的服务器都支持断点续传,这取决于服务器那边。
④待各个线程全部下载完成,将进度文件删掉。
⑤开启线程下载
5.简单封装核心代码(有修改,加入了线程池,具体看源码)
public class MultiThreadUtils implements Watcher {
private static final int CODE_COUNT_PROGRESS = 0;
//完成线程数
private int finishedThread = 0;
//总得进度
private int totalProgress = 0;
//当前下载进度
private int currentProgress = 0;
//接口回调
private DownCount downCount;
/**
* @param path 下载路径
* @param fileName 文件名字
* @param threadcount 线程数
* @param downCount 接口回调
*/
public void startDownload(String path, String fileName, int threadcount, DownCount downCount) {
this.downCount = downCount;
new Multithreading(path, fileName, threadcount).start();
}
@Override
public void updataNotify(int code, Object o) {
switch (code) {
case CODE_COUNT_PROGRESS:
int currentCount = (int) o;
downCount.count(currentCount);
break;
}
}
class Multithreading extends Thread {
private String path;
private String fileName;
private int threadcount;
public Multithreading(String path, String fileName, int threadcount) {
this.path = path;
this.fileName = fileName;
this.threadcount = threadcount;
}
@Override
public void run() {
try {
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(3000);
connection.setReadTimeout(8000);
//请求成功是响应码等于200
if (connection.getResponseCode() == 200) {
//拿到需要下载的文件的大小
int length = connection.getContentLength();
//先占一个位置,生成临时文件
File file = new File(Environment.getExternalStorageDirectory(), fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
raf.close();
totalProgress = length;
//每个线程应该下载的长度。(最后一个线程除外,因为不一定能够平分)
int size = length / threadcount;
for (int i = 0; i < threadcount; i++) {
//1.确定每个线程的下载区间
//2.开启对应的子线程
int startIndex = i * size;//开始位置
int endIndex = (i + 1) * size - 1;//结束位置
//最后一个线程特殊处理它的结束位置
if (i == threadcount - 1) {
endIndex = length - 1;
}
Log.e("DownLoadThread", "文件总大小为" + totalProgress);
Log.e("DownLoadThread", "第" + (i + 1) + "个线程的下载区间为:"
+ startIndex + "-" + endIndex);
//开启线程进行下载
DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, i, path, fileName, threadcount);
downLoadThread.start();
downLoadThread.interrupt();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class DownLoadThread extends Thread {
private int lastProgress;
private int startIndex, endIndex, threadId;
private String path;
private String fileName;
private int threadcount;
public DownLoadThread(int startIndex, int endIndex, int threadId, String path, String fileName, int threadcount) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.threadId = threadId;
this.path = path;
this.fileName = fileName;
this.threadcount = threadcount;
AchieveWatched.getInstance().add(MultiThreadUtils.this);
}
@Override
public void run() {
//简历进度临时文件,其实现在还没创建,当往里面写东西的时候才创建
File progressFile = new File(Environment.getExternalStorageDirectory(), threadId + ".txt");
//判断临时文件是够存在,没存在表示已经下载过,但还没有下载完事
try {
if (progressFile.exists()) {
FileInputStream inputStream = new FileInputStream(progressFile);
BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream));
//从进度临时文件中读取上一次下载的总进度,然后与原本的开始位置相加。得到新的开始位置
lastProgress = Integer.valueOf(bf.readLine());
startIndex += lastProgress;
currentProgress += lastProgress;
bf.close();
inputStream.close();
}
//请求数据
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(8000);
connection.setConnectTimeout(3000);
//设置本次http请求所请求的数据的区间(“这是需要服务器那边支持断点”),格式需要这样写,不能写错
connection.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
if (connection.getResponseCode() == 206) {
//此时流中只有大概1/3原数据,因为开启了3个子线程分别下载各自的区间内容
InputStream inputStream = connection.getInputStream();
File file = new File(Environment.getExternalStorageDirectory(), fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
//把文件的写入位置移动至startIndex
raf.seek(startIndex);
byte[] b = new byte[1024 * 8];
int len = 0;
int total = lastProgress;
while ((len = inputStream.read()) != -1) {
raf.write(b, 0, len);
total += len;
Log.e("DownLoadThread", "线程" + threadId + "下载了" + total);
//生成一个专门用来记录下载进度的临时文件
RandomAccessFile progressRaf = new RandomAccessFile(progressFile, "rwd");
//每次读取流里面的数据之后,同步把当前线程下载的总进度写入进度临时文件中
progressRaf.write((total + "").getBytes());
progressRaf.close();
currentProgress += len;
AchieveWatched.getInstance().notifyWatcher(CODE_COUNT_PROGRESS, (int) ((((float) currentProgress / totalProgress)) * 100));
}
raf.close();
//每完成一个线程就+1
finishedThread++;
//等标志位等于线程数的时候就说明线程全部完成了
if (finishedThread == threadcount) {
for (int i = 0; i < finishedThread; i++) {
//将生成的临时文件删除
File f = new File(Environment.getExternalStorageDirectory(), i + ".txt");
f.delete();
AchieveWatched.getInstance().clear();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public interface DownCount {
void count(int count);
}
}
6.使用方法
new MultiThreadUtils().startDownload(path, fileName, THREADCOUNT, new MultiThreadUtils.DownCount() {
@Override
public void count(int count) {
pb.setProgress(count);
Log.e("aaa", count + "%");
}
});
7.源码下载地址
源码下载地址:https://github.com/zxp19920626/MultiThreadUtils
参考博客:http://blog.csdn.net/qq_22063697/article/details/51568066