dio 断点续传的问题

断点续传需要在请求头里面传上标志,服务器要支持才能实现断点续传.

dio中的download,在使用过程中,并不能很好地支持.从源码实现上看是有问题的.

headers: {
  "range": "bytes=$processed-",
},

这是传的参数 processed是已经下载过的字节,-后面可以跟上总的. 也可以不跟.

关于网传的两个版本:

await DioClient().dio.download(
              url,
              file.path,
              cancelToken: cancelToken,
              onReceiveProgress: onReceiveProgress,
              options: Options(
                headers: {"range": "bytes=$processed-"}, //指定请求的内容区间
              ),
            );

这段代码的问题:

不能断点,因为file使用的是write,可以取消,只有下载完了才能看到文件,如果失败了,会删除已下载的.

这在源码中可以看到实现.

另一段:

try {
  var response = await DioClient().dio.get(
        cancelToken: cancelToken,
        url,
        options: Options(
          responseType: ResponseType.stream,
          followRedirects: false,
          headers: {
            "range": "bytes=$processed-",
          },
          //"start": downloadStart,
        ),
      );
  RandomAccessFile raf = file.openSync(mode: FileMode.append);
  Stream stream = response.data!.stream;
  StreamSubscription? subscription;
  subscription = stream.listen(
    (data) {
      /// 写入文件必须同步
      raf.writeFromSync(data);
      processed += data.length;
      onReceiveProgress?.call(processed, total);
    },
    onDone: () async {
      await raf.close();
    },
    onError: (e) async {
      Log.i("download.onError.retry:$retry");
      await raf.close();
      retry++;
      if (retry > 2) {
        showToast("下载出错");
        return false;
      }
    },
    cancelOnError: true,
  );
  cancelToken?.whenCancel.then((_) async {
    Log.i("download.cancelToken:");
    await subscription?.cancel();
    await raf.close();
  });
} on DioException catch (error) {
  /// 请求已发出,服务器用状态代码响应它不在200的范围内
  Log.i("download.DioException:$retry, error:$error");
  if (CancelToken.isCancel(error)) {
    return false;
  } else {
    retry++;
    if (retry > 2) {
      showToast("下载出错");
      return false;
    }
  }
}

这段代码,在我这边是取消不了.文件是有的.

打开dio的download代码一看,就是第一段的默认实现方式,只要修改两处,就可以实现断点续传与取消了.

要注意的是

int retry = 0;
    do {
      int processed = 0;
      if (await file.exists()) {
        processed = file.lengthSync();
      }
      url = "$url?start=$processed";
      Log.i("download.start:$processed, total:$total, :$url");
      try {
        Response res = await Net.dioDownload(
          url,
          file,
          onReceiveProgress: onReceiveProgress,
          cancelToken: cancelToken,
        );
        Log.i("download.processed:$processed, res:$res");
        return res.statusCode == 200 || res.statusCode == 206;
      } catch (e) {
        Log.i("download.error:$processed, e:$e");
        if (e is DioException) {
          if (CancelToken.isCancel(e)) {
            Log.i("download.error:isCancel");
            return false;
          }
        }
        retry++;
        if (retry > 2) {
          showToast("下载出错");
          return false;
        }
      }
    } while (retry <= 2);

这个res是一个responsebody对象,里面的code=200,是非断点.=206,是断点续传.判断这两个值可以得到结果是成功了还是失败了.

对于取消,它会抛出异常来,可以捕获时得到是取消的原因.这里设置了重试,避免一些网络抖动导致的下载失败.canceltoken由外部传入就可以了,要取消的时候调用cancel就能取消了.

