第3章:实现基础资源下载模块——URL下载、断点续传、文件管理、进度回调
3.1 章节概述
按需分发系统的核心能力是可靠、高效地将远程资源拉取到本地。本章将实现一个具备容错与进度感知能力的基础下载模块,涵盖HTTP(S) URL下载、断点续传、文件增量管理以及实时进度回调。该模块是后续多线程分片下载、差异化更新的基础。
3.2 原理
- URL下载:基于HTTP GET请求,服务端返回200 OK + 完整Body。
- 断点续传:利用HTTP Range请求头,客户端发送
Range: bytes=start-,服务端返回206 Partial Content以及剩余字节。若服务端不支持Range,则回退到全量下载。 - 文件管理:创建下载临时文件(如
.tmp),记录已下载字节数;下载完成后重命名为目标文件,避免脏数据。 - 进度回调:通过回调函数/接口,实时向调用方报告已下载字节数、总字节数、瞬时速率等。
提示:并非所有CDN或静态资源服务器都支持206响应。模块设计时应先发送HEAD请求探测服务器对Range的支持情况,再决定下载策略。
3.3 实现流程
- 检查本地目标文件及临时文件状态,读取已下载字节数
downloaded。 - 构造HTTP请求,若
downloaded > 0则添加Range: bytes=downloaded-头。 - 发起GET请求,分析响应状态码:
200(全量下载),206(续传),否则为失败。 - 打开文件追加写模式,设置接收缓冲区。
- 循环读取响应体数据块,写入本地文件,每读取一块调用进度回调。
- 下载完成后,关闭文件流,将临时文件重命名为目标文件名。
- 若中途失败(网络中断、磁盘满),保持临时文件不删除,供下次续传。
3.4 核心代码实现(Python + requests)
import os
import requests
from pathlib import Path
def download_with_resume(url, dest_path, callback=None):
# 检查本地临时文件
tmp_path = dest_path + ".tmp"
downloaded = 0
if os.path.exists(tmp_path):
downloaded = os.path.getsize(tmp_path)
headers = {"Range": f"bytes={downloaded}-"} if downloaded > 0 else {}
resp = requests.get(url, headers=headers, stream=True)
# 处理不支持续传的情况
if downloaded > 0 and resp.status_code == 200:
downloaded = 0
mode = "wb"
elif downloaded > 0 and resp.status_code == 206:
mode = "ab"
else:
mode = "wb"
total = int(resp.headers.get("content-length", 0)) + downloaded
with open(tmp_path, mode) as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if callback:
callback(downloaded, total)
# 下载完成,重命名
Path(tmp_path).rename(dest_path)
return total
# 使用示例
def my_progress(cur, total):
print(f"下载进度: {cur}/{total} ({cur*100//total}%)")
download_with_resume("https://example.com/resource.bin", "/tmp/resource.bin", callback=my_progress)
3.5 命令行快速验证(curl)
# 第一次下载(正常全量)
curl -o resource.bin https://example.com/resource.bin
# 模拟中断(手动停止后,再续传)
# 续传:使用 -C - 参数(自动断点续传)
curl -C - -o resource.bin https://example.com/resource.bin
# 查看服务端响应头是否支持Range
curl -I https://example.com/resource.bin
# 响应中包含 Accept-Ranges: bytes 表示支持
3.6 调试要点
| 检查项 | 预期结果 | 工具/方法 |
|---|---|---|
| Range请求头是否正确发送 | Range: bytes=1024- | Wireshark / Charles / curl -v |
| 服务端返回状态码 | 206(续传)或200(无续传) | 打印resp.status_code |
| 临时文件完整性 | 中断后文件尾部不丢失 | md5sum全量与部分对比 |
| 进度回调频率 | 每8KB回调一次(chunk_size) | 设置log,检查次数 |
3.7 部署集成建议
- 将下载模块封装为类或独立服务,支持配置超时、重试次数、默认临时目录。
- 使用异步IO或线程池避免阻塞主线程(移动端尤其重要)。
- 在应用启动时清理残留临时文件,避免磁盘膨胀。
3.8 常见错误与处理
| 错误场景 | 原因 | 处理方案 |
|---|---|---|
| 服务器返回416 Range Not Satisfiable | 客户端请求的起始字节超过文件大小 | 重置downloaded=0,从头下载 |
| 下载过程中网络闪断 | 连接丢失 | 捕获requests.exceptions.ConnectionError,保留临时文件,下次重试 |
| 磁盘空间不足 | 写入失败 | 检查预留空间,失败时删除临时文件提示用户 |
| 临时文件被其他进程占用 | 竞态 | 使用文件锁(flock)或确保单实例 |
3.9 性能与稳定性优化
- 动态缓冲区:根据网络延迟调整chunk_size(默认8KB ~ 64KB)
- 并发连接:为单个大文件启用多Range分片并行下载(后续章节展开)
- 写入优化:使用
BufferedWriter减少系统调用次数 - 校验机制:下载完成后计算文件哈希与服务器提供的ETag或Content-MD5对比