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

5268

积分

0

好友

743

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

libcurl 是一个功能强大的跨平台网络传输库,支持多种协议。本文将介绍如何使用 libcurl 的 C 语言 API,通过实际的代码示例实现文件下载功能,并进一步拓展到多线程分段下载。

1 curl 基础介绍

1.1 核心数据结构

1.1.1 CURL 句柄

CURL 是 libcurl 的核心句柄,每个请求对应一个 CURL 实例,是配置请求参数、执行操作的主要载体。

其特性包括:

  • 独立性:每个句柄都是独立的,配置互不影响。
  • 可复用性:一个句柄在完成一次请求后,可通过重新设置选项再次使用,无需重复创建和销毁。
  • 状态存储:句柄内部存储了请求的所有状态信息,直到被释放。

1.1.2 CURLcode 返回值

CURLcode 是函数返回值类型,用于判断操作是否成功。成功状态为 CURLE_OK。所有返回值枚举如下:

typedef enum {
  CURLE_OK = 0,                   /* 操作成功 */
  CURLE_UNSUPPORTED_PROTOCOL,     /* 不支持的协议 */
  CURLE_FAILED_INIT,              /* 初始化失败(如 curl_global_init 调用失败) */
  CURLE_URL_MALFORMAT,            /* URL 格式错误 */
  CURLE_URL_MALFORMAT_USER,       /* URL 中的用户名/密码格式错误 */
  CURLE_COULDNT_RESOLVE_PROXY,    /* 无法解析代理服务器 */
  CURLE_COULDNT_RESOLVE_HOST,     /* 无法解析主机名(DNS 失败) */
  CURLE_COULDNT_CONNECT,          /* 无法连接到服务器 */
  CURLE_WEIRD_SERVER_REPLY,       /* 服务器返回异常响应 */
  CURLE_REMOTE_ACCESS_DENIED,     /* 远程访问被拒绝(如权限不足) */
  CURLE_FTP_ACCEPT_FAILED,        /* FTP 接受连接失败 */
  CURLE_FTP_WEIRD_PASS_REPLY,     /* FTP 密码响应异常 */
  CURLE_FTP_ACCEPT_TIMEOUT,       /* FTP 接受超时 */
  CURLE_FTP_WEIRD_PASV_REPLY,     /* FTP PASV 命令响应异常 */
  CURLE_FTP_WEIRD_227_FORMAT,     /* FTP 227 响应格式错误 */
  CURLE_FTP_CANT_GET_HOST,        /* FTP 无法获取主机信息 */
  CURLE_HTTP2,                    /* HTTP/2 相关错误 */
  CURLE_FTP_COULDNT_SET_TYPE,     /* FTP 无法设置传输类型(ASCII/BINARY) */
  CURLE_PARTIAL_FILE,             /* 只接收到部分文件(未完成全量下载) */
  CURLE_FTP_COULDNT_RETR_FILE,    /* FTP 无法检索文件 */
  CURLE_OBSOLETE4,                /* 已废弃(历史版本使用) */
  CURLE_QUOTE_ERROR,              /* FTP QUOTE 命令执行失败 */
  CURLE_HTTP_RETURNED_ERROR,      /* HTTP 服务器返回错误状态码(如 4xx、5xx) */
  CURLE_WRITE_ERROR,              /* 写入数据失败(如本地文件写入失败) */
  CURLE_OBSOLETE9,                /* 已废弃 */
  CURLE_UPLOAD_FAILED,            /* 上传失败 */
  CURLE_READ_ERROR,               /* 读取数据失败(如本地文件读取失败) */
  CURLE_OUT_OF_MEMORY,            /* 内存不足 */
  CURLE_OPERATION_TIMEDOUT,       /* 操作超时 */
  CURLE_OBSOLETE14,               /* 已废弃 */
  CURLE_FTP_PORT_FAILED,          /* FTP PORT 命令执行失败 */
  CURLE_FTP_COULDNT_USE_REST,     /* FTP REST 命令不被支持 */
  CURLE_OBSOLETE17,               /* 已废弃 */
  CURLE_RANGE_ERROR,              /* 范围请求错误(服务器不支持 Range 或范围无效) */
  CURLE_HTTP_POST_ERROR,          /* HTTP POST 数据传输错误 */
  CURLE_SSL_CONNECT_ERROR,        /* SSL/TLS 连接失败 */
  CURLE_BAD_DOWNLOAD_RESUME,      /* 断点续传失败(如文件不存在或偏移量无效) */
  CURLE_FILE_COULDNT_READ_FILE,   /* 无法读取本地文件 */
  CURLE_LDAP_CANNOT_BIND,         /* LDAP 绑定失败 */
  CURLE_LDAP_SEARCH_FAILED,       /* LDAP 搜索失败 */
  CURLE_OBSOLETE24,               /* 已废弃 */
  CURLE_FUNCTION_NOT_FOUND,       /* 未找到指定的回调函数 */
  CURLE_ABORTED_BY_CALLBACK,      /* 被回调函数中止(如进度回调返回非零值) */
  CURLE_BAD_FUNCTION_ARGUMENT,    /* 回调函数参数错误 */
  CURLE_OBSOLETE28,               /* 已废弃 */
  CURLE_OBSOLETE29,               /* 已废弃 */
  CURLE_PARTIAL_FILE_FROM_cache,  /* 从缓存获取到部分文件 */
  CURLE_OBSOLETE31,               /* 已废弃 */
  CURLE_SSL_ENGINE_NOTFOUND,      /* 未找到 SSL 引擎 */
  CURLE_SSL_ENGINE_SETFAILED,     /* SSL 引擎初始化失败 */
  CURLE_SEND_ERROR,               /* 发送数据失败(网络发送错误) */
  CURLE_RECV_ERROR,               /* 接收数据失败(网络接收错误) */
  CURLE_OBSOLETE36,               /* 已废弃 */
  CURLE_SSL_CERTPROBLEM,          /* SSL 证书问题(如无效、过期) */
  CURLE_SSL_CIPHER,               /* 无法协商 SSL 加密套件 */
  CURLE_SSL_CACERT,               /* 无法验证 SSL CA 证书 */
  CURLE_BAD_CONTENT_ENCODING,     /* 内容编码错误(如服务器返回无效的 gzip 数据) */
  CURLE_LDAP_INVALID_URL,         /* LDAP URL 无效 */
  CURLE_FILESIZE_EXCEEDED,        /* 文件大小超出限制 */
  CURLE_USE_SSL_FAILED,           /* 启用 SSL 失败 */
  CURLE_SEND_FAIL_REWIND,         /* 发送数据前重绕(rewind)失败 */
  CURLE_SSL_ENGINE_INITFAILED,    /* SSL 引擎初始化失败 */
  CURLE_LOGIN_DENIED,             /* 登录被拒绝(如用户名/密码错误) */
  CURLE_TFTP_NOTFOUND,            /* TFTP 文件未找到 */
  CURLE_TFTP_PERM,                /* TFTP 权限错误 */
  CURLE_REMOTE_DISK_FULL,         /* 远程磁盘满 */
  CURLE_TFTP_ILLEGAL,             /* TFTP 非法操作 */
  CURLE_TFTP_UNKNOWNID,           /* TFTP 未知传输 ID */
  CURLE_REMOTE_FILE_EXISTS,       /* 远程文件已存在(不允许覆盖) */
  CURLE_TFTP_NOSUCHUSER,          /* TFTP 无此用户 */
  CURLE_CONV_FAILED,              /* 字符编码转换失败 */
  CURLE_CONV_REQD,                /* 需要字符编码转换但未启用 */
  CURLE_SSL_CACERT_BADFILE,       /* SSL CA 证书文件不存在或无法读取 */
  CURLE_REMOTE_FILE_NOT_FOUND,    /* 远程文件未找到 */
  CURLE_SSH,                      /* SSH 相关错误 */
  CURLE_SSL_SHUTDOWN_FAILED,      /* SSL 关闭失败 */
  CURLE_AGAIN,                    /* 操作需要重试(非错误,用于异步操作) */
  CURLE_SSL_CRL_BADFILE,          /* CRL 文件错误 */
  CURLE_SSL_ISSUER_ERROR,         /* SSL  issuer 证书验证失败 */
  CURLE_FTP_PRET_FAILED,          /* FTP PRET 命令失败 */
  CURLE_RTSP_CSEQ_ERROR,          /* RTSP CSEQ 不匹配 */
  CURLE_RTSP_SESSION_ERROR,       /* RTSP 会话错误 */
  CURLE_FTP_BAD_FILE_LIST,        /* FTP 文件列表格式错误 */
  CURLE_CHUNK_FAILED,             /* HTTP 分块传输编码错误 */
  CURLE_NO_CONNECTION_AVAILABLE,  /* 无可用连接(连接池耗尽) */
  CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* SSL 固定公钥不匹配 */
  CURLE_SSL_INVALIDCERTSTATUS,    /* SSL 证书状态无效 */
  CURLE_HTTP2_STREAM,             /* HTTP/2 流错误 */
  CURLE_RECURSIVE_API_CALL,       /* 递归调用 libcurl API */
  CURLE_AUTH_ERROR,               /* 认证错误 */
  CURLE_HTTP3,                    /* HTTP/3 相关错误 */
  CURLE_QUIC_CONNECT_ERROR,       /* QUIC 连接错误 */
  CURLE_PROXY,                    /* 代理相关错误 */
  CURLE_SSL_CLIENTCERT,           /* 客户端证书错误 */
  CURLE_UNRECOVERABLE_POLL,       /* 无法恢复的轮询错误 */
  CURLE_LAST                      /* 标记枚举结束(无实际意义) */
} CURLcode;

