1.原理
断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载的过程中,将一个下载文件分成了多个部分。同时进行多个部分一起的下载,当某个时间点,任务被暂停了。此时下载暂停的位置就是断点了。续传就是一个未完成的任务再次开始时,会从上次断点继续传送。
使用多线程断点续传的时候,将下载后上传任务(一个文件或者一个压缩包)人为的划分为几个部分。每一部分采用一个线程进行上传或下载,多个线程并发可以占用服务器更多资源,从而加快下载速度。在下载(或上传)过程中,如果网络故障,点量不足等原因导致下载中断,就需要用到断点续传功能。下次启动时,可以从记录位置(已下载的部分)开始,继续下载没有下载的部分,避免重复部分的下载。断点续传的实质就是能记录上一次已下载完成的位置
断点续传的过程
1 断点续传需要在下载过程中记录每条线程的下载进度;
2 每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建记录插入数据库
3 在每次向文件中写入数据之后,在数据库中更新下载进度
4 下载完成之后删除数据库中的进度
2.代码
package com.zhouzhuo.multitaskdownload;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.ProgressBar;
import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
/**
* Created by zhouzhuo on 2018/1/17.
*/
public class MainActivity extends Activity {
private String path = "http://117.169.69.238/mp3.9ku.com/m4a/186947.m4a";
private ProgressBar progressBar;
private String downloadPath = Environment.getDownloadCacheDirectory()
+ File.separator+"download";
private String targetFilePath="/"; //下载文件存放目录
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
new MuchThreadDown(path, downloadPath, 3).downLoad();
} catch (Exception e) {
e.printStackTrace();
}
}
public class MuchThreadDown {
private String targetFilePath = "/";//下载文件存放目录
private int threadCount = 3;//线程数量
private Map<Integer,ProgressBar> progressBarMap;//键 线程iD,值 进度条控件
private Handler handler;
private String path;
/**
*
* @param path 要下载文件的网络路径
* @param targetFilePath 保存下载文件的目录
* @param threadCount 开启的线程数量,默认为3
*/
public MuchThreadDown(String path,String targetFilePath,int threadCount){
this.path = path;
this.targetFilePath = targetFilePath;
this.threadCount = threadCount;
}
public Map<Integer,ProgressBar> getProgressBarMap(){
return progressBarMap;
}
public Handler getHandler(){
return handler;
}
public void setHandler(Handler handler){
this.handler = handler;
}
public void setProgressBarMap(Map<Integer,ProgressBar> progressBarMap){
this.progressBarMap = progressBarMap;
}
//下载文件
public void downLoad() throws Exception{
//连接资源
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
int code = connection.getResponseCode();
if(code == 200){
//获取资源大小
int connectionLength = connection.getContentLength();
//在本地创建一个与资源文件同样大小的文件来占位
RandomAccessFile randomAccessFile = new RandomAccessFile(new File(targetFilePath,getFileName(url)),"rw");
randomAccessFile.setLength(connectionLength);
//将下载任务分配给每个线程
//计算每个线程理论上下载的数量
int blockSize = connectionLength/threadCount;
//为每个线程分配任务
for (int threadId=0;threadId<threadCount;threadId++){
//线程开始下载的位置
int startIndex = blockSize*threadId;
//线程结束下载的位置
int endIndex = blockSize*(threadId+1)-1;
//如果是最后一个线程,将剩下的下载任务全部交给这个线程
if(threadId == threadCount -1){
endIndex = connectionLength - 1;
}
//TODO
if(progressBarMap !=null){
new DownloadThread(threadId,startIndex,endIndex,progressBarMap.get(threadId),handler);
}else {
new DownloadThread(threadId,startIndex,endIndex,null,handler).start();
}
}
}
}
}
private class DownloadThread extends Thread{
private int threadId;
private int startIndex;
private int endIndex;
private ProgressBar progressBar;
private int currentThreadTotal;//当前线程下载文件的总大小
private Handler handler;
public DownloadThread(int threadId,
int startIndex,
int endIndex,
ProgressBar progressBar,
Handler handler){
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.currentThreadTotal = endIndex -startIndex+1;
this.progressBar = progressBar;
this.handler = handler;
}
@Override
public void run() {
super.run();
Log.d("zhouzhuo","线程"+threadId+"开始下载");
//分段请求网络,分段将文件保存到本地
try {
URL url = new URL(path);
//加载下载文件的位置
File downThreadFile = new File(targetFilePath,getFileName(url)+"_downThread_"+threadId+".dt");
RandomAccessFile downThreadStream ;
if(downThreadFile.exists()){//
downThreadStream = new RandomAccessFile(downThreadFile,"rwd");
String startIndex_str = downThreadStream.readLine();
//设置下载起点
this.startIndex = Integer.parseInt(startIndex_str);
}else {
downThreadStream = new RandomAccessFile(downThreadFile,"rwd");
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
//设置分段下载的头信息。Range:做分段数据请求用的。格式:Range types = 0-1024 或者 bytes:0-1024
connection.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);
Log.d("zhouzhuo","线程_"+threadId+"的下载起点是:"+startIndex+" 下载终点是:"+endIndex);
if(connection.getResponseCode() == 206){
//200:请求全部资源成功
//206: 代表部分资源请求成功
//获取流
InputStream inputStream = connection.getInputStream();
//获取前面已创建的文件
RandomAccessFile randomAccessFile =new RandomAccessFile(
new File(targetFilePath,getFileName(url)),"rw"
);
//文件写入的开始位置
randomAccessFile.seek(startIndex);
//将网络流中的文件写入本地
byte[] buffer = new byte[1024*100];
int length = -1;
//记录本次下载文件的大小
int total = 0;
boolean flag = false;
if(progressBar !=null){
flag = true;
progressBar.setMax(currentThreadTotal);
}
while ((length = inputStream.read(buffer))>0){
randomAccessFile.write(buffer,0,length);
total += length;
//将当前下载到位置保持到文件中
int currentThreadPosition = startIndex +total;//当前文件下载位置
downThreadStream.seek(0);
downThreadStream.write(((currentThreadPosition)+"").getBytes("UTF-8"));
//设置进度条
if(flag){
progressBar.setProgress(currentThreadTotal-(endIndex-currentThreadPosition));
}
}
if(handler !=null){
Message message = Message.obtain();
message.what = 66;
message.obj = 1;
handler.sendMessage(message);
}
downThreadStream.close();
inputStream.close();
randomAccessFile.close();
//删除临时文件
cleanTemp(downThreadFile);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//获取下载文件的名称
private String getFileName(URL url) {
String filename = url.getFile();
return filename.substring(filename.lastIndexOf("/")+1);
}
//删除线程产生的临时文件
private synchronized void cleanTemp(File file){
file.delete();
}
}
3.代码分析
首先根据下载的地址获取连接流,得到下载文件的大小。然后根据下载线程的个数把文件拆分成若干断,交给各个线程去下载。
每一个下载线程里面都有下载的起始和结束位置。
通过connection.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);设置分段请求。然后通过RandomAccessFile seek移动位置,然后在相应位置RandomAccessFileWrite()写入文件。
网友评论