手把手教你写一个断点续传下载器:HTTP Range 请求实战
前言你有没有遇到过这种情况下载一个大文件进度走到90%网络突然断了。重新下载又要从头开始几个小时白等了。如果下载器支持断点续传就可以从断掉的地方继续下载省时省力。今天我们用C语言手写一个支持断点续传的命令行下载器彻底搞懂HTTP Range请求的原理和实现。---一、断点续传的核心原理1. HTTP Range 请求断点续传依赖HTTP协议的一个特性Range请求。客户端可以在请求头中告诉服务器“我已经下载了前1000个字节请从第1001个字节开始发送。”GET /bigfile.zip HTTP/1.1Host: example.comRange: bytes1000-服务器如果支持Range会返回HTTP/1.1 206 Partial ContentContent-Range: bytes 1000-199999/200000Content-Length: 199000关键状态码是 206 Partial Content不是200。2. 断点续传的流程┌─────────────┐│ 开始下载 │└──────┬──────┘│▼┌─────────────┐│ 检查本地文件│ ← 如果已存在获取已下载的大小└──────┬──────┘│▼┌─────────────┐│ 发送Range │ ← 告诉服务器从哪个位置开始│ 请求 │└──────┬──────┘│▼┌─────────────┐│ 追加写入 │ ← 把新数据追加到文件末尾│ 文件 │└──────┬──────┘│▼┌─────────────┐│ 下载完成 │└─────────────┘---二、完整代码实现c#include stdio.h#include stdlib.h#include string.h#include unistd.h#include sys/socket.h#include netdb.h#include arpa/inet.h#include sys/stat.h#define BUFFER_SIZE 4096#define USER_AGENT RangeDownloader/1.0// 解析URL提取主机名、端口、路径int parse_url(const char *url, char *host, int *port, char *path) {// 跳过 http://if (strncmp(url, http://, 7) ! 0) {fprintf(stderr, 只支持 http 协议\n);return -1;}const char *start url 7;const char *slash strchr(start, /);if (!slash) {strcpy(path, /);*port 80;} else {strncpy(path, slash, 255);path[255] \0;}// 提取主机名int host_len slash - start;strncpy(host, start, host_len);host[host_len] \0;// 检查端口char *colon strchr(host, :);if (colon) {*colon \0;*port atoi(colon 1);} else {*port 80;}return 0;}// 获取本地已下载的文件大小long long get_local_size(const char *filename) {struct stat st;if (stat(filename, st) 0) {return st.st_size;}return 0;}// 发送HTTP请求设置Range头int send_request(int sock, const char *host, const char *path,long long start_pos) {char request[1024];if (start_pos 0) {// 断点续传带Range头snprintf(request, sizeof(request),GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nRange: bytes%lld-\r\nConnection: close\r\n\r\n,path, host, USER_AGENT, start_pos);} else {// 全新下载不带Rangesnprintf(request, sizeof(request),GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: close\r\n\r\n,path, host, USER_AGENT);}return send(sock, request, strlen(request), 0);}// 解析响应头获取状态码和文件总大小int parse_response_header(int sock, long long *total_size,int *is_partial) {char buffer[BUFFER_SIZE];char *line buffer;int bytes_read 0;int header_end 0;*total_size -1;*is_partial 0;// 读取响应头while (!header_end) {int n recv(sock, buffer bytes_read, 1, 0);if (n 0) return -1;bytes_read;// 检查是否遇到空行\r\n\r\nif (bytes_read 4 buffer[bytes_read-4] \r buffer[bytes_read-3] \n buffer[bytes_read-2] \r buffer[bytes_read-1] \n) {header_end 1;}}buffer[bytes_read] \0;// 解析状态码if (strstr(buffer, 206 Partial Content)) {*is_partial 1;} else if (strstr(buffer, 200 OK)) {*is_partial 0;} else {fprintf(stderr, 服务器返回错误:\n%s\n, buffer);return -1;}// 解析 Content-Range 或 Content-Lengthchar *range_ptr strstr(buffer, Content-Range:);if (range_ptr) {// 格式Content-Range: bytes 0-199999/200000char *slash strchr(range_ptr, /);if (slash) {*total_size atoll(slash 1);}} else {char *len_ptr strstr(buffer, Content-Length:);if (len_ptr) {*total_size atoll(len_ptr 15);}}return 0;}// 下载文件主体内容int download_content(int sock, FILE *file, long long start_pos,long long total_size, long long *downloaded) {char buffer[BUFFER_SIZE];long long written 0;// 如果是从中间开始先跳到文件末尾if (start_pos 0) {fseek(file, 0, SEEK_END);written start_pos;}while (1) {int n recv(sock, buffer, BUFFER_SIZE, 0);if (n 0) break;fwrite(buffer, 1, n, file);written n;*downloaded written;// 显示进度每1%打印一次if (total_size 0) {int percent (int)(written * 100 / total_size);static int last_percent -1;if (percent ! last_percent percent % 5 0) {printf(\r下载进度: %d%% (%lld / %lld bytes),percent, written, total_size);fflush(stdout);last_percent percent;}}}printf(\n);return 0;}// 主函数int main(int argc, char *argv[]) {if (argc 2) {fprintf(stderr, 用法: %s URL [文件名]\n, argv[0]);fprintf(stderr, 示例: %s http://example.com/bigfile.zip\n, argv[0]);return 1;}char *url argv[1];char filename[256];// 确定文件名if (argc 3) {strcpy(filename, argv[2]);} else {// 从URL提取文件名const char *last_slash strrchr(url, /);if (last_slash *(last_slash 1)) {strcpy(filename, last_slash 1);} else {strcpy(filename, downloaded_file);}}// 解析URLchar host[256], path[512];int port;if (parse_url(url, host, port, path) ! 0) {return 1;}printf(主机: %s:%d\n, host, port);printf(路径: %s\n, path);printf(文件: %s\n, filename);// 获取本地已下载大小long long local_size get_local_size(filename);if (local_size 0) {printf(发现已下载 %lld 字节继续下载...\n, local_size);}// 创建socket并连接int sock socket(AF_INET, SOCK_STREAM, 0);struct hostent *server gethostbyname(host);struct sockaddr_in addr;addr.sin_family AF_INET;memcpy(addr.sin_addr.s_addr, server-h_addr, server-h_length);addr.sin_port htons(port);if (connect(sock, (struct sockaddr*)addr, sizeof(addr)) 0) {perror(连接失败);return 1;}// 发送请求send_request(sock, host, path, local_size);// 解析响应头long long total_size;int is_partial;if (parse_response_header(sock, total_size, is_partial) ! 0) {close(sock);return 1;}printf(文件总大小: %lld 字节\n, total_size);// 打开文件追加模式FILE *file fopen(filename, ab);if (!file) {perror(打开文件失败);close(sock);return 1;}// 下载数据long long downloaded 0;download_content(sock, file, local_size, total_size, downloaded);// 清理fclose(file);close(sock);printf(下载完成保存为: %s\n, filename);return 0;}---三、编译与使用编译bashgcc downloader.c -o downloader使用示例bash# 全新下载./downloader http://example.com/ubuntu.iso# 指定文件名./downloader http://example.com/bigfile.zip myfile.zip# 断点续传直接再次运行相同命令即可./downloader http://example.com/ubuntu.iso---四、代码核心要点解析1. 文件大小检测clong long get_local_size(const char *filename) {struct stat st;if (stat(filename, st) 0) {return st.st_size; // 已下载了多少}return 0;}2. Range请求头csnprintf(request, sizeof(request),GET %s HTTP/1.1\r\nHost: %s\r\nRange: bytes%lld-\r\n // 关键从start_pos开始\r\n, path, host, start_pos);3. 206响应判断cif (strstr(buffer, 206 Partial Content)) {// 服务器支持断点续传is_partial 1;}4. 追加写入c// 以追加模式打开FILE *file fopen(filename, ab);// 数据会自动写到文件末尾---五、扩展方向这个基础版本还可以进一步优化功能 实现思路多线程下载 把文件分成多个段用多个线程同时下载不同范围进度保存 定期把已下载大小写入配置文件防止意外中断MD5校验 下载完成后校验文件完整性HTTPS支持 使用OpenSSL库把socket换成SSL socket断点续传UI 加一个简单的命令行进度条用\r实现---六、踩坑提醒1. 服务器必须支持Range不是所有服务器都支持可以先发HEAD请求检查Accept-Ranges头2. 追加模式不要写成w否则会覆盖已有数据3. 大文件用long longint最大只能表示2GB大文件会溢出4. 连接超时网络不稳定时需要加超时重连机制---结语断点续传是一个很经典的网络编程练习。通过这个例子你学会了· HTTP Range请求的原理· 如何解析HTTP响应头· 文件的追加写入· Socket编程的基础流程把这些代码跑起来你会对HTTP协议的理解上一个台阶。下一篇预告《多线程下载器把文件拆成10段同时下载》---有问题欢迎评论区讨论