首页 > 其他分享 >libcurl多线程下载,支持断点续传

libcurl多线程下载,支持断点续传

时间:2025-01-17 14:43:22浏览次数:1  
标签:info 断点续传 setopt quot libcurl easy curl 多线程 CURLOPT

libcurl多线程下载一步步实现

创建时间: 2024年12月1日 17:35
标签: libcurl, linux, 下载, 多线程
最后编辑: 2025年1月16日 23:43

平台是WSL的Ubuntu22,使用Gcc编译。

单线程下载

编译命令gcc -o trans trans.c -lcurl

/* trans.c */
#include <curl/curl.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>

// 文件信息
struct fileInfo{
char* fileptr;
int offset;
};

// 回调函数,负责将数据写入文件
size_t writeFunc(void* ptr, size_t size, size_t memb, void* userdata) {
// ptr是请求到的数据,sizememb表示传输数据块的大小,userdata是我们传进来的参数,前面三个是固定的
struct fileInfo
info = (struct fileInfo)userdata;
memcpy(info->fileptr + info->offset, ptr, size
memb); // 每次拷贝sizememb的数据到fileptr,并将文件指针偏移sizememb
info->offset+=sizememb;
printf("%ld\n", size
memb);
return size*memb;
}

// 回调函数,负责消除curl_easy_setopt(curl, CURLOPT_HEADER, 1);将数据打印到控制台的行为
size_t writeNoFunc(void* ptr, size_t size, size_t memb, void* userdata) {

return size*memb;

}

// 获取下载文件的大小
double getDownloadFileLength(const char* url) {
double downloadFileLength = 0; // 创建变量记录文件长度,文件长度是个复数,其他类型变量无法正确接收文件长度
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERAGENT, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36&quot;);
curl_easy_setopt(curl, CURLOPT_HEADER, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY,1);  // 这两行指明执行时只获取响应头,不包含响应体
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeNoFunc);

CURLcode res =  curl_easy_perform(curl);
if(res == CURLE_OK) {
    curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &amp;downloadFileLength);
}else {
    downloadFileLength = -1;
}
curl_easy_cleanup(curl);

return downloadFileLength;

}

// 下载
int download(const char* url, const char* filename) {

long fileLength = getDownloadFileLength(url);
printf(&quot;fileLength: %ld\n&quot;, fileLength);
// write
int fd = open(filename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR );  // 打开文件
if(fd == -1) {
    return -1;
}

if(-1 == lseek(fd, fileLength-1, SEEK_SET))  // 开辟空间
{
    perror(&quot;lseek&quot;);
    close(fd);
    return -1;
}

if(1!=write(fd, &quot;&quot;, 1)){  // 写入&quot;&quot;就相当于写入\0,标志着字符串结尾
    perror(&quot;write&quot;);
    close(fd);
    return -1;
}

char* fileptr = (char*)mmap(NULL, fileLength, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0); // 将文件映射到内存,强转成(char*)是因为要向这块内存写入字符串
if(fileptr == MAP_FAILED){  // 映射失败处理
    perror(&quot;mmap&quot;);
    close(fd);
    return 1;
}

struct fileInfo* info = (struct fileInfo*)malloc(sizeof(struct fileInfo));
if(info == NULL){  // 结构体内存分配失败处理
    munmap(fileptr, fileLength);
    close(fd);
    return -1;
}

info-&gt;fileptr = fileptr;  // 让结构体中的fileptr指针指向映射文件的内存
info-&gt;offset = 0;

// curl
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);  // 将info 传入writeFunc进行操作

CURLcode res =  curl_easy_perform(curl);
if(res != CURLE_OK) {
    printf(&quot;res %d\n&quot;, res);
}
curl_easy_cleanup(curl);  // 清理curl
free(info);  // 释放结构体指针
close(fd);  // 关闭文件
munmap(fileptr, fileLength);  // 释放映射的内存

return 0;

}

int main() {
const char* url = "https://www.sordum.org/files/download/dns-jumper/DnsJumper.zip";
const char* filename = "./get.zip";
download(url, filename);
return 0;
}

下载完成后可以通过md5sum 文件名查看md5码来验证下载是否正确。

程序流程:

  • 获取要下载的文件大小 ,
  • open打开一个文件,返回文件描述符fd,
  • 通过lseek和write操作fd开辟出相当于要下载文件大小的空间,
  • 使用mmap将这块磁盘空间映射到内存,接着只需要使用指向这块内存的指针对这块内存读写即可。
  • 通过curl下载文件
  • 最后则是释放内存,清理资源

开辟空间的具体做法是,通过lseek将读写指针移动到合适的位置,然后通过write在该位置写入\0,这样就将这一块存储空间固定下来。

多线程版本

编译命令gcc -o trans trans.c -lcurl -lpthread,加入了pthread库,因此编译时添加-lpthread后缀

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

define THREAD_NUM 10

