之前的文章《curl编程实例-上传文件》,介绍过如何使用curl编程来上传文件,是通过指定文件路径的方式来上传,关键的特征如下:
c
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;:目前已上传的大小
c
typedef struct{
const char *filename;
size_t totalSize;
size_t uploadedSize;
}UploadInfo_t;
1.2 读回调的定义
curl的读回调,会自动给出每次需要上传的数据大小,回调中需要做的,就是根据回调参数中指定的需要上传的大小,将文件的分段内容,写入指定的缓冲区,然后将已上传的大小,记录到前面定义的UploadInfo_t结构中。
c
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进行偏移,从而实现从文件的指定位置读取指定长度的内容。
c
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编程实例-上传文件》的基础上进行修改的。
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电脑上启动一个文件服务器,可参考之前的文章《curl编程实例-上传文件》
- 在ubuntu虚拟机上启动文件上传程序

可以看到,文件通过回调的方式,多次分段上传,最终的windows电脑的文件服务器的指定目录,可以看到上传的文件
4 总结
本篇介绍了如何使用curl的C语言编程,读回调的方式,实现文件的分段上传,并通过代码实例,验证上传的结果。