news 2026/8/31 18:16:24

HarmonyOS 大文件断点续传:Range、流式落盘与完整性校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS 大文件断点续传:Range、流式落盘与完整性校验

HarmonyOS 大文件断点续传:Range、流式落盘与完整性校验

大文件下载不能简单地把普通HTTP请求换成更长超时。一次把响应读进内存会抬高峰值;网络中断后从头开始浪费流量;已有半个文件时若服务端忽略Range并返回200,继续追加会直接生成损坏文件;下载完成后若不校验,用户只能在打开时才发现内容不可用。

HarmonyOS的requestInStream适合流式响应。本文设计一个可恢复协议:临时文件记录已写长度与ETag,请求使用RangeIf-Range,只有206才允许续写;200代表资源已变化或服务端不支持续传,必须清空临时文件重新开始;最终通过长度与摘要后再原子替换正式文件。

1. 续传依赖服务端HTTP语义

客户端发出Range: bytes=1048576-后,服务端若接受范围请求,应返回206和Content-Range;若返回200,响应体通常是完整文件,不能追加到已有1MB后面。

GET /packages/map.bin HTTP/1.1 Range: bytes=1048576- If-Range: "etag-v7" HTTP/1.1 206 Partial Content Content-Range: bytes 1048576-5242879/5242880 ETag: "etag-v7"

服务端返回416时,可能是本地偏移超过远端长度,也可能文件已完整。先读取Content-Range: bytes */total再决定校验或清空,不能无条件认定成功。

2. 临时文件与元数据必须成对存在

正式路径旁保存.part文件,另存一份小型元数据。元数据至少包含URL、已写长度、ETag、期望总长度和摘要算法。应用重启后先核对实际文件长度,不能只相信元数据。

interfaceResumeMeta{url:string;downloadedBytes:number;totalBytes?:number;etag?:string;sha256?:string;updatedAt:number;}functionnormalizeMeta(meta:ResumeMeta,actualSize:number):ResumeMeta{return{...meta,downloadedBytes:Math.max(0,Math.min(meta.downloadedBytes,actualSize)),updatedAt:Date.now()};}

若元数据URL与当前任务不一致,应创建新任务,不能复用旧临时文件。

3. 大响应使用requestInStream

官方FAQ建议普通request用于不超过5MB的响应,更大内容使用requestInStream。流式接口通过dataReceive连续提供ArrayBuffer,应用边收边写,不保存完整响应。

import{http}from'@kit.NetworkKit';functionbuildHeaders(meta:ResumeMeta):Record<string,string>{constheaders:Record<string,string>={};if(meta.downloadedBytes>0){headers['Range']=`bytes=${meta.downloadedBytes}-`;if(meta.etag){headers['If-Range']=meta.etag;}}returnheaders;}

If-Range很重要:资源未变化时继续返回剩余范围;资源变化时服务端可返回完整新文件,客户端据200状态重启下载。

4. 断点下载闭环先判断状态码

读取断点只是第一步。收到状态码后才能决定追加还是清空;数据进入临时文件;结束后校验长度与摘要;全部通过才替换正式文件。任何错误都保留一致的临时状态供下次恢复。

5. 文件写入器只负责顺序落盘

下面使用应用沙箱路径打开临时文件。续传时写入位置从当前文件长度开始;重新下载时先截断为0。

import{fileIoasfs}from'@kit.CoreFileKit';classPartFileWriter{privatefile:fs.File;privateoffset:number;constructor(path:string,offset:number){this.file=fs.openSync(path,fs.OpenMode.CREATE|fs.OpenMode.READ_WRITE);this.offset=offset;}reset():void{fs.truncateSync(this.file.fd,0);this.offset=0;}append(buffer:ArrayBuffer):number{constwritten=fs.writeSync(this.file.fd,buffer,{offset:0,length:buffer.byteLength,position:this.offset});this.offset+=written;returnwritten;}getlength():number{returnthis.offset;}close():void{fs.closeSync(this.file);}}

不同SDK版本的文件导入入口可能不同,请以项目SDK声明为准。核心约束是检查实际写入字节数;若少于buffer.byteLength,应继续写剩余部分或终止任务,不能直接增加完整长度。

6. 状态码必须在写入前确定