// 文件信息
struct fileInfo{
const char* url;
char* fileptr;
int offset;
int end;
pthread_t thid;
};

// 回调函数,负责将数据写入文件
size_t writeFunc(void* ptr, size_t size, size_t memb, void* userdata) {
// ptr是请求到的数据,sizememb表示传输数据块的大小,userdata是我们传进来的参数,前面三个是固定的
struct fileInfo
info = (struct fileInfo)userdata;
memcpy(info->fileptr + info->offset, ptr, size
memb); // 每次拷贝sizememb的数据到fileptr,并将文件指针偏移sizememb
info->offset+=sizememb;
printf("write: %ld\n", size
memb);
return size*memb;
}

// 回调函数,负责消除curl_easy_setopt(curl, CURLOPT_HEADER, 1);将数据打印到控制台的行为
size_t writeNoFunc(void* ptr, size_t size, size_t memb, void* userdata) {

return size*memb;

}

// 获取下载文件的大小
double getDownloadFileLength(const char* url) {
double downloadFileLength = 0; // 创建变量记录文件长度,文件长度是个复数,其他类型变量无法正确接收文件长度
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERAGENT, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36&quot;);
curl_easy_setopt(curl, CURLOPT_HEADER, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY,1);  // 这两行指明执行时只获取响应头,不包含响应体
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeNoFunc);

CURLcode res =  curl_easy_perform(curl);
if(res == CURLE_OK) {
    curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &amp;downloadFileLength);
}else {
    downloadFileLength = -1;
}
curl_easy_cleanup(curl);

return downloadFileLength;

}

// 线程函数
void* worker(void* arg){
struct fileInfo* info = (struct fileInfo*)arg;

char range[64] = {0};
snprintf(range, 64, &quot;%d-%d&quot;, info-&gt;offset, info-&gt;end);  //设置range字符数组的值
printf(&quot;threadId: %ld, download from %d to %d\n&quot;, info-&gt;thid, info-&gt;offset, info-&gt;end);
// curl
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, info-&gt;url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc); 
curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);  // 将info 传入writeFunc进行操作
curl_easy_setopt(curl, CURLOPT_RANGE, range);  // 根据range字符串指定获取的数据范围
CURLcode res =  curl_easy_perform(curl);
if(res != CURLE_OK) {
    printf(&quot;res %d\n&quot;, res);
}
curl_easy_cleanup(curl);  // 清理curl

return NULL;

}

// 下载
int download(const char* url, const char* filename) {

long fileLength = getDownloadFileLength(url);
printf(&quot;fileLength: %ld\n&quot;, fileLength);
// write
int fd = open(filename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR );  // 打开文件
if(fd == -1) {
    return -1;
}

if(-1 == lseek(fd, fileLength-1, SEEK_SET))  // 开辟空间
{
    perror(&quot;lseek&quot;);
    close(fd);
    return -1;
}

if(1!=write(fd, &quot;&quot;, 1)){  // 写入&quot;&quot;就相当于写入\0,标志着字符串结尾
    perror(&quot;write&quot;);
    close(fd);
    return -1;
}

char* fileptr = (char*)mmap(NULL, fileLength, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0); // 将文件映射到内存,强转成(char*)是因为要向这块内存写入字符串
if(fileptr == MAP_FAILED){  // 映射失败处理
    perror(&quot;mmap&quot;);
    close(fd);
    return 1;
}

// 多线程
int i=0;
long partSize = fileLength / THREAD_NUM;  // 分块
struct fileInfo* info[THREAD_NUM+1] = {0};  // 创建结构体指针数组
for(i=0; i&lt;=THREAD_NUM; i++){  // 给每个结构体指针分配内存,并设置其中的值
    info[i] = (struct fileInfo*)malloc(sizeof(struct fileInfo));
    info[i]-&gt;offset = i*partSize;
    if(i&lt;THREAD_NUM){
        info[i]-&gt;end = (i+1)*partSize - 1;
    }else{
        info[i]-&gt;end = fileLength-1;
    }
    info[i]-&gt;fileptr = fileptr;
    info[i]-&gt;url = url;
    // printf(&quot;offset:%d, end:%d\n&quot;,info[i]-&gt;offset, info[i]-&gt;end);
}

// 线程创建
for(i=0; i&lt;=THREAD_NUM; i++){
    pthread_create(&amp;(info[i]-&gt;thid), NULL, worker, info[i]);  // info[i[作为worker参数
}

// 线程启动
for(i=0; i&lt;=THREAD_NUM; i++){
    pthread_join(info[i]-&gt;thid, NULL);
}

// 释放结构体指针指向的内存
for(i=0; i&lt;=THREAD_NUM; i++){
    free(info[i]);  
}
close(fd);  // 关闭文件IO
munmap(fileptr, fileLength);  // 释放映射的内存

return 0;

}

