美文网首页
我写了一个断点续传下载的flutter pub库

我写了一个断点续传下载的flutter pub库

作者: 野狗道人闯红灯 | 来源:发表于2020-09-18 23:19 被阅读0次

    背景

    一个大文件如果不支持断点续传,那么当下载过程中被打断了,下次还需要从头开始下载是一件很令人头疼的事情。那么这个断点续传的库就应运而生了。

    在flutter pub.dev库上搜索了一番发现以前没有这样的库,那么这件事就由我来做好了。

    原理

    众所周知,http1.1版本中header添加了range头,对于同一个文件可以支持分段请求,每次请求其中一部分资源。

    利用这个原理做两件事情。

    • 分段请求:将一个文件分成多块使用多线程去分别请求,最后再合并起来。
    • 断点续传:对于的大文件下载过程记住当前下载到了文件的某一个位置,如果下载被打断了,还可以在之前下载完的位置继续请求。

    断点续传库则根据这个原理支持一下分段请求和断点续传。具体来说是先支持分段请求,然后在分段请求的基础上支持一下断点续传。

    附加思考:多线程分段下载真的可以提高下载速度吗?

    不一定。

    1. 如果只有一个数据源则不能,流量出口速度必然是恒定的。
    2. 如果有多个数据源理论上可以提高下载速度。
    3. 如果我们设备的带宽低于数据源的带宽,则可能会受限于设备带宽。
    4. 如果多个数据源的带宽差距较大,多线程下载速度也不一定会优于单线程下载。

    综上、具体的下载速度会受限于数据源数量、数据源带宽、设备带宽、每个块的大小、分块的数量等。

    使用

    使用起来很简单,只需要传入下载地址,保存地址就可以了。可选参数有分块数量、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。

    参考文章

    相关文章

      网友评论

          本文标题:我写了一个断点续传下载的flutter pub库

          本文链接:https://www.haomeiwen.com/subject/rvvsyktx.html