Java断点续传

本文详细介绍了断点续传的基本原理,并通过Java代码实现了一个断点续传的示例。该示例包括多线程下载、暂停及续传等功能。

断点续传原理

断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。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);
	}
}
如上代码就是所有涉及到断点续传原理的代码,注释也十分完整,代码的阅读应该不会太难。大家有什么问题可以互相沟通。其中有一个问题,我百思不得其解,我用了断点续传之后比源文件总会少几个字节,但是文件本身是完整的。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值