int main() {
const char* url = "https://www.sordum.org/files/download/dns-jumper/DnsJumper.zip";
const char* filename = "./get.zip";
download(url, filename);
return 0;
}

程序流程:

  • getDownloadFileLength获取下载文件大小
  • 根据文件大小,使用open打开一个文件,并且通过lseek和write开辟磁盘空间
  • 使用mmap将文件映射到内存中,并将该内存块首地址强转为char*类型,以供写入数据
  • 将文件分块,程序中是分为10+1块,将文件总长度除以10,得到10块中每块的长度,1则是余下的文件大小
  • 创建线程函数worker,worker根据传入的参数来下载文件
  • 创建10+1个线程,每个线程写入一块文件
  • 启动多线程
  • 最后则是释放内存,清理资源

加入进度条

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

define THREAD_NUM 10

// 文件信息
struct fileInfo{
const char* url;
char* fileptr;
int offset;
int end;
pthread_t thid;
long fileLength;
};

// 进度条
int progress_bar(double x)
{
int i;
x*=10000;
x = ((double)((int)x))/100;
printf("\r\033[K");
printf("%.2f%%\t[", x);
for(i = 0; i < (x/10); i++)
{
printf("#");
}
if(x==100){
printf("]\n");
}else{
printf("]");
}
fflush(stdout);//立刻输出

return 0;

}

// 回调函数,负责将数据写入文件
size_t writeFunc(void* ptr, size_t size, size_t memb, void* userdata) {
// ptr是请求到的数据,sizememb表示传输数据块的大小,userdata是我们传进来的参数,前面三个是固定的
struct fileInfo
info = (struct fileInfo)userdata;
memcpy(info->fileptr + info->offset, ptr, size
memb); // 每次拷贝sizememb的数据到fileptr,并将文件指针偏移sizememb
info->offset+=sizememb;
// print progress bar
static double progress=0;
progress += size
memb;
double x = progress/(info->fileLength);
progress_bar(x);
return size*memb;
}

// 回调函数,负责消除curl_easy_setopt(curl, CURLOPT_HEADER, 1);将数据打印到控制台的行为
size_t writeNoFunc(void* ptr, size_t size, size_t memb, void* userdata) {

return size*memb;

}

// 获取下载文件的大小
double getDownloadFileLength(const char* url) {
double downloadFileLength = 0; // 创建变量记录文件长度,文件长度是个复数,其他类型变量无法正确接收文件长度
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERAGENT, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36&quot;);
curl_easy_setopt(curl, CURLOPT_HEADER, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY,1);  // 这两行指明执行时只获取响应头,不包含响应体
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeNoFunc);

CURLcode res =  curl_easy_perform(curl);
if(res == CURLE_OK) {
    curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &amp;downloadFileLength);
}else {
    downloadFileLength = -1;
}
curl_easy_cleanup(curl);

return downloadFileLength;

}

// 线程函数
void* worker(void* arg){
struct fileInfo* info = (struct fileInfo*)arg;

char range[64] = {0};
snprintf(range, 64, &quot;%d-%d&quot;, info-&gt;offset, info-&gt;end);  //设置range字符数组的值
printf(&quot;threadId: %ld, download from %d to %d\n&quot;, info-&gt;thid, info-&gt;offset, info-&gt;end);
// curl
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, info-&gt;url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc); 
curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);  // 将info 传入writeFunc进行操作
curl_easy_setopt(curl, CURLOPT_RANGE, range);  // 根据range字符串指定获取的数据范围
CURLcode res =  curl_easy_perform(curl);
if(res != CURLE_OK) {
    printf(&quot;res %d\n&quot;, res);
}
curl_easy_cleanup(curl);  // 清理curl

return NULL;

}

// 下载
int download(const char* url, const char* filename) {

long fileLength = getDownloadFileLength(url);
printf(&quot;fileLength: %ld\n&quot;, fileLength);
// write
int fd = open(filename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR );  // 打开文件
if(fd == -1) {
    return -1;
}

if(-1 == lseek(fd, fileLength-1, SEEK_SET))  // 开辟空间
{
    perror(&quot;lseek&quot;);
    close(fd);
    return -1;
}

if(1!=write(fd, &quot;&quot;, 1)){  // 写入&quot;&quot;就相当于写入\0,标志着字符串结尾
    perror(&quot;write&quot;);
    close(fd);
    return -1;
}

char* fileptr = (char*)mmap(NULL, fileLength, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0); // 将文件映射到内存,强转成(char*)是因为要向这块内存写入字符串
if(fileptr == MAP_FAILED){  // 映射失败处理
    perror(&quot;mmap&quot;);
    close(fd);
    return 1;
}

// 多线程
int i=0;
long partSize = fileLength / THREAD_NUM;  // 分块
struct fileInfo* info[THREAD_NUM+1] = {0};  // 创建结构体指针数组
for(i=0; i&lt;=THREAD_NUM; i++){  // 给每个结构体指针分配内存,并设置其中的值
    info[i] = (struct fileInfo*)malloc(sizeof(struct fileInfo));
    info[i]-&gt;offset = i*partSize;
    if(i&lt;THREAD_NUM){
        info[i]-&gt;end = (i+1)*partSize - 1;
    }else{
        info[i]-&gt;end = fileLength-1;
    }
    info[i]-&gt;fileptr = fileptr;
    info[i]-&gt;url = url;
    info[i]-&gt;fileLength = fileLength;
    // printf(&quot;offset:%d, end:%d\n&quot;,info[i]-&gt;offset, info[i]-&gt;end);
}

// 线程创建
for(i=0; i&lt;=THREAD_NUM; i++){
    pthread_create(&amp;(info[i]-&gt;thid), NULL, worker, info[i]);
}

// 线程启动
for(i=0; i&lt;=THREAD_NUM; i++){
    pthread_join(info[i]-&gt;thid, NULL);
}

// 释放结构体指针指向的内存
for(i=0; i&lt;=THREAD_NUM; i++){
    free(info[i]);  
}
close(fd);  // 关闭文件IO
munmap(fileptr, fileLength);  // 释放映射的内存

return 0;

}