1.2 初始化与清理

1.2.1 curl_global_init

初始化 libcurl 全局环境,必须在所有其他 libcurl 函数之前调用。

CURLcode curl_global_init(long flags);
  • 参数:flags 通常使用 CURL_GLOBAL_DEFAULT(默认初始化)。
  • 返回值:CURLcode 类型,成功返回 CURLE_OK

curl_global_init 通常需要手动调用。如果不调用,在第一次调用 curl_easy_init 时,libcurl 会隐式调用 curl_global_init(CURL_GLOBAL_DEFAULT),但这种行为并不可靠,可能存在以下问题:

  • 无法指定初始化选项(只能使用默认配置)。
  • 无法捕获 curl_global_init 的返回值(若初始化失败,难以排查原因)。
  • 可能导致多线程环境下的竞争条件(非线程安全)。
  • 部分平台或 libcurl 编译选项可能禁用此功能。

如果代码逻辑较简单,或刚开始学习 curl,可以简化逻辑先只调用 curl_easy_init

1.2.2 curl_easy_init

创建一个 CURL 句柄,用于配置单次请求。

CURL *curl_easy_init(void);
  • 返回值:成功返回 CURL* 句柄,失败返回 NULL

1.2.3 curl_easy_cleanup

释放 CURL 句柄资源。

