一句话总结
HTTP 文件下载本质上就是一次 GET 请求 → 200 OK + 文件 Body;而断点续传则是把"一次取整个文件"拆成"多次取其中一段",核心靠三个东西:
Range 请求头
告诉服务器"我想要哪一段字节"。
206 Partial Content
服务器告诉客户端"我只给你这一段"。
If-Range / ETag
续传前先校验"文件还是原来那个吗"。
读懂这三件事,再加上 Accept-Ranges 与 Content-Range 这两个辅助头,断点续传和多线程下载就完全透明了。
一、普通文件下载原理
1.1 最朴素的过程:GET → 200 OK + Body
浏览器、curl、wget 下载一个文件,最朴素的流程如下图所示:客户端发 GET 请求,服务器返回 200 OK 状态码,并把文件内容放在响应体里一次性发完。
1.2 真实报文:客户端到底发了什么、收到了什么?
下面是一段典型的"下载一个 zip 包"的完整 HTTP 交互:
1.3 关键响应头逐个拆解
| 响应头 | 作用 | 为什么重要 |
|---|---|---|
| Content-Type | 资源的 MIME 类型,如 application/zip、image/png | 浏览器据此选择"显示"还是"下载"。 |
| Content-Length | 整个响应体的字节数 | 客户端用它算进度条;也是校验文件是否完整的关键。 |
| Content-Disposition | inline 表示直接展示;attachment 表示下载 | attachment; filename="x.zip" 是触发浏览器下载框的关键。 |
| Accept-Ranges | 服务器声明支持的范围单位(bytes / none) | 值为 bytes 才支持断点续传;none 表示不支持。 |
| ETag / Last-Modified | 资源的"指纹"与最后修改时间 | 续传前用它校验"服务器上的文件没被换过"。 |
1.4 Content-Disposition:浏览器是"打开"还是"保存"?
同一个 URL,两种响应头,浏览器表现天差地别:
📄 Content-Type: image/png
(不设 Content-Disposition)
浏览器默认 inline:直接把图片渲染到页面上,不会弹下载框。
💾 Content-Disposition: attachment; filename="pic.png"
浏览器强制 下载:弹出"保存到..."对话框,文件名取自 filename。
二、断点续传原理(HTTP Range)
2.1 一句话解释 Range
普通 GET 拉的是"整个文件";如果请求里多了一行 Range: bytes=start-end,服务器就只会返回这一段字节,其余丢弃——这就是断点续传的全部秘密。
2.2 Range 请求头的几种写法
| 写法 | 含义 | 典型用途 |
|---|---|---|
Range: bytes=0-499 |
取前 500 个字节(0~499) | 下载器取第 1 片 |
Range: bytes=500-999 |
取 500~999 这一段 | 下载器取第 2 片 |
Range: bytes=500- |
从 500 取到文件末尾 | 断点续传最常用 |
Range: bytes=-100 |
取最后 100 个字节 | 快速探测文件大小 / 尾部哈希 |
2.3 正常返回:206 Partial Content
服务器如果支持 Range,就会把状态码从 200 OK 换成 206 Partial Content,并用 Content-Range 告诉你"这段在整个文件里的位置"。
bytes {start}-{end}/{total}。其中 total 是文件的完整大小,end 是这一段的最后一个字节下标(闭区间)。例如 52428800-104857599/104857600 表示从第 50MB 字节到第 100MB-1 字节,共 50MB,整文件 100MB。
2.4 服务器不支持 Range:416 Range Not Satisfiable
200 OK + 完整文件,或者 416 Range Not Satisfiable。这时断点续传就无法工作。
所以下载器一般的做法是:先用 HEAD 请求探一下,看到 Accept-Ranges: bytes 才会启用多线程下载或断点续传。
三、多线程分片下载:Range 的进阶玩法
3.1 迅雷 / IDM / Aria2 的核心套路
迅雷、IDM 这类下载器之所以比浏览器单线程快,本质就是把文件分成 N 段,开 N 个连接并发拉,最后拼起来:
3.2 分片下载的客户端拼装逻辑
- 探测文件大小与是否支持 Range。用
HEAD请求读Content-Length和Accept-Ranges。如果不支持 Range,就只能单线程下。 - 把文件按 N 等分切片。100MB 文件,4 线程就每片 25MB;8 线程就每片 12.5MB。
- 开 N 个并发连接。每条连接带不同的
Range: bytes=A-B请求对应切片。 - 每片单独写到一个临时文件。通常命名为
xxx.zip.0、xxx.zip.1… 这样某一片失败重下不会影响其它片。 - 每一片边下边校验长度。如果收到字节数不等于预期 (B-A+1),就当失败重试。
- 所有片下完之后按顺序拼接。用
cat xxx.zip.* > xxx.zip或在内存里流式拼接,最后做整体哈希校验。
四、续传前如何校验文件没变:If-Range
4.1 问题:服务器的文件换了怎么办?
断点续传有一个隐藏陷阱:你在客户端下到一半时,服务器上的文件可能被替换了(比如用户重新上传了一个同名文件,或者 CDN 源站更新)。此时如果还按偏移去取,可能拿到错位的字节,拼出来的东西会损坏。
4.2 If-Range 的两种语义
| If-Range 值 | 比对依据 | 命中(文件未变) | 未命中(文件已变) |
|---|---|---|---|
If-Range: "abc123" |
ETag 字符串 | 返回 206 Partial Content | 返回 200 OK + 完整新文件 |
If-Range: Wed, 03 Sep 2025 12:00:00 GMT |
Last-Modified 时间 | 返回 206 Partial Content | 返回 200 OK + 完整新文件 |
4.3 完整续传流程(含校验)
五、实战案例:断网后如何恢复下载
把这套机制串起来,看一次完整的"网络抖动 → 自动续传"的时序:
O_APPEND 或 seek 到偏移位置写)。如果只是攒在内存里,断电时数据全部丢失,没法续传。
六、常见问题与最佳实践
Q1:为什么我用浏览器下载大文件失败率高?
浏览器默认是单连接、不支持断点续传的。一旦 TCP 连接中途断开,整个下载任务就被浏览器当作"失败"丢掉,本地的缓存也清掉。专业下载器(IDM、迅雷、Aria2)则在收到每一个 chunk 后立刻 fsync 到磁盘,所以即使断网,下次启动还能从断点续上。
Q2:服务器如何启用 Range 支持?
主流 HTTP 服务器默认就支持:
| 服务器 | 配置方式 |
|---|---|
| Nginx | 默认支持,关闭用 max_ranges 0; |
| Apache | 默认支持,模块 mod_range |
| Tomcat | 默认支持,需 servlet 设置 Content-Length |
| CDN (CloudFront/Cloudflare) | 默认透传 Range |
| S3 / OSS | 默认支持 Range 请求 |
Q3:客户端最简单的实现示例(Python)
Q4:分片越多越快吗?
不是。一般 4~16 路并发是性价比最高的甜蜜点:
- 连接太少 → 拼不满带宽;
- 连接太多 → 服务器端并发压力大、TCP 握手/慢启动开销变大、磁盘 IO 反而成为瓶颈。
很多 CDN 还会限制单 IP 的最大并发连接数(比如 16),开多了会被 429 拒绝。
Q5:断点续传和"分片上传"是一回事吗?
完全相反:
📥 断点续传(下载)
客户端用 Range 请求文件的一部分;服务器返回 206。本地拼装。
📤 分片上传(上传)
客户端把文件切成多片,多次 POST/PUT 传给服务器,服务器合并。一般配合预签名 URL + ETag 实现。
Q6:HTTPS 下 Range 还能用吗?
完全能用。TLS 只加密传输层,对 HTTP 头里的 Range 一无所知;服务器依然会根据 Range 决定返回 206 还是 200。一些 CDN 还会在 TLS 握手前就检查 Range 头是否合法。
七、一张图总结
普通下载 =
GET /file → 200 OK + 完整 Body断点续传 =
GET /file + Range: bytes=A- + If-Range: ETag → 206 Partial Content + 切片 Body多线程下载 = 同时发 N 个 Range 请求,N 个 206 响应,本地拼接。