void signal_handler(int signum){
printf("signum: %d\n", signum);
exit(1);
}

int main() {
const char* url = "https://www.sordum.org/files/download/dns-jumper/DnsJumper.zip";
const char* filename = "./get.zip";
if(SIG_ERR == signal(SIGINT, signal_handler))
{
perror("signal\n");
return -1;
}
download(url, filename);
return 0;
}

实现断点续传

  1. 思路:在下载过程中停止下载时,通过C标准库中的signal函数发出一个信号,在该信号的处理函数中,将当前下载情况记录到一个文件中,继续下载时打开该文件从文件中获取之前的下载情况继续下载。(实际逻辑是,每次下载时检查该文件是否存在,如果存在,就从该文件中记录的下载位置下载,不存在就从头开始下载。且一旦正常下载完成,就删除该文件)

  2. 同时要更改进度条,原因是libcurl传入到progressFunc函数中的totalDownload是根据 curl_easy_setopt(curl, CURLOPT_RANGE, range)中的range变量决定的,而range是一个“offset-end”形式的字符串,即totalDownload = end-offset;而nowDownload则是根据writeFunc的返回值累计而成的。也就是说nowDownload的取值范围是[0, end-offset]。需要说明的是,每个线程都有一个totalDownload。先前我们计算进度条的方式是将downloadFileLength作为分母,如果不需要断点续传,这样是没问题的。但是断点续传时,range的offset不再是从头开始,此时的offset是原来的offset加上了断点前下载的数据大小,这就导致新的end-offset得到的值小于原来的end-offset,想象一下,断点前的offset-end是0-100,下载到50时断开了,现在的offset-end是50-100,然而downloadFileLength是不变的,所以现在能得到的进度条变成【0-50】。

  3. 更改进度条的两种方式:

    一是更改进度条的分母,也就是总文件大小,将总文件大小改成断点续传时剩下的要下载的总文件大小。即分母不取总文件大小,而是取每个线程的totalDownload的和。

    二是将分子加上之前已下载的数据量。将断点的每个线程的offset的和减去初始的每个线程的offset和,就可以得到已下载的数据量。

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

define THREAD_NUM 10

// 文件信息
struct fileInfo{
const char* url;
char* fileptr;
int offset;
int end;
pthread_t thid;
double nowDownload;
double totalDownload;
FILE* recordFile;
};

struct fileInfo** pInfoTable; // 全局变量,指向结构体指针数组

// 回调函数,负责将数据写入文件
size_t writeFunc(void* ptr, size_t size, size_t memb, void* userdata) {
// ptr是请求到的数据,sizememb表示传输数据块的大小,userdata是我们传进来的参数,前面三个是固定的
struct fileInfo
info = (struct fileInfo)userdata;
memcpy(info->fileptr + info->offset, ptr, size
memb); // 每次拷贝sizememb的数据到fileptr,并将文件指针偏移sizememb
info->offset+=size*memb;

return size*memb;

}

// 回调函数,负责消除curl_easy_setopt(curl, CURLOPT_HEADER, 1);将数据打印到控制台的行为
size_t writeNoFunc(void* ptr, size_t size, size_t memb, void* userdata) {

return size*memb;

}