void curl_easy_cleanup(CURL *curl);

1.2.4 curl_global_cleanup

清理 libcurl 全局环境,程序结束前调用。

void curl_global_cleanup(void);

1.3 下载核心配置 API

1.3.1 curl_easy_setopt

设置 CURL 句柄的各种选项,几乎所有配置都通过它完成。

函数原型:

CURLcode curl_easy_setopt(CURL *curl, CURLoption option, parameter);

参数说明:

  • CURL *curl:指向 CURL 句柄的指针,由 curl_easy_init() 创建。
  • CURLoption option:枚举类型的选项参数,指定要设置的具体配置,例如:
    • CURLOPT_URL(设置请求的 URL)。
    • CURLOPT_WRITEFUNCTION(设置数据接收回调函数)。
  • parameter:选项对应的参数值,类型根据 option 不同而变化,可能是字符串、整数、函数指针等。若选项不需要参数,可设为 0NULL

1.3.2 与文件下载相关的一些选项

  • CURLOPT_URL:设置下载的 URL(必填)。
    示例:curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/file.zip");

  • CURLOPT_WRITEFUNCTION:设置回调函数,用于处理下载的文件数据。
    回调函数原型:

    size_t (*curl_write_callback)(void *ptr, size_t size, size_t nmemb, void *userp);
    • ptr:下载到的数据缓冲区;
    • size:每个数据块的大小;
    • nmemb:数据块数量;
    • userp:自定义参数(通常是文件指针等);
    • 返回值:实际处理的字节数(需返回 size * nmemb,否则可能中断下载)。
  • CURLOPT_WRITEDATA:设置传递给 CURLOPT_WRITEFUNCTION 回调的自定义参数(如文件指针)。
    示例:curl_easy_setopt(curl, CURLOPT_WRITEDATA, file_ptr);

  • CURLOPT_FOLLOWLOCATION:设置为 1L 时,自动跟随 HTTP 重定向(如 301/302)。

  • CURLOPT_SSL_VERIFYPEER:设置为 0L 时,忽略 SSL 证书验证(仅测试用,生产环境不推荐)。

  • CURLOPT_PROGRESSFUNCTION:设置进度回调函数,用于跟踪下载进度。
    回调原型:

    int (*progress_callback)(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow);
    • dltotal:总下载大小;
    • dlnow:已下载大小。
  • CURLOPT_NOPROGRESS:设置为 0L 时,启用进度回调(需配合 CURLOPT_PROGRESSFUNCTION 使用)。

