curl使用读回调来分块上报文件

之前的文章《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语言编程,读回调的方式,实现文件的分段上传,并通过代码实例,验证上传的结果。

相关推荐
为java加瓦5 天前
Linux 系统磁盘空间清理指南:安全释放存储空间的完整方案
linux·curl
太空眼睛9 天前
【MCP】使用SpringBoot基于Streamable-HTTP构建MCP-Server
spring boot·sse·curl·mcp·mcp-server·spring-ai·streamable
风清扬_jd12 天前
libcurl 开启https一键编译指南【MT方式】
c++·https·curl
码农爱学习1 个月前
curl编程实例-上传文件
curl
龚建波1 个月前
记录:vcpkg清单模式安装指定版本的curl和OpenSSL
openssl·curl·vcpkg
YouEmbedded2 个月前
解码ARM 开发板 OpenSSL+cURL 交叉编译与 HTTPS 配置
https·openssl·curl
大米粥哥哥2 个月前
c++ libcurl报错Send failed since rewinding of the data stream failed【已解决】
开发语言·c++·http·curl·rewind
idolyXyz3 个月前
[curl-http3: 基于quiche+boringssl编译]
http3·curl·boringssl·quiche
叫我詹躲躲3 个月前
Linux 服务器磁盘满了?教你快速找到大文件,安全删掉不踩坑!
linux·前端·curl