说明
用java实现文件的断点续传,使用了HTTP的首部字段实现,在网上看到例子,手动实现一遍,理解其原理,在这记录下
正文
要实现断点续传,要在请求中设置请求开始的位置和结束位置,在HTTP请求中设置RANGE首部字段,之后服务器如果能正常返回,返回206状态码
用java实现的关键点:
1.设置请求的首部字段,使用java的net包
2.在读取资源文件后,要保存文件,从断点处保存,使用RandAccessFile类
3.使用多线程并发的方式进行,如何正确设置起始位置
主要思路就是:
1. 设置文件信息,包括文件所在的URL,文件名,文件保存的路径及文件需要分段下载的次数
2. 下载时,先连接服务器,得到文件的大小,通过服务器响应的首部字段Content-Length获得,得到文件大小后,根据分段下载的次数设置每次开始的位置,结束位置。并创造一个信息临时文件,用来保存每次分段下载的起始位置,用于非第一次下载时,可以直接本地读取起始信息
3. 分段下载根据开始位置,保存在下载文件的合适位置,使用RandAccessFile类的seek()方法定位
4.
1、设置首部字段
在分段抓取的类中,根据分段次数,设置开始位置
URL ourl = new URL(url);
HttpURLConnection httpConnection = (HttpURLConnection) ourl.openConnection();
String prop = "bytes=" + startPos + "-";
httpConnection.setRequestProperty("RANGE", prop); //设置请求首部字段 RANGE
分段抓取代码
package action;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import util.FileUtil;
import util.LogUtil;
/**
* 用于分段传输
* 使用HTTP协议的首部字段实现
* @author wds
*
*/
public class FileSplitFetch implements Runnable{
protected String url; // 文件所在url
protected long startPos; // 分段传输的开始位置
protected long endPos; // 结束位置
protected int threadID; // 线程编号
protected boolean downOver = false; // 下载完成标志
protected boolean stop = false; // 当前分段结束标志
FileUtil fileUtil = null; // 文件工具
public FileSplitFetch(String url, long startPos, long endPos, int threadID, String fileName) throws IOException {
super();
this.url = url;
this.startPos = startPos;
this.endPos = endPos;
this.threadID = threadID;
fileUtil = new FileUtil(fileName, startPos);
}
@Override
public void run() {
while(startPos < endPos && !stop){
try {
URL ourl = new URL(url);
HttpURLConnection httpConnection = (HttpURLConnection) ourl.openConnection();
String prop = "bytes=" + startPos + "-";
httpConnection.setRequestProperty("RANGE", prop); //设置请求首部字段 RANGE
LogUtil.log(prop);
InputStream input = httpConnection.getInputStream();
byte[] b = new byte[1024];
int bytes = 0;
while((bytes = input.read(b)) > 0 && startPos < endPos && !stop){
startPos += fileUtil.write(b, 0, bytes);
}
LogUtil.log("Thread" + threadID + " is done");
downOver = true;
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 打印响应的头部信息
* @param conn
*/
public void printResponseHeader(HttpURLConnection conn){
for(int i = 0; ; i++){
String fieldsName = conn.getHeaderFieldKey(i);
if(fieldsName != null){
LogUtil.log(fieldsName + ":" + conn.getHeaderField(fieldsName));
}else{
break;
}
}
}
/**
* 停止分段传输
*/
public void setSplitTransStop(){
stop = true;
}
}
2、文件的保存
在文件保存时,要根据开始位置保存在下载文件的适当位置,使用RandomAccessFile的seek()方法
public class FileUtil{
private RandomAccessFile file;
private long startPos; // 文件存储的起始位置
public FileUtil(String fileName, long startPos) throws IOException{
file = new RandomAccessFile(fileName, "rw");
this.startPos = startPos;
file.seek(startPos);
}
public synchronized int write(byte[] data, int start, int len){
int res = -1;
try {
file.write(data, start, len);
res = len;
} catch (IOException e) {
LogUtil.log(e.getMessage());
e.printStackTrace();
}
return res;
}
}
3、并发下载
在下载文件时,可以使用多个子线程并发进行,这里使用了一个数组保存多个线程
/**
* 开始下载文件
* 1. 获取文件长度
* 2. 分割文件
* 3. 实例化分段下载子线程
* 4. 启动子线程
* 5. 等待子线程的返回
* @throws IOException
*/
public void startDown(){
if(firstDown){
fileLen = getFileSize();
if(fileLen == -1){
LogUtil.log("文件大小未知");
return;
}else if(fileLen == -2){
LogUtil.log("文件不可访问");
return;
}
else{
// 设置每次分段下载的开始位置
for(int i = 0; i < startPos.length; i++){
startPos[i] = i * (fileLen / startPos.length);
}
//设置每次分段下载的结束位置
for(int i = 0; i < endPos.length - 1; i++){
endPos[i] = startPos[i + 1];
}
endPos[endPos.length - 1] = fileLen;
}
}
//启动分段下载子线程
try {
fileSplitFetchs = new FileSplitFetch[startPos.length];
for(int i = 0; i < startPos.length; i++){
System.out.println(startPos[i] + " " + endPos[i]);
fileSplitFetchs[i] = new FileSplitFetch(siteInfo.getUrl(), startPos[i], endPos[i], i,
siteInfo.getFilePath() + File.separator + siteInfo.getFileName());
LogUtil.log("Threa " + i + ", start= " + startPos[i] + ", end= " + endPos[i]);
new Thread(fileSplitFetchs[i]).start();
}
//保存文件下载信息
saveInfo();
//循环判断所有文件是否下载完毕
boolean breakWhile = false;
while(!stop){
LogUtil.sleep(500);
breakWhile = true;
for(int i = 0; i < startPos.length; i++){
if(! fileSplitFetchs[i].downOver){
breakWhile = false; // 还存在未下载完成的线程
break;
}
}
if(breakWhile)
break;
}
} catch (IOException e) {
LogUtil.log(e.getMessage());
e.printStackTrace();
}
LogUtil.log("文件下载完成");
}
4、设置文件的基本信息
基本信息包含了文件所在站点信息,文件本地保存路径,文件名,文件分段下载次数
public class SiteInfo implements Serializable {
private static final int SPLIT_COUNT = 5; // 默认次数为5次
private String url; // 文件所在站点的url
private String filePath; // 文件保存的路径
private String fileName; // 文件的名字
private int splits; // 分段下载文件的次数
public SiteInfo(){
this("","","",SPLIT_COUNT);
}
public SiteInfo(String url, String filePath, String fileName, int splits) {
this.url = url;
this.filePath = filePath;
this.fileName = fileName;
this.splits = splits;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public int getSplits() {
return splits;
}
public void setSplits(int splits) {
this.splits = splits;
}
public String getSimpleName(){
String[] names = fileName.split("\\.");
return names[0];
}
}
源码地址:https://github.com/Edenwds/breakpointtrans
网上例子:https://github.com/Edenwds/breakpointtrans/blob/master/java%E5%AE%9E%E7%8E%B0%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0.txt