///这是dio包里面取的方法,原方法如果下载失败,默认删除文件.默认是从头下载的.
  ///所以网上的方法在下面,两个都无法断点,也可能是因为我们的接口不一样
  ///file参数,必须,外部传入
  ///使用示例可以看FileViewModel._doDownload2()
  static Future dioDownload(
    String urlPath,
    File file, {
    ProgressCallback? onReceiveProgress,
    Map? queryParameters,
    CancelToken? cancelToken,
    bool deleteOnError = true,
    String lengthHeader = Headers.contentLengthHeader,
    Object? data,
    Options? options,
  }) async {
    options ??= DioMixin.checkOptions('GET', options);

    // Receive data with stream.
    options.responseType = ResponseType.stream;
    Response response;
    try {
      response = await DioClient().dio.request(
            urlPath,
            data: data,
            options: options,
            queryParameters: queryParameters,
            cancelToken: cancelToken ?? CancelToken(),
          );
    } on DioException catch (e) {
      if (e.type == DioExceptionType.badResponse) {
        if (e.response!.requestOptions.receiveDataWhenStatusError == true) {
          e.response!.data as ResponseBody;
        } else {
          e.response!.data = null;
        }
      }
      rethrow;
    }

    RandomAccessFile raf = file.openSync(mode: FileMode.append);

    final completer = Completer();
    Future future = completer.future;
    int received = 0;

    // Stream
    final stream = response.data!.stream;
    bool compressed = false;
    int total = 0;
    final contentEncoding = response.headers.value(
      Headers.contentEncodingHeader,
    );
    if (contentEncoding != null) {
      compressed = ['gzip', 'deflate', 'compress'].contains(contentEncoding);
    }
    if (lengthHeader == Headers.contentLengthHeader && compressed) {
      total = -1;
    } else {
      total = int.parse(response.headers.value(lengthHeader) ?? '-1');
    }

    Future? asyncWrite;
    bool closed = false;
    Future closeAndDelete() async {
      if (!closed) {
        closed = true;
        await asyncWrite;
        await raf.close().catchError((_) => raf);
        /*if (deleteOnError && file.existsSync()) {
          await file.delete().catchError((_) => file);
        }*/
      }
    }

    late StreamSubscription subscription;
    subscription = stream.listen(
      (data) {
        subscription.pause();
        // Write file asynchronously
        asyncWrite = raf.writeFrom(data).then((result) {
          // Notify progress
          received += data.length;
          onReceiveProgress?.call(received, total);
          raf = result;
          if (cancelToken == null || !cancelToken.isCancelled) {
            subscription.resume();
          }
        }).catchError((Object e) async {
          try {
            await subscription.cancel();
            closed = true;
            await raf.close().catchError((_) => raf);
            if (deleteOnError && file.existsSync()) {
              await file.delete().catchError((_) => file);
            }
          } finally {
            completer.completeError(
              DioMixin.assureDioException(e, response.requestOptions),
            );
          }
        });
      },
      onDone: () async {
        try {
          await asyncWrite;
          closed = true;
          await raf.close().catchError((_) => raf);
          completer.complete(response);
        } catch (e) {
          completer.completeError(
            DioMixin.assureDioException(e, response.requestOptions),
          );
        }
      },
      onError: (e) async {
        try {
          await closeAndDelete();
        } finally {
          completer.completeError(
            DioMixin.assureDioException(e, response.requestOptions),
          );
        }
      },
      cancelOnError: true,
    );
    cancelToken?.whenCancel.then((_) async {
      await subscription.cancel();
      await closeAndDelete();
    });

    final timeout = response.requestOptions.receiveTimeout;
    if (timeout != null) {
      future = future.timeout(timeout).catchError(
        (dynamic e, StackTrace s) async {
          await subscription.cancel();
          await closeAndDelete();
          if (e is TimeoutException) {
            throw DioException.receiveTimeout(
              timeout: timeout,
              requestOptions: response.requestOptions,
              error: e,
            );
          } else {
            throw e;
          }
        },
      );
    }
    return DioMixin.listenCancelForAsyncTask(cancelToken, future);
  }

关键点就是closeAndDelete,这个方法意思就是关闭流且删除未下载完的文件.我把删除这段注释了,需要外部保证下载的字节.file的写入方法从write改为append.

由于我上传的服务器支持的方式不是header传参数 ,是在url后面处理,原理是一样的.

这是从dio源码中找到的下载方法,去除了删除文件,修改write为append,也就是说,原来的方法是可以从头开始下载,但中间不能停,停了会失败.但现在文件下载了多少,需要自己去保证.否则下载的文件是从现有的追加,有可能导致文件不正确.

可以看到,与版本2不同的是

raf.writeFrom(data).然后下面的cancel才有效.我不知道别人用了上面两个版本的能不能真正实现断点续传,没有实践过就到处转发是个毛病.污染太严重了.

你可能感兴趣的:(flutter应用,flutter)