1.4 执行下载与获取信息

1.4.1 curl_easy_perform

执行配置好的请求(开始数据传输)。

CURLcode curl_easy_perform(CURL *curl);
  • 参数:指向已配置好的 CURL 句柄的指针。
  • 返回值:CURLcode 类型,CURLE_OK 表示下载成功。

1.4.2 curl_easy_getinfo

获取下载相关信息(需在 curl_easy_perform 执行后调用)。

CURLcode curl_easy_getinfo(CURL *curl, CURLINFO info, ...);

参数:

  • CURL *curl:指向已执行过请求的 CURL 句柄的指针。
  • CURLINFO info:枚举类型的参数,指定要获取的信息类型,例如:
    • CURLINFO_RESPONSE_CODE(HTTP 响应码)。
    • CURLINFO_SIZE_DOWNLOAD(下载总字节数)。
  • ...:可变参数,指向用于存储结果的变量指针,类型根据 info 不同而变化。

2 文件下载基础示例

2.1 curl 下载主逻辑

主要调用的接口如下:

  • curl_easy_init:初始化 curl 句柄。
  • curl_easy_setopt:设置选项(文件下载地址,下载的回调函数)。
  • curl_easy_perform:执行文件下载。
void http_file_download(const char *url, const char *name)
{
    printf("[%s] in\n", __func__);

    int fd = open(name, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR);
    if (-1 == fd)
    {
        printf("[%s] create file:%s failed\n", __func__, name);
        return;
    }
    printf("[%s] create file:%s ok\n", __func__, name);

    printf("[%s] curl init\n", __func__);
    CURL *curl = curl_easy_init();

    printf("[%s] curl setopt, url:%s\n", __func__, url);
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_cb); 
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &fd); 

    printf("[%s] curl performt\n", __func__);
    curl_easy_perform(curl);

    printf("[%s] close file\n", __func__);
    close(fd);

    printf("[%s] out\n", __func__);   
}

2.2 下载数据的回调

下载数据的回调需要根据回调函数的原型自己实现,这里将接收的数据通过 write 方法写入之前创建的文件中。文件的创建在 http_file_download 中通过 open 完成,并通过 CURLOPT_WRITEDATA 将文件描述符传递给回调。

size_t write_file_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    static int i = 0;
    size_t this_size = size * nmemb;
    printf("[%s] <%d> total size:%zu\n", __func__, ++i, this_size);

    int fd = *((int *)userdata);
    write(fd, ptr, this_size);

    return this_size;
}

2.3 主函数

测试文件下载需要先获得一个有效的下载链接。可以先通过浏览器下载一个文件,然后在下载记录中复制其下载链接。下面给出一个完整的测试主函数:

// gcc download_file.c -o download_file -l curl

#include <stdio.h>
#include <curl/curl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>

const char *file_url = "https://vscode.download.prss.microsoft.com/dbazure/download/stable/488a1f239235055e34e673291fb8d8c810886f81/code_1.102.3-1753759567_amd64.deb";
const char *file_name = "my_download.deb";

int main()
{
    printf("hello\n");

    time_t start;
    time_t end;

    time(&start);
    http_file_download(file_url, file_name);
    time(&end);

    int esapsed = difftime(end, start);
    printf("esapsed time:%d sec\n", esapsed);

    return 0;
}

运行后,可以使用系统自带的 md5sum 对比浏览器下载的文件和 curl 程序下载的文件的 MD5 值。若二者一致,则说明下载正确无误。

3 多线程下载

curl 下载文件的主要耗时集中在 curl_easy_perform 的阻塞等待上。多线程下载的基本思路是:创建多个 curl 句柄,将文件划分为若干段,每个线程使用独立的 curl 句柄下载不同的分段,从而实现并行传输。

3.1 curl 下载主逻辑

与单线程版本相比,多线程下载的核心区别在于:

  • 需要先获取文件长度,以便分配分段任务(通过 get_download_length 实现)。
  • 根据文件大小提前为文件开辟空间,用于分段写入(通过 lseek 实现)。
  • 将文件映射到虚拟内存,方便多线程直接写入对应偏移(通过 mmap 实现)。
  • 创建多个线程,每个线程执行各自的分段下载任务(通过 pthread_create 实现)。
