最近在折腾分段下载,把踩过的坑整理一下,给后来的人省点时间。
### 一、Range 不是所有服务器都认
普通请求是整包给:
```
GET /file.zip HTTP/1.1
Host: example.com
```
要分段就得带 Range:
```
GET /file.zip HTTP/1.1
Host: example.com
Range: bytes=1048576-
```
服务器支持的话返回 `206 Partial Content`,并且带 `Content-Range` 告诉你这段在文件里的位置和总长:
```
Content-Range: bytes 1048576-10485759/10485760
```
探测只需要一个字节:
```bash
curl -sI -H "Range: bytes=0-1023" https://example.com/file.zip | head -3
# 支持 → HTTP/1.1 206 Partial Content
# 不支持 → HTTP/1.1 200 OK
```
返回 200 的站只能整包下,别浪费时间。
### 二、别信 Content-Length
遇到过一台服务器头部写 9.4MB,实际只发 9.1MB 就关连接。程序认为下载完成,实际文件是截断的。
所以每个分片下完都要比对实际字节数,少了就重试**这一片**,而不是整包重下:
```javascript
const expect = part.to - part.from + 1 - part.got;
if (buf.length !== expect) {
if (buf.length > 0) {
await fh.write(buf, 0, buf.length, part.from + part.got);
part.got += buf.length;
}
throw new Error('short:' + buf.length + '/' + expect);
}
```
上层捕获到 `short:` 就把这片放回队列,其它错误才退避重试。
### 三、分片数 ≠ 并发数
一开始按 4MB 切片,20G 的文件切成 5000 片,然后想开 5000 并发。结果 IP 被限流,比单线程还慢。
正确的做法是:片切细(决定并行粒度),worker 数量固定(决定同时开几条连接):
```javascript
async function runPool(parts, workers, worker) {
let cursor = 0;
const runners = Array.from({ length: workers }, async () => {
while (cursor < parts.length) {
const part = parts[cursor++];
if (part.got >= part.to - part.from + 1) continue;
try { await worker(part); }
catch (e) {
if (!/^short:/.test(e.message)) {
parts.push(part);
await new Promise((r) => setTimeout(r, 500));
}
}
}
});
await Promise.all(runners);
}
```
8~16 个 worker 一般比较稳,再多容易触发单 IP 限制。
### 四、并行写文件:千万别用 append
这是我犯过最蠢的错。多个分片并行下载,用 `appendFile` 写,等于按到达顺序拼接 —— **文件大小对,字节顺序全乱**。校验能过,打开报错。
正解是打开一次文件句柄,每片带 position 写:
```javascript
const fh = await open(tmpPath, 'r+'); // r+ 不能是 w,w 会清空已下的内容
await fh.write(buf, 0, buf.length, offset);
```
### 五、进度状态要原子落盘
直接 `writeFileSync` 覆盖进度文件,断电就得到半个 JSON,`JSON.parse` 抛错,续传全废。
用「临时文件 + rename」:
```javascript
await writeFile(tmp, JSON.stringify(state), 'utf8');
await rename(tmp, path); // 同分区是原子操作
```
还有一步不能省:**恢复时比对 ETag**。存的 ETag 和重新探测的不一致,说明服务器上文件换了,必须丢弃全部进度重下,否则会得到「前半段旧文件 + 后半段新文件」这种垃圾。
### 六、几个真实的服务器行为
| 现象 | 原因 | 处理 |
|---|---|---|
| 返回 206 但内容是整包 | 服务器忽略 Range 仍回 206 | 比对 Content-Range 起点 |
| 下到一半卡死不动 | 连接空闲超时,不报错不断开 | 加读超时,超时重连同一片 |
| 开 64 条被拒 | 单 IP 并发限制 | 降到 8,失败指数退避 |
| 只有 md5 没有 sha256 | 站点只给 md5 | 校验层支持多种,没值就比字节数 |
---
不知道有没有人也遇到过「206 但其实给了整包」这种情况,我遇到两三个站是这样,一直没搞清是 CDN 的锅还是源站配置问题。有知道的可以说说。
---
---
【上面这些坑都是我真实踩过的,写法各有取舍,按你的场景选就行。】
有坑直接评论区说,我一条条看。