找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4298

积分

0

好友

560

主题
发表于 1 小时前 | 查看: 4| 回复: 0

之前的文章《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 运行结果

测试环境:

curl 读回调分块上传终端输出截图

可以看到,文件通过回调分段上传,Windows 服务器的目标目录里也出现了上传的文件。

4 总结

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




上一篇:嵌入式Linux利用core-dump与GDB精准定位程序崩溃实战
下一篇:C语言使用cJSON读写配置文件并实现交互式配置工具
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-3 05:19 , Processed in 1.450100 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表