requestInStream返回HTTP响应码。事件回调可能很快到达,因此实现中应在业务层串行化首个数据块,确保状态码规则已经确定。下面突出206与200的分支。

asyncfunctiondecideWriteMode(status:number,localBytes:number):Promise<'append'|'reset'>{if(localBytes===0&&(status===200||status===206)){return'append';}if(localBytes>0&&status===206){return'append';}if(localBytes>0&&status===200){return'reset';}thrownewError(`unexpected download status:${status}`);}

若已有部分文件却收到200,最安全的实现是销毁当前请求、截断文件后重新发起一个不带Range的新请求,而不是在同一事件流中边重置边接收,避免前几个数据块时序不清。

7. 下载器要固定回调引用并统一销毁

classStreamDownloadSession{privaterequest:http.HttpRequest=http.createHttp();privatereceivedBytes:number=0;constructor(privatewriter:PartFileWriter){}privatereadonlyonData=(chunk:ArrayBuffer):void=>{constwritten=this.writer.append(chunk);if(written!==chunk.byteLength){thrownewError('partial file write');}this.receivedBytes+=written;};asyncstart(url:string,headers:Record<string,string>):Promise<number>{this.request.on('dataReceive',this.onData);returnthis.request.requestInStream(url,{method:http.RequestMethod.GET,header:headers,connectTimeout:15000,readTimeout:60000});}close():void{this.request.off('dataReceive',this.onData);this.request.destroy();this.writer.close();}}

回调中抛错不应成为唯一取消方式。完整工程要把写入失败传给会话状态机,主动销毁请求并落盘最新元数据。

8. 续传责任边界避免正式文件半成品

下载任务负责重试与状态;HTTP层只提供响应码、头与数据流;临时文件允许中断;正式文件只接收校验通过的完整结果。播放器、解压器或地图引擎只能读取正式路径,不能窥探.part文件。

typeDownloadState=|'idle'|'requesting'|'streaming'|'verifying'|'completed'|'paused'|'failed';interfaceDownloadSnapshot{state:DownloadState;receivedBytes:number;totalBytes?:number;failureReason?:string;}

状态迁移应单向且可记录,例如streaming -> verifying -> completed。失败后回到paused还是failed,取决于错误是否可重试。

9. Content-Range必须与本地偏移吻合

续传响应为206时,解析起始位置并与本地长度比较。服务端若从其他位置返回,继续写入会产生缝隙或重叠。

interfaceContentRange{start:number;end:number;total:number;}functionparseContentRange(value:string):ContentRange|undefined{constmatch=/^bytes (\d+)-(\d+)\/(\d+)$/.exec(value.trim());if(!match)returnundefined;conststart=Number(match[1]);constend=Number(match[2]);consttotal=Number(match[3]);if(start<0||end<start||total<=end)returnundefined;return{start,end,total};}

头字段名大小写不应硬编码到单一形式,读取响应头时先做统一归一化。

10. 完整性至少校验长度,重要文件再校验摘要

下载结束先比较临时文件实际长度与Content-LengthContent-Range总长度。安装包、离线地图和模型文件还应使用服务端提供的SHA-256。摘要必须来自可信元数据,不能来自同一不受信下载通道的可修改字段。

functionverifyLength(actual:number,expected?:number):void{if(expected===undefined||expected<0){thrownewError('missing expected file length');}if(actual!==expected){thrownewError(`file length mismatch:${actual}/${expected}`);}}

摘要计算也要流式读取文件,不能为了校验再次把大文件全部载入内存。

11. 校验成功后再替换正式路径

临时文件与正式文件最好位于同一文件系统,校验通过后使用rename替换。若目标已存在,可先保存旧版本或按产品策略删除,任何时候都不要让正式路径指向半成品。

asyncfunctioncommitPartFile(partPath:string,finalPath:string):Promise<void>{conststat=fs.statSync(partPath);if(stat.size<=0){thrownewError('empty part file');}awaitfs.rename(partPath,finalPath);}

元数据清理放在rename成功之后。应用崩溃在rename之前,仍可续传;崩溃在rename之后,下次通过正式文件校验恢复完成状态。

12. 重试策略按错误类别区分

