JAVA 实现 HTTP 断点续传及原理

断点续传原理:
现在有一个文件需要我们进行下载,当我们下载了一部分的时候,出现情况了,比如:电脑死机、没电、网络中断等等。 对于以上行为,如果“下载”的行为无法记录本次下载的一个进度。那么,当我们再次下载这个文件也就只能从头来过。
所以,要实现让一种断开的行为“续”起来的目的,关键就在于要有“介质”能够记录和读取行为出现”中断”的这个节点的信息。
实际上这就是“断点续传”的基础原理,用大白话说就是:我们要在下载行为出现中断的时候,记录下中断的位置信息,在新的下载行为开始的时候,直接从记录的这个位置开始下载内容,而不再从头开始。

实现步骤:
1、当“上传(下载)的行为”出现中断,我们需要记录本次上传(下载)的位置(position)。
2、当“续”这一行为开始,我们直接跳转到postion处继续上传(下载)的行为。 

实现方式:

要从文件已经下载的地方开始继续下载,需要在客户端浏览器传给 Web 服务器的时候要多加一条信息(告诉服务器要从哪里开始下载),如下面要求从 2000070 字节开始。

GET /abc.exe HTTP/1.0   
User-Agent: NetFox   
RANGE: bytes=2000070-   
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2  


这里多了一行 RANGE: bytes=2000070- 
这一行的意思就是告诉服务器 abc.exe 穿上文件从 2000070 字节开始传,前面的字节不用传了。 服务器收到这个请求以后,返回的信息如下: 

206   
Content-Length=106786028   
Content-Range=bytes 2000070-106786027/106786028   
Date=Mon, 30 Apr 2001 12:55:20 GMT   
ETag=W/"02ca57e173c11:95b"   
Content-Type=application/octet-stream   
Server=Microsoft-IIS/5.0   
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT 


返回的信息增加了一行: Content-Range=bytes 2000070-106786027/106786028 
返回的代码也变成 206 ,而不再是 200 了。知道了这些原理,就可以进行断点续传的编程了。

关键点:
1、用什么方法实现提交 RANGE: bytes=2000070-。 
用最原始的 Socket 是肯定能完成的,那样太费事了,Java 的 net 包中已经提供了这种功能。代码如下: 

URL url = new URL("http://www.sjtu.edu.cn/abc.exe");   
HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection();   
  
// 设置 User-Agent   
httpConnection.setRequestProperty("User-Agent","NetFox");   
// 设置断点续传的开始位置   
httpConnection.setRequestProperty("RANGE","bytes=2000070");   
// 获得输入流   
InputStream input = httpConnection.getInputStream();


从输入流中取出的字节流就是 abc.exe 文件从 2000070 开始的字节流。 其实断点续传用 Java 实现起来还是很简单的,接下来要做的事就是怎么保存获得的流到文件中去。

 

2、保存文件采用的方法。 
我采用的是 IO 包中的 RandAccessFile 类。 API文档中对该类的说明:
此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。
如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。
写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。
操作相当简单,假设从 2000070 处开始保存文件,代码如下: 

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);   
} 
接下来要做的就是整合成一个完整的程序了。包括一系列的线程控制等等。

断点续传完整实现:
主要用了 6 个类,包括一个测试类:
SiteFileFetch.java         负责整个文件的抓取,控制内部线程 (FileSplitterFetch 类 )。 
FileSplitterFetch.java    负责部分文件的抓取。 
FileAccess.java            负责文件的存储。 
SiteInfoBean.java         要抓取的文件的信息,如文件保存的目录,名字,抓取文件的 URL 等。 
Utility.java                    工具类,放一些简单的方法。 
TestMethod.java          测试类。

SiteFileFetch.java 

package com.bocom.sb.http.file;
 
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
 
/**
 * 
 * 负责整个文件的抓取,控制内部线程(FileSplitterFetch类)
 * @author xiaoming
 * @since 2017-8-29
 */
