news 2026/4/21 14:20:16

手把手教你写一个断点续传下载器:HTTP Range 请求实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手教你写一个断点续传下载器:HTTP Range 请求实战

前言

你有没有遇到过这种情况:下载一个大文件,进度走到90%,网络突然断了。重新下载?又要从头开始,几个小时白等了。

如果下载器支持断点续传,就可以从断掉的地方继续下载,省时省力。

今天,我们用C语言手写一个支持断点续传的命令行下载器,彻底搞懂HTTP Range请求的原理和实现。

---

一、断点续传的核心原理

1. HTTP Range 请求

断点续传依赖HTTP协议的一个特性:Range请求。

客户端可以在请求头中告诉服务器:“我已经下载了前1000个字节,请从第1001个字节开始发送。”

```
GET /bigfile.zip HTTP/1.1
Host: example.com
Range: bytes=1000-
```

服务器如果支持Range,会返回:

```
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-199999/200000
Content-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\n"
"Host: %s\r\n"
"User-Agent: %s\r\n"
"Range: bytes=%lld-\r\n"
"Connection: close\r\n"
"\r\n",
path, host, USER_AGENT, start_pos);
} else {
// 全新下载:不带Range
snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"User-Agent: %s\r\n"
"Connection: 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\n)
if (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-Length
char *range_ptr = strstr(buffer, "Content-Range:");
if (range_ptr) {
// 格式:Content-Range: bytes 0-199999/200000
char *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");
}
}

// 解析URL
char 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;
}
```

---

三、编译与使用

编译

```bash
gcc 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. 文件大小检测

```c
long long get_local_size(const char *filename) {
struct stat st;
if (stat(filename, &st) == 0) {
return st.st_size; // 已下载了多少
}
return 0;
}
```

2. Range请求头

```c
snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Range: bytes=%lld-\r\n" // 关键:从start_pos开始
"\r\n", path, host, start_pos);
```

3. 206响应判断

```c
if (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 long:int最大只能表示2GB,大文件会溢出
4. 连接超时:网络不稳定时,需要加超时重连机制

---

结语

断点续传是一个很经典的网络编程练习。通过这个例子,你学会了:

· HTTP Range请求的原理
· 如何解析HTTP响应头
· 文件的追加写入
· Socket编程的基础流程

把这些代码跑起来,你会对HTTP协议的理解上一个台阶。

下一篇预告:《多线程下载器:把文件拆成10段同时下载》

---

有问题欢迎评论区讨论!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 14:15:58

Fluke 8060A数字万用表LCD屏幕定制与替换方案

1. 项目背景与动机作为一名电子测量设备爱好者&#xff0c;我与Fluke 8060A数字万用表的缘分可以追溯到1990年。这款经典设备陪伴我度过了无数个调试电路的日夜&#xff0c;但随着时间的推移&#xff0c;这些老伙计们开始出现一个通病——LCD屏幕漏液。特别是在我收藏的25台806…

作者头像 李华
网站建设 2026/4/21 14:13:41

2026届必备的五大AI论文神器实测分析

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 使得AI指令得以优化文本生成进程进而减少机械化印记&#xff0c;以此提升内容自然程度的&…

作者头像 李华
网站建设 2026/4/21 14:13:36

3个必学技巧:用ComfyUI Impact Pack实现AI图像增强

3个必学技巧&#xff1a;用ComfyUI Impact Pack实现AI图像增强 【免费下载链接】ComfyUI-Impact-Pack Custom nodes pack for ComfyUI This custom node helps to conveniently enhance images through Detector, Detailer, Upscaler, Pipe, and more. 项目地址: https://git…

作者头像 李华
网站建设 2026/4/21 14:13:36

AI写专著技巧大公开!AI工具助你3天完成20万字专著创作!

撰写学术专著的挑战与 AI 工具解决方案 撰写学术专著不仅是一种学术水平的考验&#xff0c;还极大地考验个人的心理承受能力。与团队合作的论文写作不同&#xff0c;专著的创作往往是研究人员“单打独斗”&#xff0c;从选题到框架设计&#xff0c;再到内容的撰写和修改&#…

作者头像 李华