之前的文章《curl编程实例-上传文件》,介绍过如何用 curl 编程上传文件,那会儿是直接指定文件路径,核心用法如下:
const char *file_path = "./test.txt"; // 要上传的本地文件
// 打开待上传的文件(二进制模式)
FILE *fp = fopen(file_path, "rb");
curl_easy_setopt(curl, CURLOPT_READDATA, fp);
curl_easy_perform(curl);
但真实场景往往需要分段上传,这时就要靠 curl 的读回调机制了。通过回调函数反复触发,每次只传一小块数据,最后拼成完整文件。
1 读回调的编写
1.1 分段上传参数
首先定义一个结构体,用来在回调里跟踪上传状态:
const char *filename;:要上传的文件名
size_t totalSize;:总大小
size_t uploadedSize;:已上传的大小
typedef struct{
const char *filename;
size_t totalSize;
size_t uploadedSize;
}UploadInfo_t;
1.2 读回调的定义
curl 的读回调会告知本次需要传输的字节数,我们只需按需从文件取数据,填入缓冲区,并更新已上传大小。代码如下:
size_t file_read_cb(char *buf, size_t size, size_t n, void *uploadInfo)
{
if (!uploadInfo)
{
return 0;
}
UploadInfo_t *info = (UploadInfo_t *)uploadInfo;
size_t bufferSize = size * n; // 此次需要上传的大小
curl_off_t remaining = info->totalSize - info->uploadedSize;
if (remaining <= 0)
{
return 0; // 已读完,结束传输
}
if (bufferSize > (size_t)remaining)
{
bufferSize = (size_t)remaining;
}
size_t bytesRead = custom_read_file(info->filename, info->uploadedSize, bufferSize, buf);
if (bytesRead == 0)
{
return 0;
}
info->uploadedSize += bytesRead;
float progress = (float) info->uploadedSize / info->totalSize * 100;
printf("[%s] read:%zu bytes, progress:%.1f%%\n", __func__, bytesRead, progress);
return bytesRead;
}
1.3 分段读取举例
分段读取的实现方式很多,可以从内存缓冲区读,也可以用其他自定义途径。这里为演示方便,仍用 fopen 打开文件,再通过 fseeko 偏移到指定位置,读取固定长度。
size_t custom_read_file(const char *file_path, size_t offset, size_t read_len, char *buffer)
{
size_t actual_read = 0;
// 这里只是使用fopen举例,实际可以是任何形式的文件读取
FILE *fp = fopen(file_path, "rb");
if (!fp)
{
printf("fopen:%s err\n", file_path);
return 0;
}
// 定位到指定偏移量:SEEK_SET 表示从文件开头计算偏移
int seek_ret = fseeko(fp, offset, SEEK_SET);
if (seek_ret != 0)
{
printf("fseeko failed: offset=%lld\n", (long long)offset);
goto END;
}
// 读取指定长度的数据到缓冲区
actual_read = fread(buffer, 1, read_len, fp);
if (actual_read != read_len)
{
// 读取不完整:可能是到文件末尾,或读取错误
if (feof(fp))
{
printf("Warning: only read %zu bytes(expect:%zu)\n", actual_read, read_len);
}
else if (ferror(fp))
{
printf("fread failed");
}
}
END:
fclose(fp);
return actual_read;
}
2 完整代码
完整代码如下,是在前文《curl编程实例-上传文件》的基础上修改而来,这里使用了curl编程的读回调机制,通过C语言实现分块上传。
// gcc file_upload2.c -o file_upload2 -lcurl
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<curl/curl.h>
#include<string.h>
#include<libgen.h> // 用于提取原始文件名
// 进度回调函数
static int upload_progress(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
if (ultotal > 0)
{
// 计算进度百分比,限制最大值为100%(实际的上传数据包含了HTTP头部等数据)
float progress = (ulnow * 100.0) / ultotal;
if (progress > 100.0) progress = 100.0;
printf("progress: %lld/%lld (%.2f%%)\n", (long long)ulnow, (long long)ultotal, progress);
}
return 0;
}
// 提取文件路径中的原始文件名(兼容绝对/相对路径)
char* get_original_filename(const char *file_path)
{
if (!file_path)
{
return strdup("unknown_file.dat");
}
char *path_copy = strdup(file_path);
char *filename = basename(path_copy);
char *result = strdup(filename);
free(path_copy);
return result;
}
// 获取文件实际大小(字节)
curl_off_t get_file_size(const char *file_path)
{
if (!file_path)
{
return -1;
}
struct stat st;
if (stat(file_path, &st) == -1)
{
printf("stat %s failed\n", file_path);
return -1;
}
return (curl_off_t)st.st_size;
}
typedef struct{
const char *filename;
size_t totalSize;
size_t uploadedSize;
}UploadInfo_t;
size_t custom_read_file(const char *file_path, size_t offset, size_t read_len, char *buffer)
{
size_t actual_read = 0;
// 这里只是使用fopen举例,实际可以是任何形式的文件读取
FILE *fp = fopen(file_path, "rb");
if (!fp)
{
printf("fopen:%s err\n", file_path);
return 0;
}
// 定位到指定偏移量:SEEK_SET 表示从文件开头计算偏移
int seek_ret = fseeko(fp, offset, SEEK_SET);
if (seek_ret != 0)
{
printf("fseeko failed: offset=%lld\n", (long long)offset);
goto END;
}
// 读取指定长度的数据到缓冲区
actual_read = fread(buffer, 1, read_len, fp);
if (actual_read != read_len)
{
// 读取不完整:可能是到文件末尾,或读取错误
if (feof(fp))
{
printf("Warning: only read %zu bytes(expect:%zu)\n", actual_read, read_len);
}
else if (ferror(fp))
{
printf("fread failed");
}
}
END:
fclose(fp);
return actual_read;
}
size_t file_read_cb(char *buf, size_t size, size_t n, void *uploadInfo)
{
if (!uploadInfo)
{
return 0;
}
UploadInfo_t *info = (UploadInfo_t *)uploadInfo;
size_t bufferSize = size * n; // 此次需要上传的大小
curl_off_t remaining = info->totalSize - info->uploadedSize;
if (remaining <= 0)
{
return 0; // 已读完,结束传输
}
if (bufferSize > (size_t)remaining)
{
bufferSize = (size_t)remaining;
}
size_t bytesRead = custom_read_file(info->filename, info->uploadedSize, bufferSize, buf);
if (bytesRead == 0)
{
return 0;
}
info->uploadedSize += bytesRead;
float progress = (float) info->uploadedSize / info->totalSize * 100;
printf("[%s] read:%zu bytes, progress:%.1f%%\n", __func__, bytesRead, progress);
return bytesRead;
}
int main(int argc, char *argv[])
{
CURL *curl = NULL;
CURLcode res;
FILE *fp = NULL;
const char *upload_url = "http://192.168.5.104:8080/upload"; // 文件服务器的地址
const char *file_path = "./test.jpg"; // 要上传的本地文件
struct curl_slist *headers = NULL;// 自定义请求头
// 文件上传的信息
UploadInfo_t uploadInfo = {0};
uploadInfo.filename = file_path;
// 获取文件大小
size_t file_size = get_file_size(file_path);
if (file_size < 0)
{
printf("%s file_size:%zu err\n", file_path, file_size);
goto cleanup;
}
printf("%s file_size: %zu\n", file_path, file_size);
uploadInfo.totalSize = file_size;
// 初始化libcurl
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if (!curl)
{
printf("curl_easy_init, err\n");
goto cleanup;
}
// 构建自定义请求头(传递原始文件名)
char *original_filename = get_original_filename(file_path);
char header_buf[256];
snprintf(header_buf, sizeof(header_buf), "X-File-Name: %s", original_filename);
headers = curl_slist_append(headers, header_buf);
// 禁用Expect头,解决POST上传阻塞问题
headers = curl_slist_append(headers, "Expect:");
// 设置上传URL
curl_easy_setopt(curl, CURLOPT_URL, upload_url);
// 设置自定义请求头
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// 设置文件上传
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)file_size);
curl_easy_setopt(curl, CURLOPT_READFUNCTION, file_read_cb);
curl_easy_setopt(curl, CURLOPT_READDATA, &uploadInfo);
// 执行上传
printf("start uoload file: %s\n", file_path);
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
printf("upload fail: %s\n", curl_easy_strerror(res));
}
else
{
printf("upload success\n");
}
// 资源清理(统一出口)
cleanup:
if (original_filename) free(original_filename);
if (headers) curl_slist_free_all(headers);
if (curl) curl_easy_cleanup(curl);
curl_global_cleanup();
return res == CURLE_OK ? 0 : 1;
}
3 运行结果
测试环境:

可以看到,文件通过回调分段上传,Windows 服务器的目标目录里也出现了上传的文件。
4 总结
本篇介绍了curl编程时如何利用读回调实现文件的分块上传,并给出了完整可运行的C语言示例代码。对比直接指定文件路径的方式,这种方法更灵活,适合有自定义数据源或需要精细控制传输进度的场景。