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

背景

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

在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。

参考文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342