断点下载的核心逻辑是利用fstream::seekp定位写入位置,需以ios::in|ios::out|ios::binary模式打开文件且不加ios::trunc,调用seekp前确认流状态为good,写入后检查fail()并flush;空文件或偏移超长时seekp易失效,建议预分配文件大小或用ios::ate+seekp(0,ios::end)获取长度;多线程分块下载须严格对齐content-range偏移与长度,并独占文件句柄或加锁;高可靠性场景推荐系统调用替代fstream。

断点下载的核心逻辑:用 fstream::seekp 定位写入位置
断点下载不是靠“续传协议”,而是靠本地文件已存在的字节长度决定从哪开始写。关键在于打开文件时不能清空(即不能用 ios::trunc),且必须用 ios::in | ios::out | ios::binary 模式,才能支持读写并定位。调用 seekp() 前,务必确认流处于 good 状态;否则 seekp() 会静默失败——这是最常被忽略的坑。
常见错误现象:write() 总是从文件开头覆盖、或写入后文件大小没变、或 seekp(n) 后实际写入位置仍是 0。
- 打开前先
stat()或file.tellg()获取当前大小(即已下载字节数) - 用
fstream file(path, ios::in | ios::out | ios::binary)打开,不加ios::trunc - 调用
file.seekp(offset)后,立刻检查file.fail()——若为 true,说明 offset 超出当前文件大小且未启用扩展(部分平台不自动扩容) - 写入前确保
file.write(buf, n)的n是实际接收字节数,别直接用缓冲区大小
为什么 seekp 在空文件或小文件里经常失效
seekp() 对空文件(size=0)或目标偏移大于当前文件长度时,行为依赖底层 OS 和 C++ 标准库实现。POSIX 系统下,seekp(1024) 到一个 100 字节的文件,不会自动填充 0x00 补齐到 1024;它只是把写位置设为 1024,但下次 write() 仍会从该位置开始——如果文件系统不支持稀疏写,或流未设置 ios::ate,就可能触发 failbit。
实战建议:
- 下载前先创建空文件并
resize到预估总长(如 HTTP Header 中的Content-Length),可用truncate()系统调用(Linux/macOS)或SetFilePointerEx() + SetEndOfFile()(Windows) - 或者更稳妥:用
ios::ate打开,再seekp(0, ios::end)获取当前长度,然后seekp(offset)——这比直接seekp(offset)更可靠 - 每次写入后调用
file.flush(),避免缓冲区延迟导致后续断点位置误判
合并分块下载文件时,seekp 和偏移计算要严格对齐
多线程分块下载(如 0–999KB、1000–1999KB)后合并,本质是把每块数据按序写入同一文件的指定区间。这里容易错的是偏移单位和边界:HTTP Range 是字节偏移,seekp() 参数也是字节,但必须确保每个块的起始 offset 和长度完全匹配服务端返回的 Content-Range 头(例如 bytes 1000-1999/10000 表示本块应写入 offset=1000,长度=1000)。
典型陷阱:
- 用
std::stoi解析Content-Range时没跳过bytes前缀,导致 offset 解析失败 - 写入长度用
recv_len而非range_end - range_start + 1,网络丢包或服务端截断时会导致后续块错位 - 多个线程共用一个
fstream对象 ——seekp()和write()非原子,必须加锁或每个线程独占文件句柄 - Windows 下若文件以
ios::binary打开但写入含 \n 的文本内容,不会自动转 \r\n,这点反而有利于二进制合并
实测发现:fstream 在大文件断点续传中比 FILE* 更难控制
标准 fstream 对 seek/write 的错误反馈太弱,fail() 可能只在第二次操作才置位;而 fseek()/fwrite() 配合 ferror() 能更快暴露问题。尤其在 2GB+ 文件上,某些 libc 实现对 seekp(>2GB) 返回无符号 long long 偏移时存在截断风险。
如果你的场景需要高可靠性(比如下载固件镜像),建议:
- 优先用
open()/lseek()/write()(Linux/macOS)或CreateFile()/SetFilePointerEx()/WriteFile()(Windows)替代fstream - 若坚持用
fstream,务必用file.clear()在每次seekp()后重置状态,并用file.tellp()反查当前位置是否等于预期 - 不要依赖
file.good()作为写入成功的依据,它不检查磁盘满、权限不足等系统级错误
偏移计算和 seekp 调用本身很简单,真正的复杂性藏在文件系统语义、C++ 流状态机与跨平台行为差异里。动手前,先用 10MB 小文件跑通完整流程,再放大到 GB 级——很多问题只在大文件稀疏写或磁盘满时才暴露。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











