断点续传的核心逻辑是记录已下载的文件片段,在网络中断后重新发起请求时,只获取未下载的部分并追加到本地文件中,避免重复传输。实现这一功能主要依赖HTTP协议的Range请求头,通过指定请求的字节范围来获取文件的分段数据。

断点续传的核心原理
HTTP协议中,客户端可以通过在请求头中添加Range字段指定需要获取的字节范围,格式为Range: bytes=start-end,其中start是起始字节偏移量,end是结束字节偏移量,若省略end则表示请求到文件末尾。服务端返回状态码为206 Partial Content,响应头中会包含Content-Range字段说明当前返回的字节范围,同时Accept-Ranges: bytes表示服务端支持字节范围请求。
实现断点续传需要完成以下步骤:
- 首次下载时向服务端请求文件总长度,创建本地临时文件记录下载信息
- 将文件分成多个片段并行或串行下载,每个片段对应一个Range请求
- 每下载完一个片段就写入本地文件对应位置,更新已下载的字节偏移量
- 下载中断后重新启动,读取本地记录的已下载偏移量,跳过已下载部分继续请求剩余内容
模块整体架构设计
整个下载模块分为三个核心模块:
网络请求模块
负责构造HTTP请求,发送Range请求头,接收服务端返回的数据。可以使用C++的socket接口或者封装好的网络库如libcurl实现,本文示例使用socket接口实现基础的HTTP请求逻辑。
文件操作模块
负责本地文件的创建、写入、偏移量设置,以及下载状态文件(记录已下载字节、文件总大小、下载URL等信息)的读写。使用C++标准库的fstream实现文件操作。
状态管理模块
负责维护下载的进度信息,包括已下载的字节数、文件总大小、每个分片的下载状态,支持将状态持久化到本地,方便中断后恢复。
完整实现代码示例
以下是使用C++ socket实现的基础断点续传下载模块代码,包含核心功能逻辑:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
// 下载状态结构体
struct DownloadState {
std::string url;
std::string fileName;
long totalSize;
long downloadedSize;
};
// 解析URL获取主机和路径
bool parseUrl(const std::string& url, std::string& host, std::string& path) {
if (url.find("http://") != 0) {
return false;
}
size_t hostStart = 7;
size_t pathStart = url.find('/', hostStart);
if (pathStart == std::string::npos) {
host = url.substr(hostStart);
path = "/";
} else {
host = url.substr(hostStart, pathStart - hostStart);
path = url.substr(pathStart);
}
return true;
}
// 创建HTTP Range请求头
std::string buildRangeRequest(const std::string& host, const std::string& path, long start, long end) {
std::string request = "GET " + path + " HTTP/1.1rn";
request += "Host: " + host + "rn";
request += "Range: bytes=" + std::to_string(start) + "-" + (end == -1 ? "" : std::to_string(end)) + "rn";
request += "Connection: closernrn";
return request;
}
// 获取文件总大小
long getFileTotalSize(const std::string& host, const std::string& path) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
return -1;
}
hostent* hp = gethostbyname(host.c_str());
if (!hp) {
close(sock);
return -1;
}
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
memcpy(&addr.sin_addr, hp->h_addr, hp->h_length);
addr.sin_port = htons(80);
if (connect(sock, (sockaddr*)&addr, sizeof(addr)) < 0) {
close(sock);
return -1;
}
std::string request = "HEAD " + path + " HTTP/1.1rnHost: " + host + "rnConnection: closernrn";
send(sock, request.c_str(), request.size(), 0);
char buffer[1024];
std::string response;
while (true) {
int len = recv(sock, buffer, sizeof(buffer)-1, 0);
if (len <= 0) break;
buffer[len] = 0;
response += buffer;
}
close(sock);
// 解析Content-Length
size_t pos = response.find("Content-Length:");
if (pos == std::string::npos) return -1;
pos += 15;
while (response[pos] == ' ') pos++;
size_t endPos = response.find("rn", pos);
std::string sizeStr = response.substr(pos, endPos - pos);
return std::stol(sizeStr);
}
// 下载指定范围的内容并写入文件
bool downloadRange(const std::string& host, const std::string& path, long start, long end, const std::string& fileName, long totalSize) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
return false;
}
hostent* hp = gethostbyname(host.c_str());
if (!hp) {
close(sock);
return false;
}
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
memcpy(&addr.sin_addr, hp->h_addr, hp->h_length);
addr.sin_port = htons(80);
if (connect(sock, (sockaddr*)&addr, sizeof(addr)) < 0) {
close(sock);
return false;
}
std::string request = buildRangeRequest(host, path, start, end);
send(sock, request.c_str(), request.size(), 0);
// 跳过响应头
char buffer[1024];
bool headerEnd = false;
std::string header;
while (!headerEnd) {
int len = recv(sock, buffer, sizeof(buffer)-1, 0);
if (len <= 0) break;
buffer[len] = 0;
header += buffer;
if (header.find("rnrn") != std::string::npos) {
headerEnd = true;
// 处理响应头后的剩余数据
size_t bodyStart = header.find("rnrn") + 4;
std::string initialBody = header.substr(bodyStart);
// 打开文件,定位到start位置写入
std::fstream file(fileName, std::ios::binary | std::ios::in | std::ios::out);
if (!file) {
file.open(fileName, std::ios::binary | std::ios::out);
}
file.seekp(start, std::ios::beg);
file.write(initialBody.c_str(), initialBody.size());
file.close();
}
}
// 接收剩余响应体数据
std::fstream file(fileName, std::ios::binary | std::ios::in | std::ios::out);
if (!file) {
close(sock);
return false;
}
file.seekp(start + (header.find("rnrn") != std::string::npos ? header.size() - header.find("rnrn") - 4 : 0), std::ios::beg);
while (true) {
int len = recv(sock, buffer, sizeof(buffer)-1, 0);
if (len <= 0) break;
file.write(buffer, len);
}
file.close();
close(sock);
return true;
}
// 保存下载状态
void saveState(const DownloadState& state, const std::string& stateFile) {
std::ofstream file(stateFile);
file << state.url << std::endl;
file << state.fileName << std::endl;
file << state.totalSize << std::endl;
file << state.downloadedSize << std::endl;
file.close();
}
// 加载下载状态
bool loadState(DownloadState& state, const std::string& stateFile) {
std::ifstream file(stateFile);
if (!file) return false;
std::getline(file, state.url);
std::getline(file, state.fileName);
file >> state.totalSize;
file >> state.downloadedSize;
file.close();
return true;
}
// 主下载函数
bool downloadWithResume(const std::string& url, const std::string& savePath) {
std::string host, path;
if (!parseUrl(url, host, path)) {
std::cout << "URL解析失败" << std::endl;
return false;
}
std::string stateFile = savePath + ".state";
DownloadState state;
long startPos = 0;
if (loadState(state, stateFile)) {
// 状态文件存在,恢复下载
startPos = state.downloadedSize;
std::cout << "恢复下载,已下载:" << startPos << "字节" << std::endl;
} else {
// 首次下载,获取文件总大小
long totalSize = getFileTotalSize(host, path);
if (totalSize <= 0) {
std::cout << "获取文件大小失败" << std::endl;
return false;
}
state.url = url;
state.fileName = savePath;
state.totalSize = totalSize;
state.downloadedSize = 0;
startPos = 0;
std::cout << "开始下载,文件总大小:" << totalSize << "字节" << std::endl;
}
// 每次下载1MB片段
const long chunkSize = 1024 * 1024;
while (startPos < state.totalSize) {
long endPos = std::min(startPos + chunkSize - 1, state.totalSize - 1);
if (downloadRange(host, path, startPos, endPos, savePath, state.totalSize)) {
startPos = endPos + 1;
state.downloadedSize = startPos;
saveState(state, stateFile);
std::cout << "已下载:" << startPos << "/" << state.totalSize << "字节" << std::endl;
} else {
std::cout << "下载片段失败,等待重试" << std::endl;
sleep(1);
}
}
// 下载完成后删除状态文件
remove(stateFile.c_str());
std::cout << "下载完成" << std::endl;
return true;
}
int main() {
std::string url = "http://ipipp.com/testfile.zip";
std::string savePath = "testfile.zip";
downloadWithResume(url, savePath);
return 0;
}
实现注意事项
实际使用中需要注意以下几点:
- 部分服务端可能不支持Range请求,需要在请求前通过HEAD请求检查
Accept-Ranges响应头,若不支持则降级为普通全量下载 - 网络请求部分需要处理超时、重试逻辑,避免网络波动导致下载直接失败
- 若采用多片段并行下载,需要注意文件写入的线程安全,避免多个线程同时写入同一文件区域造成数据错乱
- 下载状态文件需要定期更新,避免程序异常退出后状态丢失过多进度
- 大文件下载时建议分片大小设置为1MB到10MB之间,过小会导致请求次数过多,过大会增加重试的成本