用途
该技能面向用 Kotlin 协程实现“单文件并行分块下载”的场景:把一次下载拆成 N 个字节区间请求并发执行,每个分片写入自己的临时文件,最后按索引顺序拼接为目标文件,并以 Flow 形式对外报告进度。
主要能力与流程
- 能力探测:先用
head(url) { header("Range", "bytes=0-0") }探测服务器是否支持 Range,并读取Content-Length获取文件大小;探测异常时降级。 - 分支决策:仅当支持 Range 且
fileSize > CHUNK_SIZE * 2时走并行分块下载,否则走单线程下载。 - 并行分片:在
coroutineScope内为每个分片启动独立任务,计算startByte/endByte(末片结束于fileSize - 1),请求带Range: bytes=start-end,要求响应为 206,并把响应体流式写入各自的临时文件。 - 分片级重试:每个分片维护独立尝试计数,失败后按指数退避重试(上限 10 秒),超过
maxRetries抛出异常。 - 合并与清理:所有分片完成后按索引顺序拼接临时文件为目标文件,再删除临时目录。
- 进度上报:通过
channelFlow与trySend(Progress.running(p))、trySend(Progress.done(ok))推送进度与完成信号。
主要陷阱(技能重点内容)
- 探测误判:支持 Range 的服务器对
bytes=0-0返回 206 且带Content-Range;不支持的返回 200 全文。接受任意 2xx 会把“忽略 Range”当作“支持 Range”。应以 206 或Content-Range为准。 - 分片仍需校验 206:探测与真实请求可能因缓存或重定向不一致;若服务器对分片请求返回 200 全文,N 个任务各写全文,合并后文件长度变为 N 倍且无任何报错。
- 重试粒度:单个分片失败只重试该分片,而不是整个文件——这正是每片独立临时文件的意义。
- 重试必须截断重写:重开临时文件要 truncate,不能 append;否则该分片变成 1.5 倍长度,合并结果错位损坏。
- 末片边界:
fileSize / parallel整数除法丢余数,末片必须结束于fileSize - 1,否则尾部字节丢失。 - 进度发送结果:
trySend返回是否被接受;进度 tick 丢弃无害,但终止信号不能被丢弃。完成状态应通过 Flow 自身完成来表达,channel 只用于进度。 - 进度粒度:按分片计数会以 N 步跳变,且因 chunkSize 截断停在略低于 1.0(“卡在 99%”)。若进度条重要,应按字节计数,并用原子计数避免并发重复计数。
- 并行度上下限:低于阈值时两次探测往返的开销大于并发收益(阈值是固定下限常量的两倍);过高则在链路饱和、移动网络或按连接限速的主机上反而变差。
- 失败路径:catch 与合并后都要清理临时目录;不要把异常存入变量后发出“已完成、进度 0”却不再读取,否则收集方无法区分失败与零字节成功。该设计不跨进程重启恢复,续传属于另一项功能。
验证方式
技能给出四条 grep 检查:查找 header("Range"、查找 206 比较、列出所有 trySend(、统计其中检查了结果(isSuccess|isFailure|getOrNull)的数量,用于发现探测无 206 校验、进度发送无人检查等问题。分片边界可用算术测试:选取不能被分片数整除的文件大小,计算每对 start/end,断言无缝无重叠地覆盖 0..fileSize-1。
适用场景与限制
适用于需要在 Kotlin 项目中实现可观测进度的多分片并发下载,且目标服务器支持 Range 的场景。技能本身是经验与陷阱说明,未提供可直接安装的软件包、收费信息或完整可运行项目;提到的 bare HTTP 客户端细节在另一技能 ktor-kmp-client-architecture 中描述。