void http_file_download(const char *url, const char *name)
{
    printf("[%s] in\n", __func__);

    // 要下载的文件长度
    long long file_length = get_download_length(url);
    if (-1 == file_length)
    {
        return;
    }
    printf("[%s] file_length:%lld\n", __func__, file_length);

    // 创建一个文件
    int fd = open(name, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR);
    if (-1 == fd)
    {
        printf("[%s] create file:%s failed\n", __func__, name);
        return;
    }
    printf("[%s] create file:%s ok\n", __func__, name);

    // 先给文件开辟一块空间
    if (-1 == lseek(fd, file_length -1, SEEK_SET))
    {
        printf("[%s] lseek failed\n", __func__);
        goto DONE;
    }

    if (1 != write(fd, "\0", 1))
    {
        printf("[%s] write failed\n", __func__);
        goto DONE;
    }

    // 将文件映射到虚拟内存
    void *fileptr = mmap(NULL, file_length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == fileptr)
    {
        printf("[%s] mmap failed\n", __func__);
        goto DONE;
    }

    // 创建线程
    pthread_t threads[MULTI_DOWNLOAD_THEAD_NUM];
    for (int i = 0; i < MULTI_DOWNLOAD_THEAD_NUM; i++)
    {
        threadParam[i].thrd_num = (i+1);
        threadParam[i].url = (char *)url;
        threadParam[i].range[0] = (file_length / MULTI_DOWNLOAD_THEAD_NUM) * i;
        threadParam[i].range[1] = (MULTI_DOWNLOAD_THEAD_NUM - 1 == i) ? (file_length - 1) : (file_length / MULTI_DOWNLOAD_THEAD_NUM) * (i + 1) - 1; 
        threadParam[i].ptr = fileptr + threadParam[i].range[0];
        threadParam[i].offset = 0;
        pthread_create(&threads[i], NULL, thread_func, &threadParam[i]);
    }

    for (int i = 0; i < MULTI_DOWNLOAD_THEAD_NUM; i++)
    {
        pthread_join(threads[i], NULL);
    }
    printf("[%s] all download done\n", __func__);

    munmap(fileptr, file_length);

DONE:
    printf("[%s] close file\n", __func__);
    close(fd);

    printf("[%s] out\n", __func__);   
}

3.1.1 mmap 的介绍

mmap 是 Unix/Linux 系统中用于将文件或设备映射到进程虚拟内存空间的系统调用,其函数原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数:

  • void *addr:期望的映射起始地址(通常设为 NULL,由操作系统自动选择)。
  • size_t length:要映射的字节数(必须大于 0),通常是文件的大小。
  • int prot:映射区域的保护权限,可组合以下值:
    • PROT_READ:可读。
    • PROT_WRITE:可写(修改映射区会同步到文件)。
    • PROT_EXEC:可执行。
    • PROT_NONE:不可访问。
  • int flags:映射类型和属性,关键取值(必选其一):
    • MAP_SHARED:共享映射,修改映射区会同步到文件,且对其他映射该文件的进程可见。
    • MAP_PRIVATE:私有映射,修改不会同步到文件。
    • 其他常用标志:MAP_ANONYMOUS(匿名映射,用于进程间共享内存),此时 fd 需设为 -1
  • int fd:要映射的文件描述符(通过 open 打开的文件句柄)。
  • off_t offset:从文件的哪个偏移量开始映射(必须是系统页大小的整数倍,通常为 4KB 或 8KB)。

返回值:成功返回映射区域的起始地址,失败返回 MAP_FAILED(通常是 (void*)-1),并设置 errno 指示错误原因。

3.1.2 关于数据分段的计算

假设文件总长度为 file_length,使用 N 个线程同时下载。将数据平均分为 N 段(0、1、...、N-1)。若无法整除,最后一段取到文件末尾。

3.2 获取文件的长度

获取文件长度同样通过 curl,但只获取响应头信息,然后使用 curl_easy_getinfo 提取 Content-Length

long long get_download_length(const char *url)
{   
    CURL *curl = curl_easy_init();

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"); 
    curl_easy_setopt(curl, CURLOPT_HEADER, 1); 
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1);

    if (CURLM_OK != curl_easy_perform(curl))
    {
        printf("[%s] failed\n", __func__);
        return -1;
    }

    double res = 0; 
    curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &res);
    download_len = (long long)res;

    return download_len;
}

3.3 线程函数

线程函数的内容与单线程下载中的 curl 调用逻辑类似,但增加了范围请求(CURLOPT_RANGE)和进度回调。

