说明
断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度。常见的支持断点续传的上传、下载软件:QQ旋风、迅雷、快车(迷你快车)、web迅雷、影音传送带、快车、BitComet、电驴eMule、哇嘎Vagaa、RF[RaySourse/RayFile]、酷6、土豆、优酷、百度视频、新浪视频、腾讯视频、百度云等都支持断点续传。
实现方式
在下载文件的同时,创建一个info文件保存传输的文件节点,当服务器断开或者暂停下载的时候,通过info文件记录已下载的结点位置,当再次进行下载任务的时候,将info文件进行分析,告诉服务器需要继续传输的文件节点位置进行续传操作。例如如下面要求从 1526118 字节开始:
GET http://ozcte1ybf.bkt.clouddn.com/level5.mp4 HTTP/1.0
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
RANGE: bytes=1526118-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
这里多了一行 RANGE: bytes=1526118-
这一行的意思就是告诉服务器level5.mp4这个文件从 1526118 字节开始传,前面的字节不用传了。 服务器收到这个请求以后,返回的信息如下:
206
Content-Length=15261183
Content-Range=bytes 1526118-15261182/15261183
Date=Wed, 27 Dec 2017 09:17:07 GMT
ETag="A1A1A8B84E5C2F5E7A16E0886721419E"
Content-Type=application/octet-stream
Server=Tengine
Last-Modified=Sun, 22 Jan 2017 15:13:55 GMT
返回的信息增加了一行: Content-Range=bytes 1526118-15261182/15261183返回的代码也变成 206 ,而不再是 200 了。知道了这些原理,就可以进行断点续传的编程了。
注:206状态码含义:
206 Partial Content
该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。
响应必须包含如下的头部域:
Content-Range 用以指示本次响应中返回的内容的范围;如果是 Content-Type 为 multipart/byteranges 的多段下载,则每一 multipart 段中都应包含 Content-Range 域用以指示本段的内容范围。假如响应中包含 Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。
Date
ETag 和/或 Content-Location,假如同样的请求本应该返回200响应。
Expires, Cache-Control,和/或 Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。
假如本响应请求使用了 If-Range 强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了 If-Range 弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。
假如 ETag 或 Last-Modified 头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。
具体代码
主要用了 6 个类,包括一个测试类:
SiteFileFetch.java 负责整个文件的抓取,控制内部线程 (FileSplitterFetch 类 )。
FileSplitterFetch.java 负责部分文件的抓取。
FileAccess.java 负责文件的存储。
SiteInfoBean.java 要抓取的文件的信息,如文件保存的目录,名字,抓取文件的 URL 等。
Utility.java 工具类,放一些简单的方法。
TestMethod.java 测试类。
FileAccess.java
package fileHandling;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
/**
*
* 负责文件的存储
* @author yunlingfly
* @since 2018-3-11
*
*/
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