断点下载不是继续发 Range 请求
移动端资源下载最容易被低估。很多团队以为只要 HTTP 支持 Range,客户端就具备断点续传能力。实际线上问题会复杂得多:Wi-Fi 下载到一半切成蜂窝,玩家是否同意继续;后台被系统暂停后,临时 URL 是否过期;分片文件写入成功但校验未完成,下一次应该从哪里继续;资源 manifest 更新后,旧分片还能不能复用;磁盘空间不足时,哪些半包可以删除,哪些要保留。
Godot 客户端如果只是把下载逻辑写在某个页面脚本里,很快会遇到状态难以解释的问题。玩家看到 80% 进度,切出去回复消息,回来变成 0%;或者下载失败后重试,进度条看似继续,最后校验失败又从头来。更糟糕的是蜂窝网络下重复下载,既浪费流量,也损害信任。
断点下载队列要解决的不是“下载更快”,而是让每一段下载都能被证明、能恢复、能放弃。它需要资源 manifest、网络策略、分片存储、校验状态、UI 进度和后台恢复共同参与。
队列要有明确的任务模型
一个下载任务不应该只是 URL 和保存路径。至少需要 pack_id、pack_revision、manifest_hash、total_bytes、chunk_size、chunk_hashes、priority、network_policy、resume_policy、install_phase。这些字段让客户端知道当前下载属于哪个资源版本,哪些分片已经可信,哪些分片只是写入了磁盘但还没校验。
分片状态建议区分:missing、downloading、downloaded、verified、corrupted、obsolete。只有 verified 才能参与安装。downloaded 只是说明字节落盘,不代表内容正确。很多断点续传 bug 来自把文件大小当成正确性证明,一旦 CDN 返回错误内容、代理中断、磁盘写入半截,就会在安装阶段集中爆炸。
任务模型还要包含用户意图。玩家主动点击“下载当前副本资源”和后台机会型预下载不是一回事。前者可以更积极恢复,后者在蜂窝或低电量时应该暂停。队列里如果没有 reason 和 owner,策略层只能粗暴地全部继续或全部暂停。
状态流
断点下载队列适合用状态机建模。每个任务都有自己的状态,队列也有全局状态。全局状态决定是否允许并发、是否允许解压、是否需要计费确认;任务状态决定当前分片怎么处理。
flowchart TD
A["Manifest Selected"] --> B["Create Download Job"]
B --> C["Check Local Chunks"]
C --> D{"Reusable Chunks?"}
D -- "Yes" --> E["Resume From Verified Chunks"]
D -- "No" --> F["Start Fresh Chunks"]
E --> G["Network Policy Gate"]
F --> G
G --> H{"Allowed Now?"}
H -- "No" --> I["Paused With Reason"]
H -- "Yes" --> J["Download And Verify"]
J --> K{"All Verified?"}
K -- "No" --> G
K -- "Yes" --> L["Install Atomically"]
图里有两个关键点。第一,恢复基于 verified chunks,不是基于文件长度。第二,暂停必须带 reason。暂停原因可能是蜂窝未授权、后台状态、低电量、磁盘不足、token 过期、manifest 变化或服务器限流。UI 看到 reason 后才能给出正确文案。
网络策略不是下载器的附属品
下载器不应该自己决定是否能用蜂窝网络。它应该向 NetworkDownloadPolicy 提交任务信息:资源大小、任务原因、当前网络、玩家设置、是否低数据模式、是否当前玩法必需。策略返回 allow、pause、ask_user 或 defer。
这和单独的移动网络计费提示不同。断点下载队列面临的是持续变化。玩家一开始在 Wi-Fi 下同意下载,下载到 60% 时出门切到蜂窝。此时不能把旧同意自动延伸到新网络,也不能直接失败。更合理的做法是暂停任务,保留已校验分片,提示“网络已切换,是否使用蜂窝继续剩余 180MB”。如果玩家拒绝,任务保持 paused,不清除进度。
低数据模式也要参与。即使网络是 Wi-Fi,系统或玩家也可能启用低数据策略。后台机会型预下载应该延后,当前玩法必需资源可以继续但降低并发。策略要保留解释字段,不要只返回布尔值。
分片大小和校验
分片大小不是越小越好。太小会增加请求数量和元数据成本,太大则导致失败回滚多、校验粒度粗。移动端可以按资源包大小分层:小包直接整体下载,中包按 1-4MB 分片,大包按 4-16MB 分片。具体数值要看 CDN、磁盘、CPU 和游戏场景,不能只照搬。
每个分片要有哈希。manifest 里保存 chunk hash,下载后先写临时文件,再校验,校验通过后标记 verified。安装时使用 verified 分片合成或解包。不要直接覆盖正式资源目录。正式目录只在安装成功后原子切换,例如通过版本目录和 current 指针实现。
Godot 里可以把下载状态存成一个小型 JSON 或 Resource,但写入要谨慎。每个分片完成都写一次状态会增加 I/O。更稳的是批量 flush,或者在关键状态变化时写入:任务创建、分片 verified、任务暂停、安装完成。崩溃恢复时,重新扫描临时目录并校验分片,不要完全相信状态文件。
安装阶段更危险
很多下载系统只关心进度条,忽略安装。资源包下载完成后,还要解压、移动文件、更新 manifest、清理旧版本、通知资源加载层。移动端上,安装阶段可能比下载阶段更容易出错,因为它涉及磁盘空间、内存峰值和原子切换。
安装前要做三项检查。第一,磁盘空间是否足够,不只是最终体积,还包括临时解压空间。第二,当前场景是否允许切换资源版本,战斗中不应替换正在使用的资源。第三,资源依赖是否完整,不能只装主包而缺依赖包。
安装要么成功,要么保持旧版本可用。不要出现一半文件被新版本覆盖,另一半仍是旧版本。Godot 资源路径如果直接指向可变目录,要特别小心缓存。更安全的方式是按版本目录加载,安装完成后更新资源索引,下一次场景切换或安全点再启用新版本。
UI 进度要诚实
下载 UI 不能只显示一个百分比。玩家需要知道当前处于下载、暂停、等待网络确认、校验、安装还是失败恢复。80% 下载完成不等于 80% 可用,安装和校验也需要时间。可以把进度拆成下载进度和准备进度,或者在文案中明确“正在校验”“正在安装”。
暂停状态要显示原因。蜂窝未授权、磁盘不足、后台暂停、服务器限流、登录过期、资源版本变化,这些原因对应的操作完全不同。蜂窝未授权可以让玩家选择继续,磁盘不足要引导清理空间,登录过期要等会话恢复,资源版本变化则要重新计算任务。
不要把所有暂停都写成失败。暂停是系统保护行为。只有校验失败、服务器返回不可恢复错误、资源版本不兼容等情况才应该进入失败。这个细节会直接影响玩家信任。
和前后台恢复协作
断点下载队列必须接入前后台恢复。应用进入后台时,队列要保存状态,暂停高风险写入,尽量不要在系统即将冻结时做大文件移动。回到前台后,先等待 ResumeGate 完成网络和会话检查,再恢复下载。临时 URL 需要重新签名,token 过期需要刷新,网络 generation 变化要重新跑策略。
如果应用在后台被杀,下一次启动要能恢复。启动时扫描下载工作目录,把每个任务与 manifest 对齐。manifest 不存在或 revision 不匹配的任务标记 obsolete;分片文件重新校验;安装中的任务如果没有完成原子切换,应回滚到旧版本。
这套流程看起来麻烦,但它能避免最伤人的体验:下载了很久,回来全没了。即使不能继续,客户端也应该能解释为什么不能继续。
GDScript 结构示例
下面的代码只展示职责分层,不是完整下载器:
func tick_download_queue(delta: float) -> void:
if not resume_gate.is_ready_for_downloads():
queue.pause_all("resume_gate_not_ready")
return
var budget := policy.current_download_budget()
for job in queue.pick_jobs(budget):
var decision := network_policy.evaluate(job)
if decision.kind != "allow":
job.pause(decision.reason)
continue
downloader.run_job_slice(job, budget)
downloader 只做下载和校验,不决定蜂窝策略;network_policy 只做策略判断,不直接改文件;queue 只维护任务状态。边界清楚后,QA 才能通过日志判断是哪一层做了决定。
日志和指标
下载日志要按任务串起来。字段包括 job_id、pack_id、revision、manifest_hash、network_generation、resume_id、verified_chunks、total_chunks、pause_reason、install_result、bytes_reused。bytes_reused 很重要,它能衡量断点续传是否真正节省了流量。
线上指标可以关注:平均复用字节、蜂窝暂停次数、校验失败率、安装失败率、后台恢复成功率、manifest 变化导致重下比例、磁盘不足失败比例。如果断点续传系统上线后,下载失败率下降但安装失败率上升,说明问题被推迟到安装阶段,并没有真正解决。
QA 场景
必须测这些组合:Wi-Fi 下载中切蜂窝、蜂窝下载中切 Wi-Fi、下载中锁屏 5 分钟、下载中杀进程重启、下载中 token 过期、下载中 manifest 更新、下载中磁盘空间变少、下载完成但安装时切后台、校验失败后重试。
每个场景都看四件事:已校验分片是否复用,UI 文案是否准确,是否重复消耗流量,安装后资源是否一致。测试时最好准备一个可控测试包,包含固定分片哈希和可模拟错误的服务端响应。没有可控服务端,只靠真实 CDN 很难覆盖边界。
灰度发布怎么做
断点下载队列不要一次替换所有资源下载。第一阶段可以只接一个低风险资源包,例如可选语音包或活动高清贴图。它体积要足够大,能覆盖分片、暂停和安装;但失败时不能阻断主线玩法。通过这个包验证状态文件、分片复用、UI 文案和日志后,再接入关键副本资源。
灰度期间要保留旧下载路径的回退开关。新队列如果出现校验失败率异常、安装失败率异常或重复下载字节异常,客户端应能切回整体下载,至少保证玩家能拿到资源。这个回退不是长期方案,但能避免资源系统灰度时把玩家挡在入口外。
版本兼容也要提前想好。旧客户端可能不知道新 manifest 的 chunk hash,新客户端可能读到旧状态文件。状态文件需要 schema_version,升级时能迁移或丢弃。丢弃不是问题,默默按旧格式读取才危险。只要状态不可解释,就应重新校验临时目录,必要时从头下载。
磁盘预算和清理
移动端断点下载会制造临时文件。分片、合并文件、解压目录、旧版本备份都占空间。如果只在下载前检查最终包体大小,安装阶段很容易失败。队列应该维护磁盘预算:当前任务预计还需要多少临时空间,已验证分片占多少,安装峰值需要多少,旧版本能否在安装完成后删除。
清理策略要谨慎。正在下载的 verified chunk 可以长期保留,但 obsolete chunk 应该在安全时清理。安全时机包括应用启动后的恢复扫描、任务取消确认后、manifest 确认不再引用后。不要在玩家低电量、内存警告或战斗加载中做大规模删除,文件系统 I/O 也会造成卡顿。
玩家主动取消下载时,最好询问是否保留进度。对大包来说,保留进度是友好选择;对小包或版本即将过期的包,可以直接清理。这个策略也要有配置,不同资源包的取消成本不同。
和运营活动的关系
活动资源包最容易让下载队列背锅。活动上线时间固定,资源版本变化频繁,玩家常常在入口开放后集中下载。如果 manifest、CDN 和客户端队列没有配合,玩家会看到下载排队、校验失败或版本反复更新。
活动包建议提前支持预下载,但预下载必须尊重网络策略和存储空间。活动未开放前下载的包,开放时还要校验 revision 是否仍然有效。不要因为预下载成功就跳过最终 manifest 检查。活动下线后,资源清理也要走同一套 owner 和 retention 规则,避免旧活动包长期占空间。
运营配置里最好能声明资源包优先级和可选性。当前玩法必需包、活动装饰包、高清语音包的下载策略不应相同。断点下载队列只有拿到这些语义,才能做出符合体验的决策。
小结
移动端断点下载队列的核心不是 Range 请求,而是可信状态。Godot 客户端需要知道哪些分片可信、当前网络是否允许、会话是否有效、安装是否安全。只要这些问题没有统一答案,进度条就会变成一种安慰,而不是可靠承诺。
建议从任务模型和分片校验开始,不急着做复杂并发。先让“下载到一半切网络、杀进程、回来继续”这条路径稳定,再谈速度优化。对玩家来说,能继续,比快一点更重要。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。