void *thread_func(void *arg)
{
    ThreadParam *par = (ThreadParam *)arg;
    char range[64] = {0};
    sprintf(range, "%lld-%lld", par->range[0], par->range[1]);

    // 创建一个 curl 对象 
    printf("[%s] thrd<%d> curl init\n", __func__, par->thrd_num);
    CURL *curl = curl_easy_init();

    printf("[%s] thrd<%d> curl setopt, url:%s, range:%s\n", __func__, par->thrd_num, par->url, range);
    curl_easy_setopt(curl, CURLOPT_URL, par->url);
    curl_easy_setopt(curl, CURLOPT_RANGE, range);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_cb); 
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, par); 

    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); 
    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, download_progress_cb); 
    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, par); 

    // 执行网络请求
    printf("[%s] thrd<%d> curl performt\n", __func__, par->thrd_num);
    CURLcode res = curl_easy_perform(curl);   
    if (CURLE_OK != res)
    {
        printf("[%s] thrd<%d> curl_easy_perform err:%d\n", __func__, par->thrd_num, res);
    }
    printf("[%s] thrd<%d> curl performt done\n", __func__, par->thrd_num);
}

3.4 下载数据的回调

多线程版本的回调使用 memcpy 将接收的数据拷贝到内存映射文件对应的偏移位置。

size_t write_file_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    static int i = 0;
    size_t this_size = size * nmemb;

    ThreadParam *par = (ThreadParam *)userdata;
    memcpy(par->ptr + par->offset, ptr, this_size);
    par->offset += this_size;

    return this_size;
}

3.5 文件下载进度

进度回调用于汇总所有线程的下载量,并计算整体进度百分比。

int download_progress_cb(void *userdata, double totalDownload, double nowDownload, double totalUpload, double nowUpload)
{
    ThreadParam *par = (ThreadParam *)userdata;
    par->download = nowDownload;

    int percent = 0;
    static int lastPercent = -1;
    if (totalDownload > 0)
    {
        double allDownload = 0;
        for (int i = 0 ;i < MULTI_DOWNLOAD_THEAD_NUM; i++)
        {
            allDownload += threadParam[i].download;
        }

        percent = (int)(allDownload / download_len * 100);
    }

    if (lastPercent != percent)
    {
        lastPercent = percent;
        printf("[%s] percent:%d%%\n", __func__, percent);
    }

    return 0;
}

3.6 主函数

完整的测试主函数包含必要的头文件、全局变量、线程参数结构体定义,以及最终的执行逻辑。

// gcc download_file_multi_thread.c -o download_file_multi_thread -lcurl -lpthread

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <time.h>
#include <pthread.h>

const char *file_url = "https://vscode.download.prss.microsoft.com/dbazure/download/stable/488a1f239235055e34e673291fb8d8c810886f81/code_1.102.3-1753759567_amd64.deb";
const char *file_name = "my_multithread_download.deb";

#define MULTI_DOWNLOAD_THEAD_NUM (3)

typedef struct
{
    int thrd_num;
    char *url;
    long long range[2];
    char *ptr;
    int offset;
    double download;
} ThreadParam;

ThreadParam threadParam[MULTI_DOWNLOAD_THEAD_NUM];

long long download_len = -1;

size_t write_file_cb(char *ptr, size_t size, size_t nmemb, void *userdata);
int download_progress_cb(void *userdata, double totalDownload, double nowDownload, double totalUpload, double nowUpload);
long long get_download_length(const char *url);
void *thread_func(void *arg);
void http_file_download(const char *url, const char *name);

int main()
{
    printf("hello\n");

    time_t start;
    time_t end;

    time(&start);
    http_file_download(file_url, file_name);
    time(&end);

    int esapsed = difftime(end, start);
    printf("esapsed time:%d sec\n", esapsed);

    return 0;
}

运行程序后,可以在终端观察到 3 个线程依次下载,进度逐步从 0% 增长到 100%,最终文件下载完成。虽然通过计时发现多线程并未带来明显的耗时减少,但这个示例完整展示了分段下载的实现方式。

4 总结

本文详细介绍了如何使用 libcurl 的 C 语言 API 实现文件下载,包括基础 API 的用法、单线程下载示例,以及多线程分段下载的完整实现。希望通过这些代码示例,能够帮助你快速掌握 curl 编程的核心要点。




上一篇:告别 AI 设计套路:Hallmark Skill 让 Claude Code 生成更有个性的网页
下一篇:手把手教你写一个“增量同步备份”Shell脚本
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-8-3 04:04 , Processed in 1.015739 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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