// 下载进度会传入该函数处理
int progressFunc(void* userdata, double totalDownload, double nowDownload, double totalUpload, double nowUpload)
{ // 每个线程都会单独调用progressFunc,并且获取到的nowDownload和totalDownload也是当前线程下载的文件块的下载信息
int percent = 0;
static int print = 0;
struct fileInfo* info = (struct fileInfo*)userdata;
info->nowDownload = nowDownload; //更新当前线程操作的结构体的download
info->totalDownload = totalDownload;

if(totalDownload&gt;0){
    int i=0;
    double allNowDownload = 0;
    double allTotalDownload = 0;
    for(i=0; i&lt;=THREAD_NUM; i++){//将所有线程,也就是info[0-10]的download加在一起
        allNowDownload+=pInfoTable[i]-&gt;nowDownload;
        allTotalDownload+=pInfoTable[i]-&gt;totalDownload;
    }

    percent = (int)(allNowDownload/allTotalDownload*100);
}
if(percent&gt;print){
    printf(&quot;percent: %d%%\n&quot;, percent);  // 由于一些原因,percent总体上趋于100,但是局部会有下降,因此这样写确保它是一直上升的
    print=percent;
}

// printf(&quot;nowDownload: %f, totalDownload: %f\n&quot;, nowDownload,totalDownload);

return 0;

}

// 获取下载文件的大小
double getDownloadFileLength(const char* url) {
double downloadFileLength = 0; // 创建变量记录文件长度,文件长度是个复数,其他类型变量无法正确接收文件长度
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERAGENT, &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36&quot;);
curl_easy_setopt(curl, CURLOPT_HEADER, 1);
curl_easy_setopt(curl, CURLOPT_NOBODY,1);  // 这两行指明执行时只获取响应头,不包含响应体
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeNoFunc);  

CURLcode res =  curl_easy_perform(curl);
if(res == CURLE_OK) {
    curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &amp;downloadFileLength);
}else {
    perror(&quot;downloadFileLength error\n&quot;);
    downloadFileLength = -1;
}
curl_easy_cleanup(curl);

return downloadFileLength;

}

// 线程函数
void* worker(void* arg){
struct fileInfo* info = (struct fileInfo*)arg;

char range[64] = {0};
if(info-&gt;recordFile){
    fscanf(info-&gt;recordFile, &quot;%d-%d&quot;, &amp;info-&gt;offset, &amp;info-&gt;end);  // fscanf每次读取会更新内部指针,下次读取会从上次读取的末尾开始,这就使得多个线程读取一个文件时不会读取到相同的数据,而是按线程启动的顺序读完整个文件
}
if(info-&gt;offset &gt; info-&gt;end) return NULL;  // 当一段文件被下载完时,offset = end+1,因此作判断返回
snprintf(range, 64, &quot;%d-%d&quot;, info-&gt;offset, info-&gt;end);  //设置range字符数组的值

printf(&quot;threadId: %ld, download from %d to %d\n&quot;, info-&gt;thid, info-&gt;offset, info-&gt;end);
// curl
CURL* curl = curl_easy_init();

curl_easy_setopt(curl, CURLOPT_URL, info-&gt;url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc); 
curl_easy_setopt(curl, CURLOPT_WRITEDATA, info);  // 将info 传入writeFunc进行操作

curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunc);
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, info);

curl_easy_setopt(curl, CURLOPT_RANGE, range);  // 根据range字符串指定获取的数据范围
CURLcode res =  curl_easy_perform(curl);
if(res != CURLE_OK) {
    printf(&quot;res %d\n&quot;, res);
}
curl_easy_cleanup(curl);  // 清理curl

return NULL;

}

// 下载
int download(const char* url, const char* filename) {

long fileLength = getDownloadFileLength(url);
printf(&quot;fileLength: %ld\n&quot;, fileLength);
// write
int fd = open(filename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR );  // 打开文件
if(fd == -1) {
    return -1;
}

if(-1 == lseek(fd, fileLength-1, SEEK_SET))  // 开辟空间
{
    perror(&quot;lseek&quot;);
    close(fd);
    return -1;
}

if(1!=write(fd, &quot;&quot;, 1)){  // 写入&quot;&quot;就相当于写入\0,标志着字符串结尾
    perror(&quot;write&quot;);
    close(fd);
    return -1;
}

char* fileptr = (char*)mmap(NULL, fileLength, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0); // 将文件映射到内存,强转成(char*)是因为要向这块内存写入字符串
if(fileptr == MAP_FAILED){  // 映射失败处理
    perror(&quot;mmap&quot;);
    close(fd);
    return -1;
}

FILE* fp = fopen(&quot;a.txt&quot;, &quot;r&quot;);  // 打开记录文件传输断点的文件

// 多线程
int i=0;
long partSize = fileLength / THREAD_NUM;  // 分块
struct fileInfo* info[THREAD_NUM+1] = {NULL};  // 创建结构体指针数组

for(i=0; i&lt;=THREAD_NUM; i++){  // 给每个结构体指针分配内存,并设置其中的值
    info[i] = (struct fileInfo*)malloc(sizeof(struct fileInfo));
    memset(info[i], 0, sizeof(struct fileInfo));  // 将info[i]指向的结构体中的所有值置为0
    info[i]-&gt;offset = i*partSize;
    if(i&lt;THREAD_NUM){
        info[i]-&gt;end = (i+1)*partSize - 1;
    }else{
        info[i]-&gt;end = fileLength-1;
    }
    info[i]-&gt;fileptr = fileptr;
    info[i]-&gt;url = url;
    info[i]-&gt;recordFile = fp;
    // printf(&quot;offset:%d, end:%d\n&quot;,info[i]-&gt;offset, info[i]-&gt;end);
}
pInfoTable = info;
// 线程创建
for(i=0; i&lt;=THREAD_NUM; i++){
    pthread_create(&amp;(info[i]-&gt;thid), NULL, worker, info[i]);
    usleep(1);
}

// 线程启动
for(i=0; i&lt;=THREAD_NUM; i++){
    pthread_join(info[i]-&gt;thid, NULL);
}

// 释放结构体指针指向的内存
for(i=0; i&lt;=THREAD_NUM; i++){
    free(info[i]);  
}
if(fp){  // 验证a.txt是否存在,正常下载的情况下没断开过下载的情况下,不会产生a.txt,则fp=NULL,此时fclose(fp)会报错,因此要先验证fp是否存在
    fclose(fp);
    if(remove(&quot;a.txt&quot;) !=0){  // 文件正常下载完成且a.txt存在才会执行到这里,删除a.txt
        perror(&quot;error delete a.txt!&quot;);
    }
}
close(fd);  // 关闭文件IO
munmap(fileptr, fileLength);  // 释放映射的内存


return 0;

}

