您的位置:首页 > 编程语言 > Java开发

Java断点续传

2016-02-19 16:38 579 查看
断点续传原理

断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。HTTP 1.1已经帮我们实现了这个功能,我们只需要在请求的时候添加相关请求属性就可以实现。当然我们也可以实现自己的断点续传内核。其中最主要的思想就是,启用多个线程对文件进行分割下载,停止下载时记住每个线程下载的当前字节位置,在下次下载时再取出这个位置继续下载。

HTTP中断点续传的使用

在请求头中添加这行语句RANGE: bytes=2000070-

具体的Java代码如下:

1.文件请求

URL url = new URL("http://www.sjtu.edu.cn/down.zip");
HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection();

// 设置 User-Agent
httpConnection.setRequestProperty("User-Agent","NetFox");
// 设置断点续传的开始位置
httpConnection.setRequestProperty("RANGE","bytes=2000070");
// 获得输入流
InputStream input = httpConnection.getInputStream();
2.文件保存

RandomAccess oSavedFile = new RandomAccessFile("down.zip","rw");
long nPos = 2000070;
// 定位文件指针到 nPos 位置
oSavedFile.seek(nPos);
byte[] b = new byte[1024];
int nRead;
// 从输入流中读入字节流,然后写到文件中
while((nRead=input.read(b,0,1024)) > 0) {
oSavedFile.write(b,0,nRead);
}
HTTP请求文件的断点续传很简单吧,下面把我自己实现的一个断点续传实例贴上来。我的上一篇文章Java带进度多线程下载文件中实现了多线程断点下载,本文就是在上文的基础上添加续传的功能。对DownThreadClient进行了少许的改动,新增了一个PauseContinueUtil工具类,用来暂停和继续。

DownThread主要是控制多线程下载

DownThreadClient主要是测试下载任务、准备下载前的条件

ShowDownLoadPercentTask主要是显示下载进度

PauseContinueUtil主要控制文件续传,提供停止和继续功能

TestDownLoadClient测试类

DownThread.java

package com.ds.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;

