断点续传功能最核心的原理就是利用HTTP请求中的两个字段:客户端请求头中的Range,和服务端响应头的Content-Range。
我们举一个例子,模拟一下整个过程。
1、浏览器请求服务器上的一个文件时,所发出的请求如下(假设文件名为 file.zip,服务器域名为W):
GET /file.zip HTTP/1.1 //浏览器用GET方式获取file.zip文件,HTTP协议版本1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
excel, application/msword, application/vnd.ms-powerpoint //可接受的响应内容(文件)类型
Accept-Language: zh-cn //可接受的响应内容语言(简体中文)
Accept-Encoding: gzip, deflate //可接受的响应内容的编码方式
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) //浏览器的身份标识(浏览器类型)
Connection: Keep-Alive //浏览器想要优先使用的连接类型
2、服务器收到请求后,寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:
200 //响应状态码(200标识成功)
Content-Length=123456789 //响应消息的长度(单位是字节)
Accept-Ranges=bytes //服务器所支持的内容范围(字节)
Date=Mon, 30 Apr 2001 12:56:11 GMT //此消息被发送时的日期和时间
ETag=W/“02ca57e173c11:95b” //资源的标识符
Content-Type=application/octet-stream //当前内容的类型
Server=Microsoft-IIS/5.0 //服务器名称
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT //所请求的对象的最后修改日期
3、此时文件已经开始下载了,如果现在停止了下载,那么再次下载文件时就要从已经下载的地方继续下载。现在比如按下了继续下载,那么此时浏览器的请求内容如下:
GET /file.zip HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
excel, application/msword, application/vnd.ms-powerpoint
Range: bytes=200000- //告诉服务器 file.zip 这个文件从200000字节开始传,前面的字节不用传了
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Connection: Keep-Alive
4、此时服务器收到这个请求后,返回的信息如下:
206 //表示服务器已经成功处理了部分GET请求
Content-Length=123256789
Content-Range=bytes 200000-/123456789 //表示已经返回了200000B的文件数据,同时也返回了文件的全部大小
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
github地址:https://github.com/lingochamp/okdownload
com.liulishuo.okdownload:okdownload:{latest_version}
// provide sqlite to store breakpoints
com.liulishuo.okdownload:sqlite:{latest_version}
// provide okhttp to connect to backend
// and then please import okhttp dependencies by yourself
com.liulishuo.okdownload:okhttp:{latest_version}
task = new DownloadTask.Builder(url, parentFile)
.setFilename(filename)
// the minimal interval millisecond for callback progress
.setMinIntervalMillisCallbackProcess(30)
// do re-download even if the task has already been completed in the past.
.setPassIfAlreadyCompleted(false)
.build();
//异步执行任务
task.enqueue(listener);
// cancel
task.cancel();
// execute task synchronized
task.execute(listener);
单个任务的下载,设置回调,通过downloadDispatcher处理下载事务
//DownloadTask.java
public void enqueue(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().enqueue(this);
}
public void execute(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().execute(this);
}
//OkDownload.java
public class OkDownload {
//负责同步、异步、多任务的调度,执行、取消
private final DownloadDispatcher downloadDispatcher;
//下载回调(DownloadListener)
private final CallbackDispatcher callbackDispatcher;
//断点存储(内存存储,也支持本地化存储(需要引入okdownload:sqlite))
private final BreakpointStore breakpointStore;
//连接(httpUrlConnection,也支持okhttp3(需要引入okhttp3、okdownload:okhttp))
private final DownloadConnection.Factory connectionFactory;
//输出流写文件
private final DownloadOutputStream.Factory outputStreamFactory;
private final ProcessFileStrategy processFileStrategy;
private final DownloadStrategy downloadStrategy;
private final Context context;
}
负责同步、异步、多任务的调度,执行、取消
//DownloadDispatcher.java
private final List<DownloadCall> readyAsyncCalls; //等待的异步任务列表
private final List<DownloadCall> runningAsyncCalls; //运行中的异步任务列表
private final List<DownloadCall> runningSyncCalls; //运行中的同步任务列表
//执行单个异步任务
public void enqueue(DownloadTask task) {
skipProceedCallCount.incrementAndGet();
enqueueLocked(task);
skipProceedCallCount.decrementAndGet();
}
private synchronized void enqueueLocked(DownloadTask task) {
if (inspectCompleted(task)) return; //判断下载任务是否已完成,完成则回调taskEnd
if (inspectForConflict(task)) return; //判断任务是否已经在下载队列中
final int originReadyAsyncCallSize = readyAsyncCalls.size();
enqueueIgnorePriority(task);
//如果异步等待列表的数量发生了改变,根据任务优先级重新排序
if (originReadyAsyncCallSize != readyAsyncCalls.size()) Collections.sort(readyAsyncCalls);
}
private synchronized void enqueueIgnorePriority(DownloadTask task) {
//创建DownloadCall
final DownloadCall call = DownloadCall.create(task, true, store);
if (runningAsyncSize() < maxParallelRunningCount) {
//运行中的异步任务数量小于最大可并行任务数量(默认为5)
//则添加到runningAsyncCalls,线程池调度,在子线程中执行此任务
runningAsyncCalls.add(call);
getExecutorService().execute(call);
} else {
// priority
readyAsyncCalls.add(call);
}
}
每一个task对应一个DownloadCall,执行下载请求
public class DownloadCall extends NamedRunnable implements Comparable<DownloadCall> {
public static DownloadCall create(DownloadTask task, boolean asyncExecuted,
@NonNull DownloadStore store) {
return new DownloadCall(task, asyncExecuted, store);
}
public void execute() throws InterruptedException {
currentThread = Thread.currentThread();
boolean retry;
int retryCount = 0;
// ready param
final OkDownload okDownload = OkDownload.with();
final ProcessFileStrategy fileStrategy = okDownload.processFileStrategy();
// 重置store中的taskId,回调taskStart()
inspectTaskStart();
do {
// 0. check basic param before start,检查url和是否已被取消
// 1. create basic info if not exist,通过store创建或者取出断点信息(taskId,自增创建的)
// 2. remote check.校验文件大小,是否支持断点续传,是否需要重定向,设置blockInfo
final BreakpointRemoteCheck remoteCheck = createRemoteCheck(info);
remoteCheck.check();
cache.setRedirectLocation(task.getRedirectLocation());
// 3. waiting for file lock release after file path is confirmed.
// 4. reuse another info if another info is idle and available for reuse.
OkDownload.with().downloadStrategy()
.inspectAnotherSameInfo(task, info, remoteCheck.getInstanceLength());
//如果远端校验正常,校验本地,文件是否存在,blockInfo是否正确,outputstream是否支持seek
if (remoteCheck.isResumable()) {
// 5. local check
final BreakpointLocalCheck localCheck = createLocalCheck(info,
remoteCheck.getInstanceLength());
localCheck.check();
if (localCheck.isDirty()) {
// 6. assemble block data,本地校验异常,如不支持seek
fileStrategy.discardProcess(task);
assembleBlockAndCallbackFromBeginning(info, remoteCheck,
localCheck.getCauseOrThrow());
} else {
okDownload.callbackDispatcher().dispatch()
.downloadFromBreakpoint(task, info);
}
} else {
// 6. assemble block data,远端校验异常,如不支持断点续传
fileStrategy.discardProcess(task);
assembleBlockAndCallbackFromBeginning(info, remoteCheck,
remoteCheck.getCauseOrThrow());
}
// 7. start with cache and info. 真正的启动啦
start(cache, info);
if (canceled) break;
// 8. retry if precondition failed.如果是可修复异常且小于最大重试次数,则retry
} while (retry);
inspectTaskEnd(cache, cause, realCause);
}
//根据blockInfo,构建DownloadChain,责任链模式执行
void start(final DownloadCache cache, BreakpointInfo info) throws InterruptedException {
final List<DownloadChain> blockChainList = new ArrayList<>(info.getBlockCount());
startBlocks(blockChainList);
}
}
如果返回文件的一部分,则使用HTTP 206状态码;如果返回整个文件,则使用HTTP 200响应码。
Accept-Ranges:用于server到client的应答,client通过该自段判断server是否支持断点续传。
private static boolean isAcceptRange(@NonNull DownloadConnection.Connected connected)
throws IOException {
if (connected.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) return true;
final String acceptRanges = connected.getResponseHeaderField(ACCEPT_RANGES);
return "bytes".equals(acceptRanges);
}
如果我们请求的文件的URL是类似http://www.server.com/filename.exe这样的文件,则不会有问题。但是很多软件下载网站的文件下载链接都是通过程序重定向的,比如pchome的ACDSee的HTTP下载地址是:
http://download.pchome.net/php/tdownload2.php?sid=5547&url=/multimedia/viewer/acdc31sr1b051007.exe&svr=1&typ=0
这种地址并没有直接标识文件的位置,而是通过程序进行了重定向。如果向服务器请求这样的URL,服务器就会返回302(Moved Temporarily),意思就是需要重定向,同时在HTTP头中会包含一个Location字段,Location字段的值就是重定向后的目的URL。这时就需要断开当前的连接,而向这个重定向后的服务器发请求。
public class DownloadChain implements Runnable {
void start() throws IOException {
final CallbackDispatcher dispatcher = OkDownload.with().callbackDispatcher();
// connect chain
final RetryInterceptor retryInterceptor = new RetryInterceptor();
final BreakpointInterceptor breakpointInterceptor = new BreakpointInterceptor();
connectInterceptorList.add(retryInterceptor);
connectInterceptorList.add(breakpointInterceptor);
//指定Range和if-Match等相关表头信息
connectInterceptorList.add(new HeaderInterceptor());
//通过okHttpClient和RequestBuild构建请求执行,client.newCall(request).execute()
connectInterceptorList.add(new CallServerInterceptor());
connectIndex = 0;
final DownloadConnection.Connected connected = processConnect();
if (cache.isInterrupt()) {
throw InterruptException.SIGNAL;
}
dispatcher.dispatch().fetchStart(task, blockIndex, getResponseContentLength());
// fetch chain
final FetchDataInterceptor fetchDataInterceptor =
new FetchDataInterceptor(blockIndex, connected.getInputStream(),
getOutputStream(), task);
fetchInterceptorList.add(retryInterceptor);
fetchInterceptorList.add(breakpointInterceptor);
//读取字节流,写入outputstream
fetchInterceptorList.add(fetchDataInterceptor);
fetchIndex = 0;
final long totalFetchedBytes = processFetch();
dispatcher.dispatch().fetchEnd(task, blockIndex, totalFetchedBytes);
}
}
如果引入了com.liulishuo.okdownload:sqlite,则返回一个BreakpointStoreOnSQLite实例,支持本地化存储断点信息(否则BreakpointStoreOnCache只支持内存缓存断点信息)
如果只支持内存缓存断点信息,则退出app,则没了;支持sqlite,本地化数据库存储,
downloadDispatcher中的store 是RemitStoreOnSQLite实例,持有BreakpointStoreOnSQLite的引用
public static @NonNull DownloadStore createDefaultDatabase(Context context) {
// You can import through com.liulishuo.okdownload:sqlite:{version}
final String storeOnSqliteClassName
= "com.liulishuo.okdownload.core.breakpoint.BreakpointStoreOnSQLite";
try {
final Constructor constructor = Class.forName(storeOnSqliteClassName)
.getDeclaredConstructor(Context.class);
return (DownloadStore) constructor.newInstance(context);
} catch (ClassNotFoundException ignored) {
} catch (InstantiationException ignored) {
} catch (IllegalAccessException ignored) {
} catch (NoSuchMethodException ignored) {
} catch (InvocationTargetException ignored) {
}
return new BreakpointStoreOnCache(); //默认只支持内存缓存断点信息
}
public class BreakpointStoreOnSQLite implements DownloadStore {
private static final String TAG = "BreakpointStoreOnSQLite";
protected final BreakpointSQLiteHelper helper;
protected final BreakpointStoreOnCache onCache;
public BreakpointStoreOnSQLite(Context context) {
//BreakpointSQLiteHelper extends SQLiteOpenHelper,对应本地存储
//创建数据库,创建table,对应breakpoint、block、taskFileDirty
this.helper = new BreakpointSQLiteHelper(context.getApplicationContext());
//对应内存缓存处理
this.onCache = new BreakpointStoreOnCache(helper.loadToCache(),
helper.loadDirtyFileList(),
helper.loadResponseFilenameToMap());
}
public DownloadStore createRemitSelf() {
return new RemitStoreOnSQLite(this); //持有BreakpointStoreOnSQLite的引用
}
}
public class RemitStoreOnSQLite implements RemitSyncExecutor.RemitAgent, DownloadStore {
private static final String TAG = "RemitStoreOnSQLite";
@NonNull private final RemitSyncToDBHelper remitHelper;
@NonNull private final BreakpointStoreOnSQLite onSQLiteWrapper;
@NonNull private final BreakpointSQLiteHelper sqLiteHelper;
@NonNull private final DownloadStore sqliteCache;
RemitStoreOnSQLite(@NonNull BreakpointStoreOnSQLite sqlite) {
this.remitHelper = new RemitSyncToDBHelper(this);
this.onSQLiteWrapper = sqlite;
this.sqliteCache = onSQLiteWrapper.onCache;
this.sqLiteHelper = onSQLiteWrapper.helper;
}
}
request(客户端 -> 服务端):
Range:指定下载文件的某一段大小及其单位,字节偏移从0开始。
If-Match:用于匹配ETag
response(服务器 -> 客户端):
Etag:用于标识/保证文件的唯一性、完整性,每次文件有更新该值就会变化。
Content-Length:
Content-Ranges:指定了返回的文件资源的字节范围。
Transfer-Encoding:chunked,利用分块传输
Accept-Ranges:判断server是否支持断点续传。(bytes支持以bytes为单位进行传输,none不支持)
Ranges: (unit=first byte pos)-[last byte pos]
Ranges: bytes=4000- 下载从第4000字节开始到文件结束部分
Ranges: bytes=0~N 下载第0-N字节范围的内容
Ranges: bytes=M-N 下载第M-N字节范围的内容
Ranges: bytes=-N 下载最后N字节内容
remoteCheck的Range
Range:bytes=0-0
参考:
HTTP断点续传原理
http协议 文件下载原理及多线程断点续传
文件断点续传功能的原理