第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 实现流程

  1. 检查本地目标文件及临时文件状态,读取已下载字节数downloaded。
  2. 构造HTTP请求,若downloaded > 0则添加Range: bytes=downloaded-头。
  3. 发起GET请求,分析响应状态码:200(全量下载),206(续传),否则为失败。
  4. 打开文件追加写模式,设置接收缓冲区。
  5. 循环读取响应体数据块,写入本地文件,每读取一块调用进度回调。
  6. 下载完成后,关闭文件流,将临时文件重命名为目标文件名。
  7. 若中途失败(网络中断、磁盘满),保持临时文件不删除,供下次续传。

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对比

3.10 本章流程框架图

基础下载模块核心流程 开始 / 检查本地文件 本地临时文件存在? Y: 统计已下载字节 N: 从头开始下载 发送HTTP GET (带Range) 解析响应码 200/206/其他 读取数据 → 回调 → 写入 说明:临时文件 .tmp 记录断点

3.11 小结