Java断点续传
2016-02-19 16:38
579 查看
断点续传原理
断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。HTTP 1.1已经帮我们实现了这个功能,我们只需要在请求的时候添加相关请求属性就可以实现。当然我们也可以实现自己的断点续传内核。其中最主要的思想就是,启用多个线程对文件进行分割下载,停止下载时记住每个线程下载的当前字节位置,在下次下载时再取出这个位置继续下载。
HTTP中断点续传的使用
在请求头中添加这行语句RANGE: bytes=2000070-
具体的Java代码如下:
1.文件请求
DownThread主要是控制多线程下载
DownThreadClient主要是测试下载任务、准备下载前的条件
ShowDownLoadPercentTask主要是显示下载进度
PauseContinueUtil主要控制文件续传,提供停止和继续功能
TestDownLoadClient测试类
DownThread.java
DownThreadClient.java
ShowDownLoadPercentTask.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); } }如上代码就是所有涉及到断点续传原理的代码,注释也十分完整,代码的阅读应该不会太难。大家有什么问题可以互相沟通。其中有一个问题,我百思不得其解,我用了断点续传之后比源文件总会少几个字节,但是文件本身是完整的。
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序
- 二叉查找树