2. HTTP固件下载
任务目标
实现一个基于HTTP协议的固件下载模块,能够从指定URL下载固件包到本地存储,并在下载过程中处理网络错误、HTTP错误状态码以及断点续传失败时的重试逻辑。本案例聚焦于下载链路的可靠性与错误恢复,不涉及固件校验、解压或写入Flash等后续步骤。
环境依赖
- Python 3.8+(仅用于本地模拟验证,实际嵌入式环境可移植到C/ESP-IDF等平台)
- 标准库:
urllib.request、os、time、hashlib(用于验证文件完整性)
核心实现
import urllib.request
import urllib.error
import os
import time
import hashlib
class HTTPFirmwareDownloader:
def __init__(self, url, dest_path, max_retries=3, timeout=10):
self.url = url
self.dest_path = dest_path
self.max_retries = max_retries
self.timeout = timeout
self.retry_delay = 2 # 秒
def _http_request(self, headers=None):
"""发起HTTP请求,返回响应对象"""
req = urllib.request.Request(self.url, headers=headers or {})
return urllib.request.urlopen(req, timeout=self.timeout)
def download(self):
"""执行带重试的固件下载,返回(是否成功, 文件路径或错误信息)"""
for attempt in range(1, self.max_retries + 1):
try:
# 检查本地是否已有部分下载(断点续传支持)
existing_size = 0
if os.path.exists(self.dest_path):
existing_size = os.path.getsize(self.dest_path)
headers = {}
if existing_size > 0:
headers['Range'] = f'bytes={existing_size}-'
resp = self._http_request(headers)
# 处理HTTP状态码
if resp.status == 200:
# 服务器不支持断点续传,重新全量下载
mode = 'wb'
existing_size = 0
elif resp.status == 206:
# 支持断点续传,追加写入
mode = 'ab'
elif resp.status == 404:
return False, f"固件不存在(HTTP 404),URL: {self.url}"
elif resp.status == 403:
return False, "服务器拒绝访问(HTTP 403)"
else:
return False, f"HTTP错误状态码: {resp.status}"
# 流式写入文件
with open(self.dest_path, mode) as f:
while True:
chunk = resp.read(4096)
if not chunk:
break
f.write(chunk)
# 验证下载是否完整(通过Content-Length判断)
total_size = resp.headers.get('Content-Length')
if total_size and int(total_size) + existing_size != os.path.getsize(self.dest_path):
raise IOError("文件大小不匹配,下载不完整")
return True, self.dest_path
except urllib.error.HTTPError as e:
# 服务器返回4xx/5xx错误
if e.code == 416: # Range请求超出范围,需重新下载
if os.path.exists(self.dest_path):
os.remove(self.dest_path)
continue
return False, f"HTTP错误: {e.code}"
except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
# 网络层错误,进行重试
if attempt < self.max_retries:
print(f"网络错误(第{attempt}次): {e},{self.retry_delay}秒后重试...")
time.sleep(self.retry_delay)
continue
return False, f"网络错误,重试{self.max_retries}次后仍失败: {e}"
except IOError as e:
# 文件写入错误,不重试(可能是磁盘问题)
return False, f"文件写入错误: {e}"
return False, "未知错误"
def verify_sha256(self, expected_hash):
"""校验下载文件的SHA256哈希值"""
if not os.path.exists(self.dest_path):
return False
sha256 = hashlib.sha256()
with open(self.dest_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest() == expected_hash.lower()
# 使用示例
if __name__ == "__main__":
# 配置下载参数
FIRMWARE_URL = "http://example.com/firmware/v1.2.3.bin"
LOCAL_PATH = "./firmware_download.bin"
EXPECTED_SHA256 = "a3f5...(此处填写实际固件的SHA256值)"
downloader = HTTPFirmwareDownloader(FIRMWARE_URL, LOCAL_PATH, max_retries=3)
success, result = downloader.download()
if success:
print(f"固件下载成功: {result}")
if EXPECTED_SHA256 and downloader.verify_sha256(EXPECTED_SHA256):
print("SHA256校验通过")
else:
print("警告: SHA256校验失败或未提供预期哈希值")
else:
print(f"固件下载失败: {result}")
运行与验证
本地模拟测试:
# 1. 启动本地HTTP服务器模拟固件源
python -m http.server 8000 --directory ./firmware_dir
# 2. 将上述代码保存为 downloader.py,修改URL为:
# FIRMWARE_URL = "http://127.0.0.1:8000/firmware.bin"
# 3. 运行下载
python downloader.py
验证通过条件:
- 下载成功后,本地文件大小与服务器端固件文件大小一致
- 使用
sha256sum firmware_download.bin计算哈希,与服务器端原始文件哈希一致 - 模拟错误场景:在下载过程中断开网络(或使用无效URL),观察程序按预期重试3次后退出并返回错误信息
- 测试断点续传:下载到一半时终止程序,重新运行,观察日志确认发送了
Range请求且服务器返回206
关键设计说明
- 错误分类处理:网络层错误(超时、连接断开)自动重试;HTTP 4xx错误(除416外)直接返回失败不重试,避免无效请求;416错误表示Range范围无效,删除本地残留后重新全量下载
- 断点续传:通过检测本地已有文件大小,自动添加
Range头。服务器返回206时追加写入,返回200时覆盖重写 - 完整性验证:下载完成后通过
Content-Length对比文件大小,并支持外部传入SHA256进行强校验