網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
協(xié)議梳理
一般情況下,下載的功能模塊,至少需要提供如下基礎(chǔ)功能:資源下載、取消當(dāng)前下載、資源是否下載成功、資源文件的大小、清除緩存文件。而斷點(diǎn)續(xù)傳主要體現(xiàn)在取消當(dāng)前下載后,再次下載時(shí)能在之前已下載的基礎(chǔ)上繼續(xù)下載。這個(gè)能極大程度的減少我們服務(wù)器的帶寬損耗,而且還能為用戶(hù)減少流量,避免重復(fù)下載,提高用戶(hù)體驗(yàn)。
前置條件:資源必須支持?jǐn)帱c(diǎn)續(xù)傳。如何確定可否支持?看看你的服務(wù)器是否支持Range請(qǐng)求即可。
實(shí)現(xiàn)步驟
1.定好協(xié)議。我們用的http庫(kù)是dio;通過(guò)校驗(yàn)md5
檢測(cè)文件緩存完整性;關(guān)于代碼中的subDir,設(shè)計(jì)上認(rèn)為資源會(huì)有多種:音頻、視頻、安裝包等,每種資源分開(kāi)目錄進(jìn)行存儲(chǔ)。
import 'package:dio/dio.dart'; typedef ProgressCallBack = void Function(int count, int total); typedef CancelTokenProvider = void Function(CancelToken cancelToken); abstract class AssetRepositoryProtocol { /// 下載單一資源 Future<String> downloadAsset(String url, {String? subDir, ProgressCallBack? onReceiveProgress, CancelTokenProvider? cancelTokenProvider, Function(String)? done, Function(Exception)? failed}); /// 取消下載,Dio中通過(guò)CancelToken可控制 void cancelDownload(CancelToken cancelToken); /// 獲取文件的緩存地址 Future<String?> filePathForAsset(String url, {String? subDir}); /// 檢查文件是否緩存成功,簡(jiǎn)單對(duì)比md5 Future<String?> checkCachedSuccess(String url, {String? md5Str}); /// 查看緩存文件的大小 Future<int> cachedFileSize({String? subDir}); /// 清除緩存 Future<void> clearCache({String? subDir}); }
2.實(shí)現(xiàn)抽象協(xié)議,其中HttpManagerProtocol內(nèi)部封裝了dio的相關(guān)請(qǐng)求。
class AssetRepository implements AssetRepositoryProtocol { AssetRepository(this.httpManager); final HttpManagerProtocol httpManager; @override Future<String> downloadAsset(String url, {String? subDir, ProgressCallBack? onReceiveProgress, CancelTokenProvider? cancelTokenProvider, Function(String)? done, Function(Exception)? failed}) async { CancelToken cancelToken = CancelToken(); if (cancelTokenProvider != null) { cancelTokenProvider(cancelToken); } final savePath = await _getSavePath(url, subDir: subDir); try { httpManager.downloadFile( url: url, savePath: savePath + '.temp', onReceiveProgress: onReceiveProgress, cancelToken: cancelToken, done: () { done?.call(savePath); }, failed: (e) { print(e); failed?.call(e); }); return savePath; } catch (e) { print(e); rethrow; } } @override void cancelDownload(CancelToken cancelToken) { try { if (!cancelToken.isCancelled) { cancelToken.cancel(); } } catch (e) { print(e); } } @override Future<String?> filePathForAsset(String url, {String? subDir}) async { final path = await _getSavePath(url, subDir: subDir); final file = File(path); if (!(await file.exists())) { return null; } return path; } @override Future<String?> checkCachedSuccess(String url, {String? md5Str}) async { String? path = await _getSavePath(url, subDir: FileType.video.dirName); bool isCached = await File(path).exists(); if (isCached && (md5Str != null && md5Str.isNotEmpty)) { // 存在但是md5驗(yàn)證不通過(guò) File(path).readAsBytes().then((Uint8List str) { if (md5.convert(str).toString() != md5Str) { path = null; } }); } else if (isCached) { return path; } else { path = null; } return path; } @override Future<int> cachedFileSize({String? subDir}) async { final dir = await _getDir(subDir: subDir); if (!(await dir.exists())) { return 0; } int totalSize = 0; await for (var entity in dir.list(recursive: true)) { if (entity is File) { try { totalSize += await entity.length(); } catch (e) { print('Get size of $entity failed with exception: $e'); } } } return totalSize; } @override Future<void> clearCache({String? subDir}) async { final dir = await _getDir(subDir: subDir); if (!(await dir.exists())) { return; } dir.deleteSync(recursive: true); } Future<String> _getSavePath(String url, {String? subDir}) async { final saveDir = await _getDir(subDir: subDir); if (!saveDir.existsSync()) { saveDir.createSync(recursive: true); } final uri = Uri.parse(url); final fileName = uri.pathSegments.last; return saveDir.path + fileName; } Future<Directory> _getDir({String? subDir}) async { final cacheDir = await getTemporaryDirectory(); late final Directory saveDir; if (subDir == null) { saveDir = cacheDir; } else { saveDir = Directory(cacheDir.path + '/$subDir/'); } return saveDir; } }
3.封裝dio下載,實(shí)現(xiàn)資源斷點(diǎn)續(xù)傳。
這里的邏輯比較重點(diǎn),首先未緩存100%的文件,我們以.temp后綴進(jìn)行命名,在每次下載時(shí)檢測(cè)下是否有.temp的文件,拿到其文件字節(jié)大??;傳入在header中的range字段,服務(wù)器就會(huì)去解析需要從哪個(gè)位置繼續(xù)下載;下載全部完成后,再把文件名改回正確的后綴即可。
final downloadDio = Dio(); Future<void> downloadFile({ required String url, required String savePath, required CancelToken cancelToken, ProgressCallback? onReceiveProgress, void Function()? done, void Function(Exception)? failed, }) async { int downloadStart = 0; File f = File(savePath); if (await f.exists()) { // 文件存在時(shí)拿到已下載的字節(jié)數(shù) downloadStart = f.lengthSync(); } print("start: $downloadStart"); try { var response = await downloadDio.get<ResponseBody>( url, options: Options( /// Receive response data as a stream responseType: ResponseType.stream, followRedirects: false, headers: { /// 加入range請(qǐng)求頭,實(shí)現(xiàn)斷點(diǎn)續(xù)傳 "range": "bytes=$downloadStart-", }, ), ); File file = File(savePath); RandomAccessFile raf = file.openSync(mode: FileMode.append); int received = downloadStart; int total = await _getContentLength(response); Stream<Uint8List> stream = response.data!.stream; StreamSubscription<Uint8List>? subscription; subscription = stream.listen( (data) { /// Write files must be synchronized raf.writeFromSync(data); received += data.length; onReceiveProgress?.call(received, total); }, onDone: () async { file.rename(savePath.replaceAll('.temp', '')); await raf.close(); done?.call(); }, onError: (e) async { await raf.close(); failed?.call(e); }, cancelOnError: true, ); cancelToken.whenCancel.then((_) async { await subscription?.cancel(); await raf.close(); }); } on DioError catch (error) { if (CancelToken.isCancel(error)) { print("Download cancelled"); } else { failed?.call(error); } } }
寫(xiě)在最后
這篇文章確實(shí)沒(méi)有技術(shù)含量,水一篇,但其實(shí)是實(shí)用的。這個(gè)斷點(diǎn)續(xù)傳的實(shí)現(xiàn)有幾個(gè)注意的點(diǎn):
- 使用文件操作的方式,區(qū)分后綴名來(lái)管理緩存的資源;
- 安全性使用md5校驗(yàn),這點(diǎn)非常重要,斷點(diǎn)續(xù)傳下載的文件,在完整性上可能會(huì)因?yàn)楦鞣N突發(fā)情況而得不到保障;
- 在資源管理協(xié)議上,我們將下載、檢測(cè)、獲取大小等方法都抽象出去,在業(yè)務(wù)調(diào)用時(shí)比較靈活。
原文鏈接:https://juejin.cn/post/7123965772132515877
相關(guān)推薦
- 2022-12-03 PostgreSQL?數(shù)組類(lèi)型操作使用及特點(diǎn)詳解_PostgreSQL
- 2023-03-01 Android使用AndroidUtilCode實(shí)現(xiàn)多語(yǔ)言_Android
- 2023-06-03 Android廣播機(jī)制原理與開(kāi)發(fā)_Android
- 2022-09-09 使用?React?Hooks?重構(gòu)類(lèi)組件的示例詳解_React
- 2022-10-06 C++?pimpl機(jī)制詳細(xì)講解_C 語(yǔ)言
- 2022-05-31 python中三種輸出格式總結(jié)(%,format,f-string)_python
- 2022-05-28 python非單一.py文件用Pyinstaller打包發(fā)布成exe_python
- 2022-05-04 解決WCF不能直接序列化SqlParameter類(lèi)型的問(wèn)題_C#教程
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支