之前的文章《curl编程实例-上传文件》,介绍过如何使用curl编程来上传文件,是通过指定文件路径的方式来上传,关键的特征如下:
constchar*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;:目前已上传的大小
typedefstruct{constchar*filename;size_ttotalSize;size_tuploadedSize;}UploadInfo_t;1.2 读回调的定义
curl的读回调,会自动给出每次需要上传的数据大小,回调中需要做的,就是根据回调参数中指定的需要上传的大小,将文件的分段内容,写入指定的缓冲区,然后将已上传的大小,记录到前面定义的UploadInfo_t结构中。
size_tfile_read_cb(char*buf,size_tsize,size_tn,void*uploadInfo){if(!uploadInfo){return0;}UploadInfo_t*info=(UploadInfo_t*)uploadInfo;size_tbufferSize=size*n;// 此次需要上传的大小curl_off_tremaining=info->totalSize-info->uploadedSize;if(remaining<=0){return0;// 已读完,结束传输}if(bufferSize>(size_t)remaining){bufferSize=(size_t)remaining;}size_tbytesRead=custom_read_file(info->filename,info->uploadedSize,bufferSize,buf);if(bytesRead==0){return0;}info->uploadedSize+=bytesRead;floatprogress=(float)info->uploadedSize/info->totalSize*100;printf("[%s] read:%zu bytes, progress:%.1f%%\n",__func__,bytesRead,progress);returnbytesRead;}1.3 分段读取举例
分段读取的实现形式有很多,比如通过自定义的接口,从自定义的内存缓冲区,或其它自定义的方式,进行读取。
这里只是演示分段读取的过程,就还以fopen读文件的方式举例,再通过fseeko进行偏移,从而实现从文件的指定位置读取指定长度的内容。
size_tcustom_read_file(constchar*file_path,size_toffset,size_tread_len,char*buffer){size_tactual_read=0;// 这里只是使用fopen举例,实际可以是任何形式的文件读取FILE*fp=fopen(file_path,"rb");if(!fp){printf("fopen:%s err\n",file_path);return0;}// 定位到指定偏移量:SEEK_SET 表示从文件开头计算偏移intseek_ret=fseeko(fp,offset,SEEK_SET);if(seek_ret!=0){printf("fseeko failed: offset=%lld\n",(longlong)offset);gotoEND;}// 读取指定长度的数据到缓冲区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);}elseif(ferror(fp)){printf("fread failed");}}END:fclose(fp);returnactual_read;}2 完整代码
完整代码如下,是在之前那篇《curl编程实例-上传文件》的基础上进行修改的。
// 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>// 用于提取原始文件名// 进度回调函数staticintupload_progress(void*p,curl_off_tdltotal,curl_off_tdlnow,curl_off_tultotal,curl_off_tulnow){if(ultotal>0){// 计算进度百分比,限制最大值为100%(实际的上传数据包含了HTTP头部等数据)floatprogress=(ulnow*100.0)/ultotal;if(progress>100.0)progress=100.0;printf("progress: %lld/%lld (%.2f%%)\n",(longlong)ulnow,(longlong)ultotal,progress);}return0;}// 提取文件路径中的原始文件名(兼容绝对/相对路径)char*get_original_filename(constchar*file_path){if(!file_path){returnstrdup("unknown_file.dat");}char*path_copy=strdup(file_path);char*filename=basename(path_copy);char*result=strdup(filename);free(path_copy);returnresult;}// 获取文件实际大小(字节)curl_off_tget_file_size(constchar*file_path){if(!file_path){return-1;}structstatst;if(stat(file_path,&st)==-1){printf("stat %s failed\n",file_path);return-1;}return(curl_off_t)st.st_size;}typedefstruct{constchar*filename;size_ttotalSize;size_tuploadedSize;}UploadInfo_t;size_tcustom_read_file(constchar*file_path,size_toffset,size_tread_len,char*buffer){size_tactual_read=0;// 这里只是使用fopen举例,实际可以是任何形式的文件读取FILE*fp=fopen(file_path,"rb");if(!fp){printf("fopen:%s err\n",file_path);return0;}// 定位到指定偏移量:SEEK_SET 表示从文件开头计算偏移intseek_ret=fseeko(fp,offset,SEEK_SET);if(seek_ret!=0){printf("fseeko failed: offset=%lld\n",(longlong)offset);gotoEND;}// 读取指定长度的数据到缓冲区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);}elseif(ferror(fp)){printf("fread failed");}}END:fclose(fp);returnactual_read;}size_tfile_read_cb(char*buf,size_tsize,size_tn,void*uploadInfo){if(!uploadInfo){return0;}UploadInfo_t*info=(UploadInfo_t*)uploadInfo;size_tbufferSize=size*n;// 此次需要上传的大小curl_off_tremaining=info->totalSize-info->uploadedSize;if(remaining<=0){return0;// 已读完,结束传输}if(bufferSize>(size_t)remaining){bufferSize=(size_t)remaining;}size_tbytesRead=custom_read_file(info->filename,info->uploadedSize,bufferSize,buf);if(bytesRead==0){return0;}info->uploadedSize+=bytesRead;floatprogress=(float)info->uploadedSize/info->totalSize*100;printf("[%s] read:%zu bytes, progress:%.1f%%\n",__func__,bytesRead,progress);returnbytesRead;}intmain(intargc,char*argv[]){CURL*curl=NULL;CURLcode res;FILE*fp=NULL;constchar*upload_url="http://192.168.5.104:8080/upload";// 文件服务器的地址constchar*file_path="./test.jpg";// 要上传的本地文件structcurl_slist*headers=NULL;// 自定义请求头// 文件上传的信息UploadInfo_t uploadInfo={0};uploadInfo.filename=file_path;// 获取文件大小size_tfile_size=get_file_size(file_path);if(file_size<0){printf("%s file_size:%zu err\n",file_path,file_size);gotocleanup;}printf("%s file_size: %zu\n",file_path,file_size);uploadInfo.totalSize=file_size;// 初始化libcurlcurl_global_init(CURL_GLOBAL_ALL);curl=curl_easy_init();if(!curl){printf("curl_easy_init, err\n");gotocleanup;}// 构建自定义请求头(传递原始文件名)char*original_filename=get_original_filename(file_path);charheader_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:");// 设置上传URLcurl_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();returnres==CURLE_OK?0:1;}3 运行结果
测试环境:
- 在windows电脑上启动一个文件服务器,可参考之前的文章《curl编程实例-上传文件》
- 在ubuntu虚拟机上启动文件上传程序
可以看到,文件通过回调的方式,多次分段上传,最终的windows电脑的文件服务器的指定目录,可以看到上传的文件
4 总结
本篇介绍了如何使用curl的C语言编程,读回调的方式,实现文件的分段上传,并通过代码实例,验证上传的结果。