阿里云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();
    }
}

四、疑问
进度条的回调还是有些搞不明白,我对回调函数其实理解不深,不太明白为什么不能使用类成员函数来进行回调,导致我额外创建了个中间类来发送信号,感觉特别别扭,不知道还有没有其他的实现方式。
另外,分片上传中途停止后,之前上传的分片去哪了,会不会在云端残留,会不会导致资源外泄等情况,这部分也还是没有检查的。