// 断开下载时,会执行以下代码
void signal_handler(int signum){
printf("signum: %d\n", signum);
int i=0;
int fd = open("a.txt", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
if(fd==-1)
{
exit(1);
}
for(i=0; i<=THREAD_NUM; i++)
{ // 把断开下载时,每个线程的下载情况写入a.txt
char range[64] = {0};
snprintf(range, 64, "%d-%d\r\n", pInfoTable[i]->offset, pInfoTable[i]->end);

    write(fd, range, strlen(range));
}
close(fd);
exit(1);

}

// 根据URL创建文件名
void getFileName(const char* url, char** filename){
// const char* delim = '/';
const char* lastSlash = strrchr(url, '/');
if(lastSlash!=NULL){
filename = (char)malloc(strlen(lastSlash+1)+3);
strcpy(filename, ++lastSlash);
}
printf("%s\n", lastSlash);
size_t len = strlen(
filename);
(filename)[len-4] = 'c';
(
filename)[len-3] = 'p';
(filename)[len-2] = 'y';
(
filename)[len-1] = lastSlash[len-4];
(filename)[len] = lastSlash[len-3];
(
filename)[len+1] = lastSlash[len-2];
(filename)[len+2] = lastSlash[len-1];
(
filename)[len+3] = '\0';
}

int main() {

// const char* url = &quot;https://www.sordum.org/files/download/dns-jumper/DnsJumper.zip&quot;;
const char* url = &quot;https://uu.gdl.netease.com/5051/UU-5.30.1.exe&quot;;
char* filename;
getFileName(url, &amp;filename);
if(SIG_ERR == signal(SIGINT, signal_handler))  // 断开下载时调用signal_handler
{
    perror(&quot;signal\n&quot;);
    return -1;
}
download(url, filename);
return 0;

}

常用函数

  1. 初始化和清理
    • curl_global_init():初始化libcurl环境,通常在程序开始时调用一次。
    • curl_global_cleanup():清理libcurl环境,通常在程序结束时调用。
    • curl_easy_init():初始化一个CURL类型的指针,用于创建一个新的curl会话。
    • curl_easy_cleanup():释放curl会话资源。
  2. 设置选项
    • curl_easy_setopt1:用于设置curl​会话的选项。
    • ​curl_easy_getinfo()2:用于获取curl​会话的信息。
  3. 执行请求:
    • curl_easy_perform():执行一个curl会话的请求。

问题记录

curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);  // 写入响应体数据(body)
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &file);  // 写入响应头数据(header)

在linux环境下,不会出现任何问题。但是在windows下配置的linux编译环境中,会有问题产生。 这两行代码,只执行第一行时,可以正常将body的数据写入文件,都执行则可以将header和body的数据都写入文件。但是只执行第二行的代码,文件中将不会写入任何数据。

想要单独获取响应头信息,需要通过
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);,设置处理头信息的回调函数。回调函数的格式如下:

size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) {
    // 这里处理头部数据,例如打印或者保存到文件
    printf("Header: %.*s", (int)(nitems * size), buffer);
    return nitems * size;
}

但是,实际情况依然是无法单独得到响应头。

curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &file);

如果只执行上面两行代码,则会将响应体的内容打印出来,而响应头的位置则是空行。

总结

curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); 
// curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &file);
res = curl_easy_perform(curl); 

