背景
一个大文件如果不支持断点续传,那么当下载过程中被打断了,下次还需要从头开始下载是一件很令人头疼的事情。那么这个断点续传的库就应运而生了。
在flutter pub.dev库上搜索了一番发现以前没有这样的库,那么这件事就由我来做好了。
原理
众所周知,http1.1版本中header添加了range头,对于同一个文件可以支持分段请求,每次请求其中一部分资源。
利用这个原理做两件事情。
- 分段请求:将一个文件分成多块使用多线程去分别请求,最后再合并起来。
- 断点续传:对于的大文件下载过程记住当前下载到了文件的某一个位置,如果下载被打断了,还可以在之前下载完的位置继续请求。
断点续传库则根据这个原理支持一下分段请求和断点续传。具体来说是先支持分段请求,然后在分段请求的基础上支持一下断点续传。
附加思考:多线程分段下载真的可以提高下载速度吗?
不一定。
- 如果只有一个数据源则不能,流量出口速度必然是恒定的。
- 如果有多个数据源理论上可以提高下载速度。
- 如果我们设备的带宽低于数据源的带宽,则可能会受限于设备带宽。
- 如果多个数据源的带宽差距较大,多线程下载速度也不一定会优于单线程下载。
综上、具体的下载速度会受限于数据源数量、数据源带宽、设备带宽、每个块的大小、分块的数量等。
使用
使用起来很简单,只需要传入下载地址,保存地址就可以了。可选参数有分块数量、dio实例(用于设置特殊参数)。
import 'package:dio_range_download/dio_range_download.dart';main() async { print("hello world"); rangeDownload();}rangeDownload() async { print("start"); bool isStarted = false; var url = "http://music.163.com/song/media/outer/url?id=1357233444.mp3"; var savePath = "download_result/music.mp3"; await RangeDownload.downloadWithChunks(url, savePath, // maxChunk: 6, // dio: Dio(),//Optional parameters "dio".Convenient to customize request settings. onReceiveProgress: (received, total) { if (total != -1) { print("${(received / total * 100).floor()}%"); } });}
具体编写
目前dio这个网络库比较火,支持的功能比较完善,所以断点续传功能我是基于这个库来完成的。
下载
首先是关键的断点续传代码,主要是根据传入的开始和结束节点给到range参数,进行下载。
其中为了支持断点续传,判断了目标文件是否已存在,如果已存在则说明是上次中断了的请求已下载的部分,这里将中断了的请求文件保存下来以备下载完成之后进行合并,并修改一下要下载文件的开始位置,在原来的基础上继续下载。
当然如果你的下载过程连续断了两次,这里会先检查一下是不是不仅有上次断掉的,还有上上次断掉的记录,会将上两次断掉的先进行一次合并,再继续下载。
Future<Response> downloadChunk(url, start, end, no, {isMerge = true}) async { int initLength = 0; --end; var path = savePath + "temp$no"; File targetFile = File(path); if(await targetFile.exists() && isMerge) { print("good job start:${start} length:${File(path).lengthSync()}"); if(start + await targetFile.length() < end) { initLength = await targetFile.length(); start += initLength; var preFile = File(path + "_pre"); if(await preFile.exists()) { mergeFiles(preFile, targetFile, preFile); } else { await targetFile.rename(preFile.path); } } else { await targetFile.delete(); } } progress.add(initLength); progressInit.add(initLength); return dio.download( url, path, onReceiveProgress: createCallback(no), options: Options( headers: {"range": "bytes=$start-$end"}, ), ); }
下载进度回调
对于下载一个大文件,需要一个下载进度的回调,以便得知当前的进度状态。对于单文件下载比较简单,但是对于分段下载,需要将各个文件的进度汇总在一起。
这里借用了一个长度为分段数量的数组,每次计算最终大小的时候,将数组里面的所有进度汇总起来返回给使用者。
createCallback(no) { return (int received, rangeTotal) async { if(received >= rangeTotal) { var path = savePath + "temp${no}"; var oldPath = savePath + "temp${no}_pre"; File oldFile = File(oldPath); if(oldFile.existsSync()) { await mergeFiles(oldPath, path, path); } } progress[no] = progressInit[no] + received; if (onReceiveProgress != null && total != 0) { onReceiveProgress(progress.reduce((a, b) => a + b), total); } }; }
文件合并
在分段下载、断点续传结束的时候都需要将文件拼接起来。我们这里主要分两种情况,将多个文件按顺序拼接起来,将两个文件按顺序拼接起来,逻辑都差不多,为了方便这里给分成两段代码。
Future mergeTempFiles(chunk) async { File f = File(savePath + "temp0"); IOSink ioSink= f.openWrite(mode: FileMode.writeOnlyAppend); for (int i = 1; i < chunk; ++i) { File _f = File(savePath + "temp$i"); await ioSink.addStream(_f.openRead()); await _f.delete(); } await ioSink.close(); await f.rename(savePath); } Future mergeFiles(file1, file2, targetFile) async { File f1 = File(file1); File f2 = File(file2); IOSink ioSink= f1.openWrite(mode: FileMode.writeOnlyAppend); await ioSink.addStream(f2.openRead()); await f2.delete(); await ioSink.close(); await f1.rename(targetFile); }
整体流程
整体流程首先请求一小块内容,检测是否支持断点续传,如果支持则根据分段数量机型拆分并启动分段请求,请求结束之后进行文件合并。
Response response = await downloadChunk(url, 0, firstChunkSize, 0, isMerge: false); if (response.statusCode == 206) { print("This http protocol support range download"); total = int.parse( response.headers.value(HttpHeaders.contentRangeHeader).split("/").last); int reserved = total - int.parse(response.headers.value(HttpHeaders.contentLengthHeader)); int chunk = (reserved / firstChunkSize).ceil() + 1; if (chunk > 1) { int chunkSize = firstChunkSize; if (chunk > maxChunk + 1) { chunk = maxChunk + 1; chunkSize = (reserved / maxChunk).ceil(); } var futures = <Future>[]; for (int i = 0; i < maxChunk; ++i) { int start = firstChunkSize + i * chunkSize; int end; if(i == maxChunk - 1) { end = total; } else { end = start + chunkSize; } futures.add(downloadChunk(url, start, end, i + 1)); } await Future.wait(futures); } await mergeTempFiles(chunk); } else { print("This http protocol don't support range download"); }
代码已开源到github,并可能会不断改动,具体代码可以直接前往github:https://github.com/qiaoshouqing/dio_range_download 阅读观看,并欢迎Star。
断点续传库的地址是:https://pub.dev/packages/dio_range_download,欢迎使用,欢迎like。
如何上传到pub.dev就暂且不说了,步骤很简单,最大的困难是KXSW。