public class SiteFileFetch extends Thread {
	// 文件信息Bean
	SiteInfoBean siteInfoBean = null;
	// 开始位置
	long[] nStartPos;
	// 结束位置
	long[] nEndPos;
	// 子线程对象
	FileSplitterFetch[] fileSplitterFetch;
	// 文件长度
	long nFileLength;
	// 是否第一次取文件
	boolean bFirst = true;
	// 停止标志
	boolean bStop = false;
	// 文件下载的临时信息
	File tmpFile;
	// 输出到文件的输出流
	DataOutputStream output;
 
	public SiteFileFetch(SiteInfoBean bean) throws IOException {
		siteInfoBean = bean;
		// tmpFile = File.createTempFile ("zhong","1111",new
		// File(bean.getSFilePath()));
		tmpFile = new File(bean.getSFilePath() + File.separator + bean.getSFileName() + ".info");
		if (tmpFile.exists()) {
			bFirst = false;
			read_nPos();
		} else {
			nStartPos = new long[bean.getNSplitter()];
			nEndPos = new long[bean.getNSplitter()];
		}
	}
 
	public void run() {
		// 获得文件长度
		// 分割文件
		// 实例FileSplitterFetch
		// 启动FileSplitterFetch线程
		// 等待子线程返回
		try {
			if (bFirst) {
				nFileLength = getFileSize();
				if (nFileLength == -1) {
					System.err.println("File Length is not known!");
				} else if (nFileLength == -2) {
					System.err.println("File is not access!");
				} else {
					for (int i = 0; i < nStartPos.length; i++) {
						nStartPos[i] = (long) (i * (nFileLength / nStartPos.length));
					}
					for (int i = 0; i < nEndPos.length - 1; i++) {
						nEndPos[i] = nStartPos[i + 1];
					}
					nEndPos[nEndPos.length - 1] = nFileLength;
				}
			}
			// 启动子线程
			fileSplitterFetch = new FileSplitterFetch[nStartPos.length];
			for (int i = 0; i < nStartPos.length; i++) {
				fileSplitterFetch[i] = new FileSplitterFetch(siteInfoBean.getSSiteURL(),
						siteInfoBean.getSFilePath() + File.separator + siteInfoBean.getSFileName(), nStartPos[i],
						nEndPos[i], i);
				Utility.log("Thread " + i + " , nStartPos = " + nStartPos[i] + ", nEndPos = " + nEndPos[i]);
				fileSplitterFetch[i].start();
			}
			// fileSplitterFetch[nPos.length-1] = new
			// FileSplitterFetch(siteInfoBean.getSSiteURL(),
			// siteInfoBean.getSFilePath() + File.separator +
			// siteInfoBean.getSFileName(),nPos[nPos.length-1],nFileLength,nPos.length-1);
			// Utility.log("Thread " + (nPos.length-1) + " , nStartPos = " +
			// nPos[nPos.length-1] + ", nEndPos = " + nFileLength);
			// fileSplitterFetch[nPos.length-1].start();
			// 等待子线程结束
			// int count = 0;
			// 是否结束while循环
			boolean breakWhile = false;
			while (!bStop) {
				write_nPos();
				Utility.sleep(500);
				breakWhile = true;
				for (int i = 0; i < nStartPos.length; i++) {
					if (!fileSplitterFetch[i].bDownOver) {
						breakWhile = false;
						break;
					}
				}
				if (breakWhile)
					break;
				// count++;
				// if(count>4)
				// siteStop();
			}
			System.err.println("文件下载结束!");
		} catch (Exception e) {
			e.printStackTrace();
		}
	} // 获得文件长度
 
