断点续传功能在网络文件传输中非常实用,尤其是大文件在网络不稳定的环境下传输时,如果每次中断都从头开始,会极大浪费带宽和时间。用C++实现这一能力,核心在于准确记录已经传输完成的字节偏移量,并在恢复时将该偏移量作为新的起点继续读写。本文从文件偏移记录、传输逻辑设计以及异常处理三个角度,详细说明如何用标准C++完成一个可恢复的传输模块。

文件传输位置的记录与持久化
实现断点续传的第一步,是把当前已经传输的字节数保存到非易失介质中。最直观的做法是维护一个进度文件,例如 progress.dat,里面只存放一个 long long 类型的整数,表示源文件已经成功发送或接收的字节位置。程序启动时要先尝试读取这个文件,如果读取成功且数值合法,就把它作为文件流的起始偏移;如果文件不存在或者内容损坏,就从零开始传输。
在C++里可以用 std::fstream 以二进制模式打开进度文件,写入时采用 seekp 定位到开头再写,避免追加导致数据变长。为了防止进度文件和主文件状态不一致,建议在每次成功写入网络缓冲区或落盘后再更新进度值,而不是提前更新。下面是一个简单的进度读写示例:
#include <fstream>
#include <iostream>
bool save_progress(const std::string& path, long long offset) {
std::fstream fs(path, std::ios::out | std::ios::binary | std::ios::trunc);
if (!fs) return false;
fs.write(reinterpret_cast<const char*>(&offset), sizeof(offset));
fs.flush();
return true;
}
long long load_progress(const std::string& path) {
std::fstream fs(path, std::ios::in | std::ios::binary);
if (!fs) return 0;
long long offset = 0;
fs.read(reinterpret_cast<char*>(&offset), sizeof(offset));
if (fs.gcount() != sizeof(offset)) return 0;
return offset;
}
上面的代码把偏移量以二进制形式写入文件,比用文本方式更安全,不会因为区域设置或换行符产生解析错误。在实际项目中,还可以给进度文件加上简单的校验和,比如把偏移量写两遍,读取时比较是否相等,从而发现断电导致的半截写入问题。
另一个容易被忽略的点是进度刷盘频率。如果每传一个网络包都写一次磁盘,机械硬盘或低端嵌入式设备上的IO开销会非常明显。更合理的方案是由独立线程每隔几秒将内存中的最新偏移量写入文件,或者在传输暂停、正常退出时强制保存。这样即使程序异常崩溃,也只会损失最近几秒的进度,而不是全部重来。
基于偏移量的文件读写与传输恢复
记录下偏移量之后,真正的文件操作需要使用 std::ifstream 和 std::ofstream 的 seekg 与 seekp 方法定位。对于发送端,用二进制方式打开源文件,调用 seekg(offset, std::ios::beg) 跳到断点位置再开始读;对于接收端,用 seekp(offset, std::ios::beg) 在已有文件上继续写。注意接收端打开文件时不能用 ios::trunc,否则之前的内容会被清空。
下面是一段发送端从断点读取并模拟发送的代码,实际网络部分可以替换成 socket 或 HTTP 库:
#include <fstream>
#include <vector>
void send_from_offset(const std::string& file, long long offset) {
std::ifstream in(file, std::ios::in | std::ios::binary);
if (!in) return;
in.seekg(offset, std::ios::beg);
char buf[4096];
while (in) {
in.read(buf, sizeof(buf));
std::streamsize len = in.gcount();
if (len > 0) {
// 这里把 buf 通过 socket 发送出去
// send(sock, buf, len, 0);
offset += len;
save_progress("progress.dat", offset);
}
}
}
如果传输协议是HTTP,可以构造 Range 请求头,例如 Range: bytes=offset-,服务端会只返回剩余部分。此时本地记录的位置要和服务端确认返回的状态码是否为 206 Partial Content,并且核对响应中的 Content-Range 是否和本地偏移一致,防止中间代理篡改了范围。
在恢复传输时还要处理文件被删除或截断的情况。如果本地记录偏移量大于当前文件实际大小,说明源文件可能已被修改,应当放弃断点并提示用户重新选择文件。可以在恢复前用 std::filesystem::file_size 获取真实大小并与偏移比较,这样能避免读出非法数据或陷入死循环。
异常场景与数据一致性保障
断点续传并不是简单记下数字就够了,在异常场景下很容易出现进度文件和实际数据不匹配。比如网络断了但进度已经写了,而接收端其实没收到最后一块,这时如果以记录值为准继续传,文件就会缺一段。解决办法是采用“先确认后记账”的顺序:接收端落盘成功并回包确认,发送端收到确认才更新进度文件,任何一步失败都保持旧偏移。
另外一个常见误区是多个进程同时传输同一个文件却共用一个进度文件。如果没有文件锁,两个进程可能交叉写入偏移量,导致彼此覆盖。在Linux下可以用 flock,Windows下可以用 LockFileEx 对进度文件加锁;或者更简单地让每个任务拥有独立的进度文件名,例如用源文件路径的哈希值命名,从设计上避开并发冲突。
#include <filesystem>
#include <string>
std::string progress_name(const std::string& file) {
std::size_t h = std::hash<std::string>{}(file);
return "prog_" + std::to_string(h) + ".dat";
}
// 使用 progress_name(file) 代替固定 "progress.dat"
最后还要考虑程序被强制结束时的收尾。如果只在正常退出时保存进度,那么 kill -9 或断电就会丢失内存里最新的偏移。前面提到的定时刷盘线程就是为此而设,同时可以在每次网络写完成一个完整缓冲区后,顺带把偏移写进内存变量,由刷盘线程负责落地。通过这种分层设计,既保证了性能,也把数据丢失窗口控制在可接受范围内,从而让C++编写的传输工具具备真正可靠的断点续传能力。