对于以上五行代码

  • 注释掉第二行的情况下,可以将响应头和响应体都写入到文件中(正常状态)
  • 注释掉第二、三行的情况下,文件中写入空行
  • 注释掉第一、三行时,会将响应体打印到控制台,而响应头的位置是空行
  • 不注释,则会将响应体写入到文件,控制台打印空行

  1. curl_easy_setopt详细介绍

    curl_easy_setopt详细介绍

    1. curl_easy_setopt(curl, CURLOPT_URL, info->url);

      设置请求的url

    2. curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunc)

      1. CURLOPT_WRITEFUNCTION设置一个自定义的回调函数writeFunc来处理从服务器下载的数据,传入的writeFunc是函数指针
      2. writeFunc 的原型 size_t writeFunc(void *ptr, size_t size, size_t nmemb, void *stream);
        • ptr:指向包含接收到的数据的内存块的指针。
        • size:每个数据块的大小。
        • nmemb:数据块的数量。
        • stream:一个用户定义的指针,通常用来传递额外的数据给回调函数,比如可以将文件描述符传送进来,就可以将数据写入文件
        • **size*nmemb**是每次下载的数据长度,并且要将该值返回 return **size*nmemb**
    3. curl_easy_setopt(curl,cURLoPT_WRITEDATA,info);

      • info传入给writeFunc处理,info就是writeFunc中的stream
    4. curl_easy_setopt(curl, CURLOPT_NOPROGRESS, OL);

      • CURLOPT_NOPROGRESS 是一个选项,用于控制是否显示进度信息。当设置 CURLOPT_NOPROGRESS0L(即 long 类型的 0),libcurl 将显示进度信息;如果你设置为 1L(即 long 类型的 1),libcurl 将不显示进度信息。
    5. curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunc)

      1. CURLOPT_PROGRESSFUNCTION:指定了当 libcurl 在传输数据时应该调用的进度回调函数。progressFunc值应该是一个指向函数的指针
      2. progressFunc原型
        int progressFunc(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
        • userdata是一个用户定义的指针,可以在回调函数中使用。
        • dltotaldlnow 分别表示下载的总字节数和当前已下载的字节数。
        • ultotalulnow 分别表示上传的总字节数和当前已上传的字节数。
    6. curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, info)

      • info传递给progressFunc中的userdata
    7. curl_easy_setopt(curl, CURLOPT_RANGE, range);

      1. 这行代码是用于设置 CURL 选项,使得 CURL 请求时可以指定下载文件的特定范围CURLOPT_RANGE 选项允许你指定想要检索的数据范围,格式通常是 "X-Y",其中 X 和 Y 是字节索引,X 和 Y 都可以省略。内容对于 HTTP 传输,还支持多个间隔,用逗号分隔,如 "X-Y,N-M"。
      2. 例如,如果你想请求一个文件的前200个字节,可以设置range字符串为 为 "0-199"。如果你想从第200个字节开始获取文件的其余部分,可以设置为 "200-"。还可以请求多个范围,如 "0-199,1000-1199",这将请求文件的前200个字节和从1000字节开始的200个字节。
      3. 需要注意的是,服务器是否支持范围请求(byte range)取决于服务器的配置。即使客户端设置了 CURLOPT_RANGE,服务器也可能因为各种原因不遵守这个请求,而是返回完整的响应内容。因此,使用这个选项时,应当准备好处理完整的响应内容,而不仅仅是请求的范围。
      4. int progressFunc(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)中的dltotal = Y-X,dlnow则是从0开始,不断加上size_t writeFunc(void *ptr, size_t size, size_t nmemb, void *stream)的返回值,最高等于Y-X。
  2. curl_easy_getinfo

    curl_easy_getinfo

    上次编辑时间: 2024年12月6日 18:00
    创建时间: 2024年12月6日 14:46

    CURLINFO_CONTENT_LENGTH_DOWNLOADCURLINFO_CONTENT_LENGTH_DOWNLOAD_T 都是 libcurl 提供的选项,用于获取下载内容的长度。它们的主要区别在于返回值的类型和对大文件的支持。

    1. CURLINFO_CONTENT_LENGTH_DOWNLOAD
      • 这个选项要求传递一个 double 类型的指针,用来存放下载内容的 Content-Length 信息。
      • 如果文件大小无法获取,那么函数返回值为 1
      • 这个选项在 libcurl 7.19.4 版本之后,如果大小未知会返回 1
      • 这个选项已经被标记为弃用(Deprecated since 7.55.0)。
    2. CURLINFO_CONTENT_LENGTH_DOWNLOAD_T
      • 这个选项是 CURLINFO_CONTENT_LENGTH_DOWNLOAD 的更新替代品,它要求传递一个 curl_off_t 类型的指针,curl_off_t 是一个更大的数据类型,能够支持更大的文件大小。
      • 这个选项在 libcurl 7.55.0 版本中被加入,用于替代 CURLINFO_CONTENT_LENGTH_DOWNLOAD
      • 如果内容长度未知,这个选项也会返回 1
      • 这个选项专门用于 HTTP 协议,并且能够更准确地处理大文件的 Content-Length

    也就是说,CURLINFO_CONTENT_LENGTH_DOWNLOAD_T 是一个更新的选项,它使用 curl_off_t 类型来支持更大的文件大小,并且是 CURLINFO_CONTENT_LENGTH_DOWNLOAD 的替代品。

    示例:

    int main(void) {
        CURL *curl = curl_easy_init();
        if(curl) {
            CURLcode res;
            curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
            /* Perform the request */
            res = curl_easy_perform(curl);
            if(!res) {
                /* check the size */
                double cl;
                res = curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &cl);
                if(!res) {
                    printf("Size: %.0f\n", cl);
                }
            }
        }
    }
    