网络断开、超时 -> 保留断点,退避后续传 HTTP 401/403 -> 刷新授权,不自动高频重试 HTTP 404 -> 任务失败,提示资源不存在 已有断点却返回200 -> 清空临时文件,重新完整下载 Content-Range不一致 -> 停止追加,重新建立任务 磁盘空间不足 -> 保留可解释状态,用户处理后再试 摘要不一致 -> 删除不可信临时文件,从头下载

13. 断点续传验收清单

[ ] 大于5MB响应使用requestInStream [ ] 本地偏移来自真实临时文件长度 [ ] 续传只接受起点一致的206响应 [ ] 200响应不会追加到旧临时文件 [ ] 数据块写入后核对实际字节数 [ ] 退出时解除事件并destroy HttpRequest [ ] 完成前校验总长度与可信摘要 [ ] 校验通过后才替换正式文件 [ ] 杀进程重启后可从一致断点恢复

14. 流式下载资料索引

  • Network Kit大文件请求FAQ
  • HttpRequest.requestInStreamdataReceive与文件接口:以本机HarmonyOS SDK API 23声明为准。

断点续传的难点不在Range字符串,而在每个边界都要可证明:响应码证明能否追加,Content-Range证明起点一致,临时文件证明中断可恢复,长度与摘要证明内容完整,最终rename证明正式路径只暴露完成结果。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 18:16:00

FPGA驱动OV5640摄像头实时显示:从I2C配置到VGA/LCD驱动的全流程解析

简介&#xff1a;本资源是一套完整的FPGA图像采集与显示系统工程&#xff0c;面向数字电路与嵌入式视觉方向的学习者与开发者&#xff0c;解决OV5640摄像头数据实时采集、缓存与VGA/LCD双路显示的核心问题。工程基于Cyclone IV E系列EP4CE6F17C8芯片&#xff0c;使用Quartus 17…

作者头像 李华
网站建设 2026/8/31 18:15:49

SOT-23 P沟道MOS管选型全攻略:参数详解与热门料号对比

经常有调试电路板的同学问我&#xff1a;“想用 P 沟道 MOS 管做电源开关&#xff0c;SOT-23 封装到底选哪颗料&#xff1f;”这个问题听起来很简单&#xff0c;但真正上手时才发现坑很多&#xff1a;同是 SOT-23 封装&#xff0c;丝印五花八门&#xff0c;适配电压不同&#x…

作者头像 李华
网站建设 2026/8/31 18:13:02

1981-2021全球逐月太阳辐射栅格数据:GIS处理与光伏农业应用

简介&#xff1a;本资源为1981–2021年全球逐月太阳辐射GIS空间分布数据集&#xff0c;面向气象、能源、地理信息及可再生能源领域研究者与高校师生&#xff0c;支撑太阳能资源评估、光伏潜力分析、气候模型验证等科研与工程应用。数据以气候平均值形式呈现&#xff0c;覆盖41年…

作者头像 李华
网站建设 2026/8/31 18:12:53

NASA CEA化学平衡计算程序实战:从火箭燃烧室到比冲估算

简介&#xff1a;NASA开发的CEA&#xff08;Chemical Equilibrium with Applications&#xff09;程序是一款面向火箭发动机研究者、燃烧工程专家及能源化工从业者的热化学计算工具&#xff0c;专用于求解高温高压环境下化学反应达到平衡时的组分浓度、温度、压力及热力学性质&…

作者头像 李华
网站建设 2026/8/31 18:11:15

图纸、表格、扫描件,这些“AI 读不懂“的文件现在也能问了

之前我们介绍了 Knowhere 的纯视觉理解路线&#xff1a;VISION-MAP&#xff0c;它能够更好地识别图像类资料&#xff0c;而且能把原始页面留在系统里&#xff0c;让 Agent 需要时直接看页&#xff0c;每个结论都能回到证据上核对。 有读者好奇&#xff1a;一份几百页的文档&am…

作者头像 李华
网站建设 2026/8/31 18:09:57

OpenHarmony分布式音乐播放器开发实战:跨设备协同与ArkTS实现

简介&#xff1a;本资源是一个面向OpenHarmony开发者与嵌入式系统学习者的分布式音乐播放器源码工程&#xff0c;聚焦轻量级设备上的音频播放、跨端UI交互与DSoftBus通信实践&#xff0c;适用于鸿蒙生态应用开发入门与进阶实训。压缩包共260个文件&#xff0c;涵盖40个GN构建脚…

作者头像 李华