如果你用过FDM或者IDM大概率知道它们能加速下载。但加速背后的协议机制是什么分段请求怎么实现断点续传线程数设多少才不浪费这篇文章不介绍工具怎么用直接从HTTP协议层入手一步步写一个生产可用的多线程下载器。代码用Python核心逻辑不到200行跑起来之后你会对下载管理器的底层实现有一个完整的认知。一、Range请求被低估的HTTP能力HTTP/1.1规范里有一个叫Range的请求头RFC 7233允许客户端只请求文件的一部分而不是全部。绝大多数CDN和文件服务器都支持它但浏览器默认不用——它只会发一个完整的GET请求单线程从头拉到尾。一次Range请求的完整交互过程# 客户端先发HEAD确认服务器是否支持分段 HEAD /files/ubuntu-22.04.iso HTTP/1.1 Host: releases.ubuntu.com # 服务器返回 HTTP/1.1 200 OK Content-Length: 2852126720 Accept-Ranges: bytes ← 关键有这个才支持分段 # 客户端确认支持发分段请求 GET /files/ubuntu-22.04.iso HTTP/1.1 Host: releases.ubuntu.com Range: bytes0-1048575 ← 只请求前1MB # 服务器只返回这一段 HTTP/1.1 206 Partial Content Content-Range: bytes 0-1048575/2852126720 Content-Length: 1048576注意几个细节响应码是206不是200——这很关键解析的时候要判断Content-Range的格式是bytes start-end/totaltotal在整个文件下载过程中是不变的可以用来校验完整性如果服务器不支持分段它会返回200并给你整个文件客户端代码要处理这个退化情况二、拆解核心模块一个生产级的下载器至少需要四个模块1. 文件探测模块—— 发送HEAD请求获取Content-Length和Accept-Ranges。如果Accept-Ranges缺失退化为单线程下载。defprobe(url):resprequests.head(url,allow_redirectsTrue,timeout10)sizeint(resp.headers.get(Content-Length,0))supports_rangeresp.headers.get(Accept-Ranges,).lower()bytesreturnsize,supports_range2. 分片调度模块—— 将文件按线程数等分计算每个线程的起始和结束字节位置。注意最后一个分片要覆盖到文件末尾defsplit_chunks(file_size,num_threads):chunk_sizefile_size//num_threads chunks[]foriinrange(num_threads):starti*chunk_size endfile_size-1ifinum_threads-1else(i1)*chunk_size-1chunks.append((i,start,end))returnchunks3. 下载执行模块—— 每个线程独立下载自己的分片带重试机制。这是整个下载器最核心的部分defdownload_chunk(url,chunk_id,start,end,output_dir,progress_dict,max_retries3):chunk_pathos.path.join(output_dir,fpart_{chunk_id:04d})downloaded0headers{Range:fbytes{start}-{end}}forattemptinrange(max_retries):try:resprequests.get(url,headersheaders,streamTrue,timeout30)ifresp.status_codenotin(200,206):raiseException(f非预期状态码:{resp.status_code})withopen(chunk_path,wb)asf:fordatainresp.iter_content(chunk_size65536):ifdata:f.write(data)downloadedlen(data)progress_dict[chunk_id]downloaded# 验证下载大小expectedend-start1ifdownloaded!expected:raiseException(f大小不匹配: expected{expected}, got{downloaded})returnexceptExceptionase:print(f[chunk{chunk_id}] 第{attempt1}次失败:{e})ifattemptmax_retries-1:time.sleep(2**attempt)# 指数退避else:raise关键细节streamTrue防止一次性加载整个分片到内存——如果分片有1GB内存直接炸iter_content(chunk_size65536)以64KB为单位流式写入磁盘内存占用始终可控重试指数退避2s/4s/8s给服务器恢复时间下载完成后校验字节数防止网络传输截断4. 合并模块—— 所有分片完成后按顺序拼接同时清理临时分片defmerge_chunks(output_path,total_chunks,output_dir):withopen(output_path,wb)asoutfile:foriinrange(total_chunks):part_pathos.path.join(output_dir,fpart_{i:04d})withopen(part_path,rb)asinfile:whileTrue:chunkinfile.read(4194304)# 4MB缓冲减少系统调用ifnotchunk:breakoutfile.write(chunk)os.remove(part_path)# 合并后立即清理三、断点续传把进度持久化上面的代码有致命缺陷——下载到一半程序崩溃所有分片全废。生产级的下载器必须把进度写到磁盘。思路是在每个分片文件的文件名里编码它的起止位置下载中断后通过扫描分片目录恢复进度defresume_download(url,output_path,num_threads,output_dir):file_size,supports_rangeprobe(url)ifnotsupports_range:# 服务器不支持分段老老实实单线程download_single(url,output_path)return# 检查是否有未完成的分片completed_chunks{}forfinos.listdir(output_dir):iff.startswith(part_):chunk_idint(f.split(_)[1])completed_chunks[chunk_id]os.path.getsize(os.path.join(output_dir,f))chunkssplit_chunks(file_size,num_threads)pending[]forchunk_id,start,endinchunks:ifchunk_idincompleted_chunks:actualcompleted_chunks[chunk_id]expectedend-start1ifactualexpected:continue# 已完成跳过else:# 只下载未完成的部分pending.append((chunk_id,startactual,end))else:pending.append((chunk_id,start,end))# 只对未完成的分片启动下载线程threads[]progress{}forchunk_id,start,endinpending:tthreading.Thread(targetdownload_chunk,args(url,chunk_id,start,end,output_dir,progress))t.start()threads.append(t)fortinthreads:t.join()merge_chunks(output_path,num_threads,output_dir)即使下载到一半强行kill进程下次启动时已完成的字节不会重新下载。四、线程数到底设多少一个常见误区是线程越多越快。同一台机器电信200Mbps对同一个CDN直链实测线程数平均速度带宽利用率说明13.1 MB/s12.4%单线程性能瓶颈明显414.8 MB/s59.2%提升显著821.5 MB/s86.0%性价比最优1622.7 MB/s90.8%增幅开始衰减3222.9 MB/s91.6%几乎无增益8线程之后曲线接近持平。原因在服务器端——CDN通常对每个客户端IP有并发连接数的软限制。实际应用中8线程是最佳实践。另外小文件小于10MB多线程反而更慢——分片调度和合并的额外开销会吃掉提速收益iffile_size10*1024*1024:num_threads1# 小文件不拆分五、完整入口把上面的模块串起来defdownload(url,output_path,num_threads8):output_diroutput_path.partsos.makedirs(output_dir,exist_okTrue)try:resume_download(url,output_path,num_threads,output_dir)finally:ifos.path.exists(output_dir)andnotos.listdir(output_dir):os.rmdir(output_dir)# 清理空目录六、不想造轮子的选择理解原理的目的是在选工具的时候知其所以然。成熟的下载管理器——比如FDM——就是把上面这些协议细节和工程优化全部封装好同时还额外实现了FTP协议栈和BitTorrent协议栈DHTPEX磁力链接一个工具覆盖HTTP/FTP/BT三种场景。FDM的源码在GitHub开源GPL协议HttpDownloader类、DhtEngine类、ChunkMerger类的实现值得细读。安装包从 fdm.ijinshan.com 下载——国内分发地址速度比海外官网快。这篇文章读完你理解了HTTP Range的协议细节、分片调度策略、断点续传的持久化方案、线程数选择依据以及什么时候不该用多线程。不管最终用不用FDM这些知识在任何大文件下载场景里都用得上。