标签:info,断点续传,setopt,quot,libcurl,easy,curl,多线程,CURLOPT
From: https://www.cnblogs.com/xue3z/p/18676982/libcurl-multi-threaded-download-supports-breakpoi

相关文章

  • 在线客服系统 QPS 突破 240/秒,连接数突破 4000,日请求数接近1000万次,.NET 多线程技术的
    背景我在业余时间开发了一款自己的独立产品:升讯威在线客服与营销系统。陆陆续续开发了几年,从一开始的偶有用户尝试,到如今的QPS突破240次/秒,连接数突破4000,日请求数接近1000万。(PS:阿里云真贵啊)在这篇文章中,我将简要介绍我在技术上做了哪些工作,我是如何做到的。PS:虽......
  • 11 多线程详解
    线程简介程序:程序就像一堆写好的指令和数据放在一起,它是静止的,不会自己动起来。进程(Process):进程是把程序真正运行起来的过程,它是动态的,系统会给它分配各种资源,比如内存等。线程(Thread):一个进程里通常会有好几个线程,最少也得有一个,不然进程就没啥用了。线程是CPU安排干活和实际......
  • 27. 多线程技术
    一、多线程技术  QThread类是PySide中的核心线程类,要实现一个线程,需要创建QThread类的有一个子类,并且实现其run()方法。  线程也有自己自己的生命周期,其中包含5种状态,分别为:新建状态、就绪状态、运行状态、阻塞状态**和死亡状态。新建状态就是线程被创建时的状......
  • Java多进程多线程处理详解
    在Java编程中,多进程和多线程是两种常见的并发编程技术,用于提高程序的执行效率和响应速度。本文将详细介绍Java中的多进程和多线程处理,包括理论概述和代码示例。通过本文,你将了解如何在Java中实现多进程和多线程,以及它们在实际应用中的价值和意义。一、理论概述1.多进程与多线程......
  • 多线程同步与任务完成等待机制总结
    多线程同步与任务完成等待机制总结在多线程编程中,合理的同步机制能够有效地协调多个线程之间的执行顺序,确保任务按照预期执行。常见的同步机制包括CountDownLatch、CyclicBarrier、CompletableFuture、ExecutorService.invokeAll()和Phaser。接下来,我们将通过具体场景加......
  • c语言——【linux】多线程编程 (内附练习及代码)
    1:开启一个线程主线程中:使用标准IO,向一个文件中写入任意数据分支线程:使用标准IO,读取该文件中的数据#include<stdio.h>#include<string.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<p......
  • 多线程的实现
    1.线程和进程的概述1.1什么是进程?正在执行的程序,称为进程。进程是系统分配资源的单元。现在系统支持多进程的。1.2什么是线程?线程,又称轻量级进程(LightWeightProcess)。线程是进程中的一条执行路径,也是CPU的基本调度单位。若一个程序可同一时间执行多个线程,就是支......
  • 【git】Qualcomm 代码clone失败出现RProtocol https not supported or disabled in li
    问题描述    在尝试从https://服务器(ChipCode是)克隆任何内容时收到此输出,则表示您正在使用的curl/libcurl实例是在不支持此协议的情况下构建的。如果在构建时运行的configure脚本找不到curl使SSL工作所需的所有库和包含文件,则可能会发生这种情况。如果conf......
  • 进程、线程,java如何实现多线程任务,Thread里面常用的方法
    1.线程和进程的概述1.1什么是进程正在执行的程序,称为进程。进程是系统分配资源的单元。1.2什么是线程线程又称轻量级的进程,是CPU调度的基本单位,一个进程由一个或多个线程组成进程与进程之间无法共享数据同一个进程的线程与线程之间可以共享数据2.并发与并行的概述并......
  • Qt多线程开发全解析
     目录一、引言二、Qt多线程基础概念2.1线程与进程2.2Qt中的线程类三、Qt多线程的使用场景3.1耗时操作处理3.2实时数据处理3.3多任务并发执行四、Qt多线程的实现方式4.1继承QThread类4.2使用QThreadPool线程池4.3利用QtConcurrent框架五、线程间的同步......