public class DownThread extends Thread {

private final int BUFF_LEN = 1024;
private InputStream inputStream;
private RandomAccessFile raf;
private long start;
private long end;
private int flag = 1;

/**
* @param start 下载开始位置
* @param end 下载结束位置
* @param inputStream 输入流
* @param raf 输出流
* @param flag 第n个线程
*/
public DownThread(long start,long end,InputStream inputStream,RandomAccessFile raf,int flag){
this.start = start;
this.end = end;
this.inputStream = inputStream;
this.raf = raf;
this.flag = flag;
}

public void run(){
//System.out.println("Thread "+ flag +" start!");
try {
//初始化输入输出流的位置
inputStream.skip(start);
raf.seek(start);
byte[] buffer = new byte[BUFF_LEN];
long contentLen = end - start;
//设置读取界限,避免超过线程读取的文件分区范围,区间数为times+1
int times = (int)(contentLen/BUFF_LEN);
int hasRead = 0;
//根据读取界限读取文件
for(int i=0;i<=times;i++){
hasRead = inputStream.read(buffer);
if(hasRead == -1) break;
if(i==times){
raf.write(buffer, 0, (int)(contentLen%BUFF_LEN));
}else {
raf.write(buffer, 0, BUFF_LEN);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally{
//统一由发起输入输出流的类关闭
//inputStream.close();
//raf.close();
}
}
}

DownThreadClient.java

package com.ds.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Timer;

import com.hundsun.jres.common.util.UUID;

public class DownThreadClient {

//默认启动4个线程
private int threadAccount = 4;
private String fileSavePath = "C:/Users/Administrator/Desktop/";
private InputStream[] inputStreams;//输入流
private RandomAccessFile[] rdfs;//输出流
private File file;//待下载文件,此处是下载本机的文件到本机,后面也可以 扩展为从服务器上下载
private String bakFilePath;//临时的备份文件地址

//为扩展续传功能,添加下载起始点的数组
private ArrayList<Long> starts = new ArrayList<Long>();//下载开始点数组
private ArrayList<Long> ends = new ArrayList<Long>();//下载结束点数组
private String newFileName;//下载时生成新的文件名
private ArrayList<DownThread> downThreads = new ArrayList<DownThread>();//线程数组
private Timer timer;//定时器,用于进度显示

public Timer getTimer() {
return timer;
}

public ArrayList<Long> getStarts() {
return starts;
}

public void setStarts(ArrayList<Long> starts) {
this.starts = starts;
}

public ArrayList<Long> getEnds() {
return ends;
}

public String getNewFileName() {
return newFileName;
}

public String getFileSavePath() {
return fileSavePath;
}

public String getBakFilePath() {
return bakFilePath;
}

public int getThreadAccount() {
return threadAccount;
}

public InputStream[] getInputStreams() {
return inputStreams;
}

public File getFile() {
return file;
}

public DownThreadClient() {
super();
}

/**
* @param threadAccount 线程数
* @param fileSavePath 存储文件目录
* @param file 要下载的文件
* @param bakFilePath 备份文件
* @param starts 文件下载起点数组
* @param ends 问价下载结束点数组
* @param newFileName 新文件名
*/
public DownThreadClient(int threadAccount, String fileSavePath, File file,
String bakFilePath, ArrayList<Long> starts,
ArrayList<Long> ends, String newFileName) {
super();
this.threadAccount = threadAccount;
this.fileSavePath = fileSavePath;
this.file = file;
this.bakFilePath = bakFilePath;
this.starts = starts;
this.ends = ends;
this.newFileName = newFileName;
inputStreams = new InputStream[threadAccount];
rdfs = new RandomAccessFile[threadAccount];
}

/**
* @param threadAccount 线程数
* @param fileSavePath 新文件存储目录
* @param file 要下载的文件
*/
public DownThreadClient(int threadAccount, String fileSavePath, File file) {
this.threadAccount = threadAccount;
this.fileSavePath = fileSavePath;
this.file = file;
inputStreams = new InputStream[threadAccount];
rdfs = new RandomAccessFile[threadAccount];
}

/**
* @param fileSavePath 新文件存储目录
* @param file 要下载的文件
*/
public DownThreadClient(String fileSavePath, File file) {
this.fileSavePath = fileSavePath;
this.file = file;
inputStreams = new InputStream[threadAccount];
rdfs = new RandomAccessFile[threadAccount];
}

/**
* 拼接出存储文件的绝对路径,文件名随机生成
* @param oldFileName 原始文件名
* @return
*/
public String getFilePath(String oldFileName){
//获取原始文件名的后缀
String suffix = oldFileName.substring(oldFileName.lastIndexOf("."));
UUID uuid = UUID.randomUUID();
String fileName = uuid.toString()+suffix;
return fileSavePath+fileName;
}

//设置一个相同大小的空备份文件,避免磁盘空间不足
public void creatBlankFile() throws Exception{
String filePath = getFilePath("xx.bak");
RandomAccessFile raf = new RandomAccessFile(filePath, "rw");
raf.setLength(file.length());
raf.close();
bakFilePath = filePath;
}

//删除备份文件
public void deleteBlankFile(String filePath) throws Exception{
File file = new File(filePath);
if(file.exists()){
file.delete();
}
}

//开始下载任务
public void downLoad() throws Exception{
//start无值则表示不是断点续传,则不生成备份文件
if(starts.size()==0){
creatBlankFile();
}
long fileLen = file.length();//文件总长度
long partLen = fileLen/threadAccount;//分区长度
String newFilePath = newFileName==null ?
getFilePath(file.getName()):(fileSavePath+newFileName);//文件存储新路径
newFileName = newFilePath.substring(newFilePath.lastIndexOf("/")+1);
for(int i=0;i<threadAccount;i++){
long start = 0;
long end = 0;
//b不是断点续传,按常规设置起始点、结束点
if(starts.size()==0){
start = i* partLen;
end = (i+1)*partLen;
ends.add(end);
}else {//是断点续传,读取传过来的起始点、结束点
start = starts.get(i);
end = ends.get(i);
}
//初始化输入输出流
inputStreams[i] = new FileInputStream(file);
rdfs[i] = new RandomAccessFile(newFilePath, "rw");
//如果是最后一段,并且不是续传,则设置下载结束位置为文件最末尾
if(i==threadAccount-1 && starts.size()==0){
end = file.length();
}
//初始化并开启下载线程
DownThread downThread = new DownThread(start, end, inputStreams[i], rdfs[i], i);
downThreads.add(downThread);
downThread.start();
}
}

/**
* 获取下载进度
* @param dtc DownThreadClient对象
*/
public void getDownLoadPercent(DownThreadClient dtc){
Timer timer = new Timer();
ShowDownLoadPercentTask sdlp = new ShowDownLoadPercentTask(dtc, timer);
//延迟1秒开启任务,每秒钟执行一次
timer.schedule(sdlp, 1000, 1000);
this.timer = timer;
}

//关闭输入输出流
public void closeIs(){
try {
for(int i=0; i<threadAccount; i++){
inputStreams[i].close();
rdfs[i].close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

//结束线程
public void stopThread(){
for(DownThread downThread:downThreads){
downThread.stop();
}
}

}


ShowDownLoadPercentTask.java

package com.ds.io;

import java.io.IOException;
import java.io.InputStream;
import java.util.Timer;
import java.util.TimerTask;

public class ShowDownLoadPercentTask extends TimerTask{

private Timer timer;
private DownThreadClient dtc;

/**
* @param dtc DownThreadClient对象
* @param timer 定时器
*/
public ShowDownLoadPercentTask(DownThreadClient dtc, Timer timer) {
super();
this.dtc = dtc;
this.timer = timer;
}

public void run() {
long currentLen = 0;
long totleLen = dtc.getFile().length();
try {
//计算已读取的字节数
for(int i=0; i<dtc.getThreadAccount(); i++){
//计算方式:已读长度=总长度-可读长度-跳过长度
currentLen += (totleLen - dtc.getInputStreams()[i].available()
-i*(totleLen/dtc.getThreadAccount()));
}
//获取下载进度
double percent = Math.ceil(currentLen*1.0/totleLen*10000);
if(percent >= 10000) {
//停止定时任务,关闭输入输出流,删除备份文件
timer.cancel();
dtc.closeIs();
dtc.deleteBlankFile(dtc.getBakFilePath());
System.out.println(dtc.getBakFilePath());
System.out.println("100%\n下载完成");
}else {
System.out.println(percent/100.0+"%");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
PauseContinueUtil.java

package com.ds.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;

public class PauseContinueUtil {

/**
* 保存数据,停止下载
* @param dtc DownThreadClient对象
* @throws Exception
*/
public static void stopDownLoad(DownThreadClient dtc) throws Exception{
//停止线程、IO、定时器
stopThreadIO(dtc);
//组装必要数据保存到文件中:即初始化DownThreadClient需要的变量以及下载起始点和结束点
//以键值对的方式存储下载信息,方便以后读取
ArrayList<String> writeString = new ArrayList<String>();
writeString.add("fileSavePath="+dtc.getFileSavePath());
writeString.add("newFileName="+dtc.getNewFileName());
writeString.add("oldFilePath="+dtc.getFile().getAbsolutePath().replaceAll("\\\\", "/"));
writeString.add("bakFilePath="+dtc.getBakFilePath());
writeString.add("threadAccount="+dtc.getThreadAccount());
String arrayString = "positionArray=";
for(int i=0;i<dtc.getStarts().size();i++){
arrayString += dtc.getStarts().get(i)+","+dtc.getEnds().get(i)+";";
}
writeString.add(arrayString);
String tempFilePath = dtc.getFilePath("xx.properties");
OutputStreamWriter outputStream = new FileWriter(tempFilePath);
for(String s:writeString){
outputStream.write(s+"\n");
}
outputStream.close();
}

/**
* 从文件中读取数据,开始下载
* @param tempFilePath 存放下载信息的配置文件路径
* @return 返回DownThreadClient对象
* @throws Exception
*/
public static DownThreadClient startDownload(String tempFilePath) throws Exception{
//以Properties方式读取文件
Properties properties = new Properties();
InputStream inputStream = new FileInputStream(tempFilePath);
properties.load(inputStream);
//读取文件必要信息
String fileSavePath = properties.getProperty("fileSavePath");
String newFileName = properties.getProperty("newFileName");
String oldFilePath = properties.getProperty("oldFilePath");
String bakFilePath = properties.getProperty("bakFilePath");
int threadAccount = Integer.valueOf(properties.getProperty("threadAccount"));
ArrayList<ArrayList<Long>> arrayLists = getPositonArray(properties.getProperty("positionArray"));
DownThreadClient dtc = new DownThreadClient(threadAccount, fileSavePath, new File(oldFilePath),
bakFilePath, arrayLists.get(0), arrayLists.get(1), newFileName);
inputStream.close();//关闭输入流
File file = new File(tempFilePath);
file.delete();//删除配置文件
return dtc;
}

/**
* 组装下载起始点、结束点的数组
* @param positionArray 下载起始点结束点的字符串
* @return
*/
public static ArrayList<ArrayList<Long>> getPositonArray(String positionArray){
ArrayList<ArrayList<Long>> arrayList = new ArrayList<ArrayList<Long>>();
ArrayList<Long> arrayListStarts = new ArrayList<Long>();
ArrayList<Long> arrayListEnds = new ArrayList<Long>();
//以分号分隔出单组起始结束点
String[] array1 = positionArray.split(";");
for(String element:array1){
if(!StringUtils.isEmpty(element)){
//以逗号分隔出每组的起始点和结束点
String[] array2 = element.split(",");
arrayListStarts.add(Long.valueOf(array2[0]));
arrayListEnds.add(Long.valueOf(array2[1]));
}
}
arrayList.add(arrayListStarts);
arrayList.add(arrayListEnds);
return arrayList;
}

/**
* 停止多线程、定时器、收集下载点的位置、关闭输入输出流
* @param dtc
* @throws Exception
*/
public static void stopThreadIO(DownThreadClient dtc) throws Exception{
dtc.stopThread();
dtc.getTimer().cancel();
//收集下载点的位置
ArrayList<Long> starts = new ArrayList<Long>();
for(int i=0;i<dtc.getThreadAccount();i++){
long startPositon = dtc.getFile().length() - dtc.getInputStreams()[i].available();
starts.add(startPositon);
}
dtc.setStarts(starts);
dtc.closeIs();
System.out.println("下载已暂停");
}

}
TestDownLoadClient.java

package com.ds.io;

import java.io.File;

public class TestDownLoadClient {

public static void main(String arg[]) throws Exception{
testStop();
//testContinue();
}

//测试停止方法
public static void testStop() throws Exception{
String filePath = "E:/Linux.pdf";
String fileSavePath = "C:/Users/Administrator/Desktop/";
File file = new File(filePath);
DownThreadClient dtc = new DownThreadClient(fileSavePath, file);
dtc.downLoad();
//显示下载进度
dtc.getDownLoadPercent(dtc);
Thread.sleep(100);
//停止下载
PauseContinueUtil.stopDownLoad(dtc);
}

//测试继续方法
public static void testContinue() throws Exception{
//继续下载,参数为停止下载后生成的属性文件路径
DownThreadClient dtc = PauseContinueUtil.startDownload("C:/Users/Administrator/Desktop/3701f73a0ca44653b009286b7357739f.properties");
dtc.downLoad();
//显示下载进度
dtc.getDownLoadPercent(dtc);
}
}
如上代码就是所有涉及到断点续传原理的代码,注释也十分完整,代码的阅读应该不会太难。大家有什么问题可以互相沟通。其中有一个问题,我百思不得其解,我用了断点续传之后比源文件总会少几个字节,但是文件本身是完整的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息