前言
你有没有遇到过这种情况:下载一个大文件,进度走到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段同时下载》
---
有问题欢迎评论区讨论!