	public long getFileSize() {
		int nFileLength = -1;
		try {
			URL url = new URL(siteInfoBean.getSSiteURL());
			HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
			httpConnection.setRequestProperty("User-Agent", "NetFox");
			int responseCode = httpConnection.getResponseCode();
			if (responseCode >= 400) {
				processErrorCode(responseCode);
				return -2; // -2 represent access is error
			}
			String sHeader;
			for (int i = 1;; i++) {
				// DataInputStream in = new
				// DataInputStream(httpConnection.getInputStream ());
				// Utility.log(in.readLine());
				sHeader = httpConnection.getHeaderFieldKey(i);
				if (sHeader != null) {
					if (sHeader.equals("Content-Length")) {
						nFileLength = Integer.parseInt(httpConnection.getHeaderField(sHeader));
						break;
					}
				} else
					break;
			}
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		Utility.log(nFileLength);
		return nFileLength;
	}
 
	// 保存下载信息(文件指针位置)
	private void write_nPos() {
		try {
			output = new DataOutputStream(new FileOutputStream(tmpFile));
			output.writeInt(nStartPos.length);
			for (int i = 0; i < nStartPos.length; i++) {
				// output.writeLong(nPos[i]);
				output.writeLong(fileSplitterFetch[i].nStartPos);
				output.writeLong(fileSplitterFetch[i].nEndPos);
			}
			output.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
 
	// 读取保存的下载信息(文件指针位置)
	private void read_nPos() {
		try {
			DataInputStream input = new DataInputStream(new FileInputStream(tmpFile));
			int nCount = input.readInt();
			nStartPos = new long[nCount];
			nEndPos = new long[nCount];
			for (int i = 0; i < nStartPos.length; i++) {
				nStartPos[i] = input.readLong();
				nEndPos[i] = input.readLong();
			}
			input.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
 
	private void processErrorCode(int nErrorCode) {
		System.err.println("Error Code : " + nErrorCode);
	}
 
	// 停止文件下载
	public void siteStop() {
		bStop = true;
		for (int i = 0; i < nStartPos.length; i++)
			fileSplitterFetch[i].splitterStop();
	}
 
}


FileSplitterFetch.java

package com.bocom.sb.http.file;
 
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
 
/**
 * 
 * 负责部分文件的抓取
 * @author xiaoming
 * @since 2017-8-29
 */
public class FileSplitterFetch extends Thread {
	// File URL
	String sURL;
	// File Snippet Start Position
	long nStartPos;
	// File Snippet End Position
	long nEndPos;
	// Thread's ID
	int nThreadID;
	// Downing is over
	boolean bDownOver = false;
	// Stop identical
	boolean bStop = false;
	// File Access interface
	FileAccess fileAccessI = null;
 
	public FileSplitterFetch(String sURL, String sName, long nStart, long nEnd, int id) throws IOException {
		this.sURL = sURL;
		this.nStartPos = nStart;
		this.nEndPos = nEnd;
		nThreadID = id;
		fileAccessI = new FileAccess(sName, nStartPos);
	}
 
	public void run() {
		while (nStartPos < nEndPos && !bStop) {
			try {
				URL url = new URL(sURL);
				HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
				httpConnection.setRequestProperty("User-Agent", "NetFox");
				String sProperty = "bytes=" + nStartPos + "-";
				httpConnection.setRequestProperty("RANGE", sProperty);
				Utility.log(sProperty);
				InputStream input = httpConnection.getInputStream();
				// logResponseHead(httpConnection);
				byte[] b = new byte[1024];
				int nRead;
				while ((nRead = input.read(b, 0, 1024)) > 0 && nStartPos < nEndPos && !bStop) {
					nStartPos += fileAccessI.write(b, 0, nRead);
					// if(nThreadID == 1)
					// Utility.log("nStartPos = " + nStartPos + ", nEndPos = " +
					// nEndPos);
				}
				Utility.log("Thread " + nThreadID + " is over!");
				bDownOver = true;
				// nPos = fileAccessI.write (b,0,nRead);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
 
	// 打印回应的头信息
	public void logResponseHead(HttpURLConnection con) {
		for (int i = 1;; i++) {
			String header = con.getHeaderFieldKey(i);
			if (header != null)
				// responseHeaders.put(header,httpConnection.getHeaderField(header));
				Utility.log(header + " : " + con.getHeaderField(header));
			else
				break;
		}
	}
 
	public void splitterStop() {
		bStop = true;
	}
 
}


FileAccess.java

package com.bocom.sb.http.file;
 
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
 
/**
 * 
 * 负责文件的存储
 * @author xiaoming
 * @since 2017-8-29
 * 
 */
public class FileAccess implements Serializable {
	private static final long serialVersionUID = -6335788938054788024L;
 
	RandomAccessFile oSavedFile;
 
	long nPos;
 
	public FileAccess() throws IOException {
		this("", 0);
	}
 
	public FileAccess(String sName, long nPos) throws IOException {
		oSavedFile = new RandomAccessFile(sName, "rw");
		this.nPos = nPos;
		oSavedFile.seek(nPos);
	}
 
	public synchronized int write(byte[] b, int nStart, int nLen) {
		int n = -1;
		try {
			oSavedFile.write(b, nStart, nLen);
			n = nLen;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return n;
	}
 
}


SiteInfoBean.java

package com.bocom.sb.http.file;
 
/**
 * 
 * 要抓取的文件的信息,如文件保存的目录,名字,抓取文件的URL等
 * @author xiaoming
 * @since 2017-8-29
 */  
public class SiteInfoBean  
{  
    // Site's URL  
    private String sSiteURL;   
    // Saved File's Path  
    private String sFilePath;  
    // Saved File's Name  
    private String sFileName;   
    // Count of Splited Downloading File  
    private int nSplitter;   
  
    public SiteInfoBean()  
    {  
        // default value of nSplitter is 5  
        this("", "", "", 5);  
    }  
  
    public SiteInfoBean(String sURL, String sPath, String sName, int nSpiltter)  
    {  
        sSiteURL = sURL;  
        sFilePath = sPath;  
        sFileName = sName;  
        this.nSplitter = nSpiltter;  
    }  
  
    public String getSSiteURL()  
    {  
        return sSiteURL;  
    }  
  
    public void setSSiteURL(String value)  
    {  
        sSiteURL = value;  
    }  
  
    public String getSFilePath()  
    {  
        return sFilePath;  
    }  
  
    public void setSFilePath(String value)  
    {  
        sFilePath = value;  
    }  
  
    public String getSFileName()  
    {  
        return sFileName;  
    }  
  
    public void setSFileName(String value)  
    {  
        sFileName = value;  
    }  
  
    public int getNSplitter()  
    {  
        return nSplitter;  
    }  
  
    public void setNSplitter(int nCount)  
    {  
        nSplitter = nCount;  
    }  
  
}  


Utility.java

package com.bocom.sb.http.file;
 
/**
 * 
 * 工具类,日志处理,线程等待
 * @author xiaoming
 * @since 2017-8-29
 */
public class Utility {
	public Utility() {
	}
 
	public static void sleep(int nSecond) {
		try {
			Thread.sleep(nSecond);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
 
	public static void log(String sMsg) {
		System.err.println(sMsg);
	}
 
	public static void log(int sMsg) {
		System.err.println(sMsg);
	}
 
}


TestMethod.java

package com.bocom.sb.http.file;
 
/**
 * 
 * 测试类
 * @since 2017-8-29
 * @author xiaoming
 *
 */
public class TestMethod {
	public TestMethod() {
		try {
			//从http://localhost:8080/abc.exe取文件,存到D:\\temp,命名为abc.exe,开启5个线程
			SiteInfoBean bean = new SiteInfoBean("http://localhost:8080/file/abc.exe", "D:\\temp",
					"abc.exe", 5);
			SiteFileFetch fileFetch = new SiteFileFetch(bean);
			fileFetch.start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}


Spring boot 设置一个对外接口,调用断点续传,或在TestMethod下直接写个main方法进行测试。

@RequestMapping("download")
public void download(){
	new TestMethod();
}

abc.exe存放在 src/main/resources/static/file/ 下面。


访问方式:

http://127.0.0.1:8080/spring/download

 

问题:

项目启动后,下载时报错:Java sockets - java.net.ConnectException: Connection refused: connect

在Stack Overflow上搜到一个非常精简有用的答案:

Have a look at the answers to: java.net.ConnectException: Connection refused
My first suspicion however would be a firewall issue.....

解决方法:关了你的防火墙...


到此,断点续传已实现完成,在测试时准备一个大一些的文件,在还没有下载完成的时候,停掉服务器,这时会发现文件仅下载了一部分,然后启动服务器,再次下载,程序会从上次中断的位置继续下载,等待下载完成,打开abc.exe,能够正常打开,传输成功!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值