阿里云OSS对象存储上传文件(二)C++上传(含代码)
因为实际项目需求,需要使用阿里云oss的对象存储来上传文件。本篇将讲述个人代码使用上的经验,不代表适用所有人,仅供参考。
代码尝试前,请先确保SDK安装和lib文件编译等预备工作,详细可查看上一篇文章:
阿里云OSS对象存储上传文件(一)SDK安装
环境是windows系统,vs2017+qt5.14(vs2015+qt5.6也尝试过,效果都一样)
官方示例代码:官方文档:C++上传文件
一、核心需求
简单查看官方文档后,会发现它具备了好几种上传方法,包括简单上传,追加上传,断点续传上传,分片上传,等。因为我的项目核心需求是同时上传单个文件,所以过程中尝试了简单上传和分片上传,其他未做尝试,有兴趣可自行实现。
另一要求则是过程中需要给出进度条的ui提示,这需要获取oss的进度条反馈信息,这部分sdk也是有实现的。
以下先简单讲述以下简单上传和分片上传的优缺点。
(1)简单上传
即直接单次提交整个文件,只要不超过5GB即可。这种方法步骤比较简单,但同时没有办法实现中途中断功能(有人实现了可在评论区告知),并可能因为文件过大,而导致上传过程阻塞过久,往往可能一两分钟,又没办法停止,体验一般。
(2)分片上传
即把文件分成若干片段,分别上传。这种适合大文件的上传,最大不超过48.8TB。当然,几十MB大小的文件也同样可以上传,这个都没有问题。相较于简单上传,分片上传的灵活性显然提高了许多。如果想要实现停止上传,只要完成当前分片的上传把并退出上传循环即可。因为没有到最终的整合分片环节,所以云上的文件并不会最终生成(有没有缓存文件不知道)。
特别说明:
(1)值得一提的是,官方示例中的分片大小是100k,这会造成分片数量太过庞大,导致循环上传中各种初始化资源大量重复。比如上传一个100M的视频文件,用简单上传来整个文件上传,以及使用分片上传,按照100k分片,循环上传1000次,耗时上是有显著增加的。经过尝试,我认为控制在1M的分片会比较恰当,网络通畅的情况下几乎是瞬间上传完毕,不会造成明显的阻塞感。
(2)官方文档中有进度条回调的使用,但注意,不管是对简单上传还是分片上传,它的本质上都是对单次上传的进度条反抗,放在分片上传中,就是对单个切片的进度反馈,而不是一整个的。这在我实现分片上传中的进度条显示中遇到了困难。
a.初步的尝试,是利用切片的数量进行进度显示,即假如分片了100份,那当前上传到哪一片,自然就得到了当前的进度。当你知道的,切片不一定会超过100份,假如只有10份,那进度反馈将是跳跃式的,给人直观上不好。
b.最终的方案是,利用一开始对文件总大小的记录,以及当前上传切片编号,当前上传切片的进度反馈,自行实现数字累加,以达到真实的进度反馈。这部分比较繁琐,但也算是符合常理的。
OK,讲了这么多,接下来详细讲述以下代码。
二、简单上传
//进度条回调
void ProgressCallback(size_t increment, int64_t transfered, int64_t total, void* userData)
{
std::cout << "ProgressCallback[" << userData << "] => " <<
increment <<" ," << transfered << "," << total << std::endl;
//alioosProgressCallback是我创建的全局回调对象,在这里间接地将进度信息发送到外部
alioosProgressCallback.send_ProgressCallback(transfered, total);
}
//简单上传
bool AliossUpload::PutObjectFromFile()
{
//根据自己实际情况,初始OSS账号信息
/* 初始化OSS账号信息 */
/* 如何获取AccessKeyId和AccessKeySecret:
https://help.aliyun.com/knowledge_detail/48699.html
*/
std::string AccessKeyId = _AccessKeyId.toStdString();
std::string AccessKeySecret = _AccessKeySecret.toStdString();
/* 根据文档填写所需Endpoint:
https://help.aliyun.com/document_detail/31837.html?spm=a2c4g.11186623.6.586.48977f5ev3c5Ht
*/
std::string Endpoint = _Endpoint.toStdString();
/* 填写你的存储空间名*/
std::string BucketName = _BucketName.toStdString();
/* ObjectName表示上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如image/bkg1.png文件 */
std::string ObjectName = std::string(_ObjectName.toLocal8Bit());
/* 本地文件实际路径,注意\要替换为/,不然中文路径会出问题 */
std::string FilePathAndName = std::string(_FilePathAndName.toLocal8Bit());
/* 初始化网络等资源 */
InitializeSdk();
/* 基本参数 */
ClientConfiguration conf;
OssClient client(Endpoint, AccessKeyId, AccessKeySecret, conf);
std::shared_ptr<std::iostream> content = std::make_shared<std::fstream>(FilePathAndName, std::ios::in|std::ios::binary);
PutObjectRequest request(BucketName, ObjectName, content);
//进度回调
TransferProgress progressCallback = { ProgressCallback , this };
request.setTransferProgress(progressCallback);
/* 上传文件 */
auto outcome = client.PutObject(request);
bool ret = true;
if (!outcome.isSuccess()) {
/* 异常处理 */
std::cout << "PutObject fail" <<
",code:" << outcome.error().Code() <<
",message:" << outcome.error().Message() <<
",requestId:" << outcome.error().RequestId() << std::endl;
ret = false;
}
/* 释放网络等资源 */
ShutdownSdk();
qDebug()<<"upload oss"<<ret;
return ret;
}
简单上传较为简单,而且也是实现分片上传的基础代码,注意一下中文路径的问题即可。
二、分片上传
一下子好像复杂了很多,哈哈,没关系,我们慢慢看。
简单来说就是,初始化、循环上传、最终整合文件并释放资源,三个步骤。
(multipart_progress_type=1和=2分别对应两种进度条实现的方式)
//进度回调
void ProgressCallback(size_t increment, int64_t transfered, int64_t total, void* userData)
{
std::cout << "ProgressCallback[" << userData << "] => " <<
increment <<" ," << transfered << "," << total << std::endl;
//alioosProgressCallback是我创建的全局回调对象,在这里间接地将进度信息发送到外部
/*实时计算当前进度条信息,其中设置了好几个临时的中间变量,
其实都是利用当前切片数量(编号),当前切片的上传进度,整个文件的大小来做计算的
*/
alioosProgressCallback.multipart_current_transfered_size -= alioosProgressCallback.multipart_last_add_size;
alioosProgressCallback.multipart_current_transfered_size += transfered;
alioosProgressCallback.multipart_last_add_size = transfered;
alioosProgressCallback.send_ProgressCallback(alioosProgressCallback.multipart_current_transfered_size, alioosProgressCallback.multipart_file_size);
}
bool AliossUpload::MultipartUploadFileFromFile()
{
//置真上传标志
is_upload = true;
/* 初始化OSS账号信息 */
std::string AccessKeyId = _AccessKeyId.toStdString();
std::string AccessKeySecret = _AccessKeySecret.toStdString();
std::string Endpoint = _Endpoint.toStdString();
/* 填写Bucket名称,例如examplebucket */
std::string BucketName = _BucketName.toStdString();
/* ObjectName表示上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如image/bkg1.png文件 */
std::string ObjectName = std::string(_ObjectName.toLocal8Bit());
/* 本地文件实际路径,注意\要替换为/,不然中文路径会出问题 */
std::string FilePathAndName = std::string(_FilePathAndName.toLocal8Bit());
/* 初始化网络等资源 */
InitializeSdk();
/* 基本参数 */
ClientConfiguration conf;
OssClient client(Endpoint, AccessKeyId, AccessKeySecret, conf);
InitiateMultipartUploadRequest initUploadRequest(BucketName, ObjectName);
/*(可选)请参见如下示例设置存储类型 */
//initUploadRequest.MetaData().addHeader("x-oss-storage-class", "Standard");
/* 初始化分片上传事件 */
auto uploadIdResult = client.InitiateMultipartUpload(initUploadRequest);
auto uploadId = uploadIdResult.result().UploadId();
std::string fileToUpload = FilePathAndName;
int64_t partSize = UPLOAD_MAX;//1024*1024
PartList partETagList;
auto fileSize = getFileSize(fileToUpload);
int partCount = static_cast<int>(fileSize / partSize);
/* 计算分片个数 */
if (fileSize % partSize != 0) {
partCount++;
}
qDebug()<<"fileSize"<<static_cast<int64_t>(fileSize);
qDebug()<<"partCount"<<partCount;
if(multipart_progress_type == 1){
//利用分片总数和当前上传分片数来当做进度反馈,这样不需要在回调函数做处理
emit sig_upload_progress(0, partCount);
}else if(multipart_progress_type == 2){
//初始化进度回调类,额外自行计算(比较推荐)
alioosProgressCallback.set_progress_type(multipart_progress_type);
alioosProgressCallback.set_multipart_progress_cfg(fileSize, 0, 0);
}
/* 对每一个分片进行上传 */
for (int i = 1; i <= partCount; i++) {
if(!is_upload){
break; //中途中断
}
auto skipBytes = partSize * (i - 1);
auto size = (partSize < fileSize - skipBytes) ? partSize : (fileSize - skipBytes);
std::shared_ptr<std::iostream> content = std::make_shared<std::fstream>(fileToUpload, std::ios::in|std::ios::binary);
content->seekg(skipBytes, std::ios::beg);
UploadPartRequest uploadPartRequest(BucketName, ObjectName, content);
uploadPartRequest.setContentLength(size);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setPartNumber(i);
//针对每一个分片做的回调,需要在回调函数中详细计算当前进度值
if(multipart_progress_type == 2){
TransferProgress progressCallback = { ProgressCallback , this };
uploadPartRequest.setTransferProgress(progressCallback);
//设置当前编号,重置状态
alioosProgressCallback.set_multipart_progress_current_part_num(i);
}
auto uploadPartOutcome = client.UploadPart(uploadPartRequest);
if (uploadPartOutcome.isSuccess()) {
Part part(i, uploadPartOutcome.result().ETag());
partETagList.push_back(part);
}
else {
std::cout << "uploadPart fail" <<
",code:" << uploadPartOutcome.error().Code() <<
",message:" << uploadPartOutcome.error().Message() <<
",requestId:" << uploadPartOutcome.error().RequestId() << std::endl;
}
//根据当前上传分片数,发送进度值
if(multipart_progress_type == 1){
emit sig_upload_progress(i, partCount);
}
}
/* 完成分片上传 */
CompleteMultipartUploadRequest request(BucketName, ObjectName);
request.setUploadId(uploadId);
request.setPartList(partETagList);
/*(可选)请参见如下示例设置读写权限ACL */
//request.setAcl(CannedAccessControlList::Private);
auto outcome = client.CompleteMultipartUpload(request);
bool ret = true;
if (!outcome.isSuccess()) {
/* 异常处理 */
std::cout << "CompleteMultipartUpload fail" <<
",code:" << outcome.error().Code() <<
",message:" << outcome.error().Message() <<
",requestId:" << outcome.error().RequestId() << std::endl;
ret = false;
}else{
qDebug()<<"MultipartUploadFileFromFile succeed...";
}
/* 释放网络等资源 */
ShutdownSdk();
return ret;
}
三、示例代码文件,及初始化使用
资源在文章顶部有,请自行尝试,代码比较杂乱,多多包涵。
封装成文件后,就可以在外部使用啦。像这样多线程运行,也不会阻塞,释放资源也是OK的。
void XXX::upload_record_file_alioss(QString AccessKeyId, QString AccessKeySecret, QString Endpoint, QString BucketName, QString ObjectName, QString FilePathAndName)
{
if(!ptr_aliossUpload && !ptr_aliossUpload_thread){
ptr_aliossUpload = new AliossUpload();
connect(ptr_aliossUpload, &AliossUpload::sig_upload_stop, this, &XXX::slot_record_oss_upload_stop);
connect(ptr_aliossUpload, &AliossUpload::sig_upload_error, this, &XXX::slot_record_oss_upload_error);
connect(ptr_aliossUpload, &AliossUpload::sig_upload_finish, this, &XXX::slot_record_oss_upload_finish);
connect(ptr_aliossUpload, &AliossUpload::sig_upload_progress, this, &XXX::slot_record_oss_upload_ProgressCallback);
ptr_aliossUpload->set_upload_cfg(AccessKeyId, AccessKeySecret, Endpoint, BucketName, ObjectName, FilePathAndName);
ptr_aliossUpload_thread = new QThread();
ptr_aliossUpload->moveToThread(ptr_aliossUpload_thread);
connect(ptr_aliossUpload_thread, &QThread::started, ptr_aliossUpload, &AliossUpload::start);
connect(ptr_aliossUpload_thread, &QThread::finished,this,[=](void)mutable{
qDebug()<<"QThread-finished...";
delete ptr_aliossUpload;
ptr_aliossUpload = nullptr;
delete ptr_aliossUpload_thread;
ptr_aliossUpload_thread = nullptr;
});
ptr_aliossUpload_thread->start();
}
}
四、疑问
进度条的回调还是有些搞不明白,我对回调函数其实理解不深,不太明白为什么不能使用类成员函数来进行回调,导致我额外创建了个中间类来发送信号,感觉特别别扭,不知道还有没有其他的实现方式。
另外,分片上传中途停止后,之前上传的分片去哪了,会不会在云端残留,会不会导致资源外泄等情况,这部分也还是没有检查的。