1. 项目概述:为什么我们需要断点续传工具?
如果你曾经在下载一个几GB的大文件时,网络突然中断,或者电脑意外重启,然后不得不从头开始下载,那种感觉一定糟透了。我经历过太多次,尤其是在处理虚拟机镜像、高清视频素材或者大型软件安装包的时候。这种场景下,一个可靠的“断点续传”功能,就不再是锦上添花,而是雪中送炭的必需品了。
简单来说,断点续传工具的核心价值,就是让文件传输过程具备“记忆”和“恢复”能力。它把一个大文件在逻辑上切成许多小块,并精确记录下每一块的传输状态。当传输因任何原因中断后,再次启动时,工具会先检查本地已接收的部分,然后只从断掉的地方继续下载或上传剩余的部分,而不是傻乎乎地重头再来。这不仅能节省大量时间和带宽,更重要的是,它极大地提升了传输任务的可靠性和用户体验,尤其是在网络不稳定或需要长时间运行任务的场景下。
从个人用户备份照片视频,到开发者同步代码仓库,再到运维人员部署大型应用,断点续传都是一个底层但至关重要的能力。市面上虽然有很多集成此功能的下载器(如浏览器、专业下载软件),但一个独立、轻量、可编程的断点续传工具,能让我们更深入地理解其原理,并将其灵活嵌入到自己的自动化脚本或定制化应用中。接下来,我们就从设计思路开始,拆解如何打造这样一个工具。
2. 核心设计思路与协议选择
要自己实现一个断点续传工具,首先得想清楚它的工作模式和技术选型。这决定了工具的灵活性、兼容性和最终实现的复杂度。
2.1 客户端与服务器模型
最常见的模型是客户端/服务器(C/S)模型。我们的工具通常作为客户端,需要从一个支持断点续传的服务器(例如标准的HTTP/1.1服务器、FTP服务器或自定义的TCP服务器)获取文件。因此,工具的实现分为两个层面:
- 协议支持层:工具需要遵循特定网络协议中关于断点续传的规范。
- 本地管理层:工具需要在本地管理文件分块、记录下载状态、处理文件IO。
对于大多数应用场景,HTTP协议是首选。因为它是互联网上最通用的协议,几乎所有的Web服务器都支持HTTP/1.1,而HTTP/1.1标准明确规定了用于断点续传的头部字段,实现起来有规可循,兼容性最好。
2.2 HTTP断点续传原理剖析
HTTP协议实现断点续传,主要依赖两个请求头和一个响应头:
Range(请求头):由客户端发送,告知服务器需要文件的哪个字节范围。- 格式:
Range: bytes=start-end - 例如:
Range: bytes=1024-2047表示请求从第1024字节到第2047字节(共1024字节)的数据。Range: bytes=1024-表示请求从第1024字节直到文件末尾的所有数据。
- 格式:
Content-Range(响应头):由支持范围请求的服务器返回,告知客户端当前返回的数据在完整文件中的位置。- 格式:
Content-Range: bytes start-end/total - 例如:对于上面的请求,服务器可能返回:
Content-Range: bytes 1024-2047/10240,表示这是总共10240字节的文件中的1024-2047字节部分。
- 格式:
Accept-Ranges(响应头):服务器在响应初始请求(非Range请求)时,通过此头部声明自己是否支持范围请求。通常是Accept-Ranges: bytes。
工作流程:
- 工具首先发送一个普通的
HEAD或GET请求(不携带Range头),获取文件的基本信息,如文件总大小(Content-Length)和是否支持断点续传(Accept-Ranges: bytes)。 - 检查本地是否存在部分下载的临时文件或状态记录文件。如果存在,读取已下载的字节数,假设为
downloaded_size。 - 构造并发送携带
Range: bytes=downloaded_size-的GET请求。 - 服务器返回
206 Partial Content状态码(而非完整的200 OK),并在响应体中包含从downloaded_size开始的文件数据,同时在Content-Range头中指明范围。 - 客户端将接收到的数据追加写入到本地临时文件的末尾。
- 重复步骤3-5,直到
Content-Range指示已传输完毕(例如bytes x-y/total中的y等于total-1)。
注意:并非所有服务器都完美支持
Range请求。有些可能忽略Range头直接返回整个文件(状态码200),有些可能返回200 OK但内容却是部分数据(不符合规范)。因此,健壮的工具必须能处理这些异常情况,例如通过比较已接收数据量和Content-Range声明的大小进行校验。
2.3 多线程并发下载的考量
单纯的单线程断点续传可以解决“中断恢复”的问题,但为了最大化利用带宽(尤其是在高延迟网络或服务器限速的情况下),我们通常会引入多线程(或多连接)并发下载。
其基本思路是:
- 获取文件总大小(
total_size)。 - 根据设定的线程数(如4个),将文件平均分成相应的段(Segment)。
- 例如:文件大小 10MB,线程数4,则每个线程负责下载 2.5MB。
- 线程1:
Range: bytes=0-2621439 - 线程2:
Range: bytes=2621440-5242879 - 以此类推。
- 每个线程独立发起自己的
Range请求,下载指定的字节段,并写入到本地文件的指定位置(需要使用支持随机写入的文件IO操作)。 - 所有线程下载完成后,合并成一个完整的文件。
这种方式能显著提升下载速度,但复杂度也更高:
- 状态管理更复杂:需要记录每个分块的下载状态(未开始、下载中、已完成、错误)。
- 文件IO需要随机写入:每个线程需要将数据写入文件的不同偏移量处。
- 错误处理与恢复:单个线程失败,只需重试该线程负责的区块,不影响其他线程。
- 最终合并:所有分块下载完成后,需要确保文件拼接正确。
在我们的工具实现中,我会先实现一个稳固的单线程断点续传基础框架,然后再在此基础上扩展多线程功能,这样更容易理解和调试。
3. 工具核心模块设计与实现
我们将使用Python来构建这个工具,因为它语法简洁,网络库强大,非常适合做原型和实际工具。核心模块主要分为:网络请求模块、状态管理模块、文件IO模块和主控逻辑模块。
3.1 网络请求模块:使用requests库处理Range请求
Python的requests库对HTTP协议支持非常友好,可以很容易地设置请求头。我们将用它来发送携带Range头的请求,并处理响应。
首先,我们需要一个函数来获取文件信息:
import requests import os def get_file_info(url): """ 获取远程文件信息(大小、是否支持断点续传)。 返回: (support_breakpoint, file_size) 或 (False, None) """ try: # 使用HEAD方法,只获取头部信息,不下载主体 resp = requests.head(url, timeout=10, allow_redirects=True) resp.raise_for_status() # 检查HTTP错误 # 检查是否支持字节范围请求 accept_ranges = resp.headers.get('Accept-Ranges', 'none') support_breakpoint = (accept_ranges.lower() == 'bytes') # 获取文件总大小 content_length = resp.headers.get('Content-Length') file_size = int(content_length) if content_length else None return support_breakpoint, file_size, resp.url # 返回最终URL(处理重定向后) except requests.exceptions.RequestException as e: print(f"获取文件信息失败: {e}") return False, None, url关键点:
- 使用
HEAD方法而非GET,避免在获取元信息时下载整个文件体。 allow_redirects=True自动处理重定向,并返回最终的URL。- 从
Accept-Ranges头判断服务器支持情况。 - 从
Content-Length头获取文件大小,这是后续分块的基础。
接下来,是支持断点续传的下载函数核心部分:
def download_range(url, start_byte, end_byte, local_file_path): """ 下载文件的指定字节范围,并写入本地文件的指定位置。 """ headers = {'Range': f'bytes={start_byte}-{end_byte}'} try: resp = requests.get(url, headers=headers, stream=True, timeout=30) resp.raise_for_status() # 检查响应状态码,206表示部分内容,200可能表示服务器不支持Range if resp.status_code == 206: # 打开文件,定位到start_byte位置进行写入 with open(local_file_path, 'r+b') as f: f.seek(start_byte) for chunk in resp.iter_content(chunk_size=8192): # 分块写入,避免内存占用过高 if chunk: f.write(chunk) f.flush() # 及时刷入磁盘,状态更可靠 return True elif resp.status_code == 200: print("警告:服务器可能不支持断点续传,收到了完整文件响应。") # 这种情况下,需要根据策略决定是覆盖写入还是放弃 # 简单处理:直接覆盖从头写入(这会导致断点续传失效) with open(local_file_path, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) return True else: print(f"意外的HTTP状态码: {resp.status_code}") return False except requests.exceptions.RequestException as e: print(f"下载范围 {start_byte}-{end_byte} 失败: {e}") return False关键点:
headers={'Range': ...}是发起范围请求的关键。stream=True至关重要,它让requests以流的方式获取响应体,我们可以用iter_content方法分块读取,避免大文件一次性加载到内存。- 对
status_code的判断是健壮性的体现。正确处理206和200。 - 使用
'r+b'模式打开文件,seek到指定位置进行写入,这是实现“续传”和“多线程分块写入”的基石。 f.flush()建议在每次写入后调用,确保数据写入磁盘,这样即使程序意外退出,已下载的数据也是持久化的。
3.2 状态管理模块:如何记录下载进度
要实现断点续传,工具必须“记住”已经下载了哪些部分。我们有两种主流方式:
方式一:临时文件法这是最直观的方法。下载时,目标文件(例如video.mp4)可能被命名为video.mp4.part或video.mp4.download。同时,创建一个同名的状态文件,如video.mp4.part.status.json,里面用JSON格式记录:
{ "url": "http://example.com/largefile.zip", "total_size": 104857600, "downloaded_size": 52428800, "chunks": [ {"start": 0, "end": 26214399, "finished": true}, {"start": 26214400, "end": 52428799, "finished": true}, {"start": 52428800, "end": 78643199, "finished": false}, ... ] }每次写入数据后,都更新这个状态文件。恢复时,读取状态文件就知道该从哪里开始了。
方式二:文件本身作为状态记录(推荐)更简单巧妙的方法是:直接利用下载中的文件本身。我们总是向最终目标文件名(如video.mp4)写入数据。开始时,如果文件不存在,就创建它,并将其大小“扩展”到与远程文件一样大(例如,在Windows下快速创建一个全零文件,在Linux下使用truncate或fallocate)。然后,每个下载线程只负责填充文件中属于自己的那部分区间。
恢复时,我们只需要:
- 检查目标文件是否存在。
- 如果存在,获取它的当前大小(
os.path.getsize),这个大小就是已下载的字节数。 - 从这个大小处开始继续请求(
Range: bytes=current_size-)。
这种方法零额外状态文件,完全依赖文件系统的可靠性,更加简洁。但它更适合单线程或顺序写入的断点续传。对于多线程分块下载,因为文件是预先分配好的,每个线程写入固定位置,恢复时需要知道每个块是否完成,此时可能仍需一个轻量级的状态文件来记录各分块完成情况,或者通过读取文件各区块的数据是否完整(例如校验和)来判断,后者实现较复杂。
在我们的单线程示例中,采用第二种方法最为简单。我们通过检查本地文件大小来决定续传的起始点。
3.3 文件IO与完整性校验
文件操作是数据落地的最后一步,必须保证正确性和效率。
- 追加写入与定位:如上述代码所示,使用
open(file_path, 'r+b')模式,seek到文件末尾,然后进行写入。 - 分块写入:使用
resp.iter_content(chunk_size=8192)循环读取和写入。chunk_size可以根据实际情况调整(如 64KB),平衡内存使用和IO效率。 - 完整性校验(可选但重要):下载完成后,为了确保文件在网络传输中没有发生错误,应该进行校验。常见做法是,服务器在提供文件时也提供其哈希值(如MD5、SHA1、SHA256),放在响应头(如
ETag,但ETag不一定基于内容)或单独的校验文件中。下载完成后,工具计算本地文件的哈希值并与服务器提供的进行比对。
如果校验失败,说明文件损坏,需要删除重新下载或重新下载出错的部分。import hashlib def calculate_file_hash(file_path, algorithm='sha256'): hash_func = hashlib.new(algorithm) with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(4096), b""): hash_func.update(chunk) return hash_func.hexdigest()
4. 单线程断点续传工具完整实现与演示
结合以上模块,我们可以组装一个完整的、命令行的单线程断点续传下载工具。
#!/usr/bin/env python3 """ 简易单线程HTTP断点续传下载器 """ import requests import os import sys import time def download_file_with_resume(url, local_filename=None): """ 支持断点续传的下载函数(单线程) """ if not local_filename: local_filename = url.split('/')[-1] or 'downloaded_file' # 1. 获取文件信息 print(f"正在检查文件信息...") support_breakpoint, total_size, final_url = get_file_info(url) if total_size is None: print("无法获取文件大小,退出。") return False print(f"文件总大小: {total_size / (1024*1024):.2f} MB") if not support_breakpoint: print("警告:服务器可能不支持断点续传,将尝试完整下载。") # 2. 处理本地文件,确定起始点 start_byte = 0 if os.path.exists(local_filename): start_byte = os.path.getsize(local_filename) print(f"发现已存在的文件 '{local_filename}',大小: {start_byte} 字节") if start_byte >= total_size: print("文件已存在且大小不小于远程文件,跳过下载。") return True if start_byte > 0: print(f"将从字节 {start_byte} 处继续下载。") else: print(f"将开始下载新文件到 '{local_filename}'") # 3. 设置请求头,请求剩余部分 headers = {} if start_byte > 0: headers['Range'] = f'bytes={start_byte}-' # 4. 发起请求并流式下载 try: with requests.get(final_url, headers=headers, stream=True, timeout=30) as r: r.raise_for_status() # 检查响应状态 if start_byte > 0 and r.status_code != 206: print(f"警告:请求续传但服务器返回状态码 {r.status_code},可能不支持断点续传,将重新下载。") # 可以选择删除已存在部分,这里我们选择覆盖 start_byte = 0 mode = 'wb' else: mode = 'ab' if start_byte > 0 else 'wb' # 获取本次实际要下载的内容长度 content_length = r.headers.get('Content-Length') current_total = int(content_length) if content_length else 0 if start_byte > 0 and r.status_code == 206: # 对于206响应,Content-Length是本次返回的部分的大小 remaining_size = current_total else: # 对于200响应或从头下载,Content-Length是总大小(如果已知) remaining_size = total_size - start_byte if total_size else None print(f"开始下载剩余数据...") downloaded = start_byte last_print_time = time.time() with open(local_filename, mode) as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) # 简单进度显示 current_time = time.time() if current_time - last_print_time > 1.0: # 每秒更新一次 if remaining_size: percent = (downloaded / total_size) * 100 print(f"\r进度: {downloaded}/{total_size} bytes ({percent:.1f}%)", end='', flush=True) else: print(f"\r已下载: {downloaded} bytes", end='', flush=True) last_print_time = current_time print() # 换行 print(f"下载完成!文件保存为: {local_filename}") return True except requests.exceptions.RequestException as e: print(f"\n下载过程中发生错误: {e}") return False except KeyboardInterrupt: print(f"\n\n下载被用户中断。文件 '{local_filename}' 已保存了 {os.path.getsize(local_filename)} 字节。") print("下次运行将自动从断点处继续。") sys.exit(0) if __name__ == '__main__': if len(sys.argv) < 2: print("用法: python resume_downloader.py <文件URL> [本地文件名]") sys.exit(1) url = sys.argv[1] local_name = sys.argv[2] if len(sys.argv) > 2 else None success = download_file_with_resume(url, local_name) sys.exit(0 if success else 1)使用演示:
- 将代码保存为
resume_downloader.py。 - 在命令行中运行:
python resume_downloader.py https://example.com/path/to/largefile.zip - 下载过程中,可以按
Ctrl+C中断。 - 再次运行相同的命令,工具会自动检测到已存在的
.zip文件,并从断点处继续下载。
这个工具已经具备了核心的断点续传能力。它结构清晰,包含了错误处理、进度显示和用户中断处理,是一个可用的基础版本。
5. 进阶:多线程并发下载的实现与优化
单线程工具在稳定性上表现很好,但速度可能受限于单TCP连接的带宽。接下来,我们将其升级为多线程并发下载工具。
5.1 分块策略与线程池
我们使用Python的concurrent.futures模块中的ThreadPoolExecutor来管理下载线程。
import concurrent.futures import threading class MultiThreadedDownloader: def __init__(self, url, local_path, num_threads=4): self.url = url self.local_path = local_path self.num_threads = num_threads self.total_size = 0 self.chunk_size = 0 self.lock = threading.Lock() # 用于线程安全地更新进度 self.downloaded_chunks = 0 def prepare(self): """准备工作:获取文件大小,创建空文件,计算分块""" support, self.total_size, _ = get_file_info(self.url) if not support or self.total_size is None: raise Exception("无法获取文件信息或不支持断点续传") # 计算每个线程负责的字节范围 self.chunk_size = self.total_size // self.num_threads self.ranges = [] for i in range(self.num_threads): start = i * self.chunk_size # 最后一个线程获取所有剩余字节 end = (self.total_size - 1) if (i == self.num_threads - 1) else (start + self.chunk_size - 1) self.ranges.append((start, end)) # 预创建(或清空)目标文件 with open(self.local_path, 'wb') as f: f.truncate(self.total_size) # 关键:预分配磁盘空间 print(f"文件已预分配,总大小: {self.total_size} bytes, 线程数: {self.num_threads}") def download_chunk(self, thread_id, start, end): """单个线程下载指定范围的数据""" chunk_filename = f"{self.local_path}.part{thread_id}" # 临时分块文件 headers = {'Range': f'bytes={start}-{end}'} try: with requests.get(self.url, headers=headers, stream=True, timeout=60) as r: r.raise_for_status() if r.status_code != 206: print(f"线程{thread_id}: 服务器未返回206,可能不支持分块下载。") return False downloaded = 0 with open(chunk_filename, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) # 下载完成后,将分块文件内容写入最终文件的正确位置 with open(self.local_path, 'r+b') as final_f: final_f.seek(start) with open(chunk_filename, 'rb') as chunk_f: final_f.write(chunk_f.read()) # 删除临时分块文件 os.remove(chunk_filename) with self.lock: self.downloaded_chunks += 1 print(f"线程{thread_id} 完成 ({start}-{end})。 总体进度: {self.downloaded_chunks}/{self.num_threads}") return True except Exception as e: print(f"线程{thread_id} 下载失败: {e}") return False def run(self): """启动多线程下载""" self.prepare() print("开始多线程下载...") with concurrent.futures.ThreadPoolExecutor(max_workers=self.num_threads) as executor: # 提交所有任务 future_to_chunk = { executor.submit(self.download_chunk, i, start, end): i for i, (start, end) in enumerate(self.ranges) } # 等待所有任务完成,并处理结果 results = [] for future in concurrent.futures.as_completed(future_to_chunk): chunk_id = future_to_chunk[future] try: result = future.result() results.append(result) except Exception as e: print(f"线程{chunk_id} 产生异常: {e}") results.append(False) if all(results): print(f"\n恭喜!文件 '{self.local_path}' 下载完成。") return True else: print(f"\n下载过程中部分线程失败。") # 这里可以实现更复杂的重试逻辑 return False关键改进与解释:
- 预分配文件:
f.truncate(self.total_size)在开始下载前就创建了一个大小与远程文件一致的空文件(或稀疏文件)。这有两个好处:一是提前检查磁盘空间是否足够,二是为多线程并行写入固定位置做好了准备。 - 分块临时文件:每个线程先将自己的数据块下载到一个独立的临时文件(如
.part0),完成后再一次性写入最终文件的指定位置。这比多个线程同时seek和写入同一个文件更安全,避免了复杂的文件锁和写入位置冲突。 - 线程安全进度更新:使用
threading.Lock来保护self.downloaded_chunks这个共享变量,确保进度打印不会错乱。 - 错误隔离:一个线程的失败不会直接影响其他线程,所有任务提交后由
ThreadPoolExecutor统一管理。最后检查所有线程的结果,判断整体成功与否。
5.2 更健壮的状态管理与恢复
上述多线程版本在任务中断后,重新运行会从头开始,因为它没有记录每个分块的状态。为了支持多线程的断点续传,我们需要一个状态文件来记录每个范围(chunk)的完成情况。
我们可以设计一个状态文件(如.status.json):
{ "url": "http://...", "total_size": 104857600, "num_chunks": 4, "chunks": [ {"id": 0, "start": 0, "end": 26214399, "finished": true, "temp_file": "largefile.zip.part0"}, {"id": 1, "start": 26214400, "end": 52428799, "finished": false, "temp_file": "largefile.zip.part1"}, ... ] }在prepare()阶段,先检查状态文件是否存在。如果存在,就加载状态,只对那些"finished": false的分块重新创建下载任务。每个分块下载完成后,立即更新状态文件并将finished改为true。这样,即使程序中途崩溃,重启后也能精确恢复。
5.3 性能调优与注意事项
- 线程数设置:不是线程越多越快。受限于本地CPU、磁盘IO和服务器并发连接限制,通常4-8个线程是甜点区间。可以做成可配置参数。
- 分块大小:避免分块过小(导致大量HTTP请求开销)或过大(失去并发意义)。通常1MB到10MB是一个合理的范围。可以根据文件总大小动态计算。
- 磁盘IO瓶颈:多个线程同时写入多个临时文件,最后再合并,可能会造成磁盘IO竞争。如果下载速度远超磁盘写入速度,多线程带来的提升会有限,甚至可能变慢。使用SSD会好很多。
- 服务器限制:有些服务器会对同一IP的并发连接数或请求频率进行限制。过多的线程可能导致IP被暂时封禁。增加重试机制和指数退避策略是必要的。
- 内存使用:使用
stream=True和分块读取写入,确保即使下载超大文件,内存占用也保持稳定。
6. 常见问题排查与实战技巧
在实际使用和开发断点续传工具时,你会遇到各种各样的问题。下面是我总结的一些典型场景和解决思路。
6.1 服务器返回状态码200而不是206
现象:你发送了带Range头的请求,但服务器返回了200 OK和整个文件。原因:
- 服务器根本不支持
Range请求(Accept-Ranges头为none或不存在)。 - 服务器支持,但你请求的范围无效(例如,起始位置大于文件大小)。
- 某些服务器或CDN配置问题,对某些文件类型或路径不支持断点续传。应对:
- 首先检查
HEAD请求返回的Accept-Ranges头。 - 如果服务器明确不支持,工具应降级为普通单线程下载,并给出明确提示。
- 在代码中做好兼容,当收到200响应时,根据策略决定是覆盖还是放弃。
6.2 下载的文件大小不正确或损坏
现象:下载完成,但文件无法打开,或对比哈希值不一致。原因:
- 网络传输错误:TCP协议能保证数据顺序和可靠性,但在应用层,如果HTTP连接异常中断,最后收到的数据包可能不完整。
- 服务器动态内容:你下载的是一个动态生成的页面或文件,其
Content-Length在两次请求间可能发生变化。 - 多线程写入冲突或错误:多线程工具中,如果文件写入逻辑有bug,可能导致数据覆盖或错位。排查与解决:
- 强制校验:如果服务器提供了
Content-MD5或ETag(强校验类型),下载完成后务必进行校验。没有官方校验值时,可以尝试从其他可信源获取哈希值进行比对。 - 日志与重试:为工具增加详细日志,记录每个分块的下载开始、结束和大小。对于校验失败的分块,自动进行重试(例如最多3次)。
- 使用更可靠的协议:对于关键文件,考虑使用支持完整性校验的协议,如
rsync或BitTorrent。
6.3 连接超时与不稳定网络处理
现象:下载经常中断,超时错误频发。技巧:
- 设置合理的超时:
requests.get(timeout=(连接超时, 读取超时))。例如timeout=(10, 30)表示10秒连接超时,30秒读取超时。 - 自动重试机制:使用
urllib3的Retry或第三方库如tenacity,为请求配置重试策略(例如,对连接错误、超时、5xx状态码进行最多3次重试,并加入随机间隔避免拥塞)。from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry_strategy = Retry( total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504] ) adapter = HTTPAdapter(max_retries=retry_strategy) session = requests.Session() session.mount("http://", adapter) session.mount("https://", adapter) # 然后用session代替requests进行请求 - 分块大小自适应:在网络极差的环境下,可以减小分块大小,这样每次重试的代价更小。
6.4 实战技巧:提升工具的用户体验
- 友好的进度显示:除了百分比,可以显示下载速度(MB/s)、剩余时间。计算速度时,建议用平滑算法(如移动平均)避免数字跳动过快。
- 支持暂停/继续:捕捉
Ctrl+C信号,优雅地保存状态后退出。这就是我们之前代码中except KeyboardInterrupt做的事情。 - 配置文件与命令行参数:使用
argparse库增强命令行工具,允许用户指定线程数、输出目录、代理等。 - 代理支持:在
requests.get()中传入proxies参数,方便内网用户或需要代理的场景。 - 递归下载目录(高级):如果服务器支持目录列表(如Apache的
Indexes选项),可以扩展工具,使其能解析HTML页面,递归下载整个目录下的所有文件,并为每个文件应用断点续传。
开发一个断点续传工具,从简单的单线程版本到功能齐全的多线程版本,是一个逐步深入理解HTTP协议、网络编程、文件系统和并发处理的过程。它虽然不复杂,但涉及到的细节很多,每一个细节都影响着工具的稳定性和用户体验。