当前位置: 首页 > 工具软件 > OSSC > 使用案例 >

windows 接入阿里云oss C SDK, 实现异步下载 的经验和总结

殳越
2023-12-01

前段时间项目有个需求:游戏中接入自定义头像。

我们项目采用的方案,是把用户自定义的头像存储到阿里云的 oss 上。需要的时候,把头像图片下载到本地,放到游戏引擎能取到的路径,通知游戏下载成功,游戏逻辑端再像操作普通的资源一样,创建用户自定义头像的图片作为精灵使用到游戏里。

iOS端和android端已经由其他同事接入了。但是平时开发时不便于测试,因此需要再在电脑上,对于我来说就是win32平台,也接入 oss 上传和下载的流程。

这周任务不是很多,于是前两天折腾了一下。

OSS 的 官方文档在此:

https://help.aliyun.com/document_detail/32137.html?spm=a2c4g.11186623.6.803.q6Fjje

打开链接,发现有各种语言的SDK,好像很方便的样子。 iOS,android 的都有现成的。win32用哪个呢?往下一翻,看到了 C语言版的,就是它了。

官方的文档很全面,c语言 sdk 下载包里面甚至还有全套的源代码,依赖的第三方库 在 third_party 路径的 include 和 prebuilt  里摆得整整齐齐,接入到工程里还算轻松。整合到项目里,经过一翻调试,上传下载均OK 。

本以为完事大吉。然而此时突然发现:C 语言的 oss 库代码 ,并不支持异步下载。也就是说下载个图片会卡游戏主线程。如果进个排行榜页面,一口气下载50来个自定义图片,卡主线程很久肯定是无法接受的。于是,看了一下 iOS 和 android 的 oss sdk 是怎么解决的。

大概看了一下,发现这两个平台的 SDK 都把异步封装好了,下载时,设置好回调函数就行了。反观 C 语言版的 库,并无如此高级的功能。不过仔细想了一下这也正常:毕竟 C语言标准库 里 好像就没有跟线程相关的内容,只能自己起一个新的线程做异步处理了。

思路大概就是要实现一个简单的“生产者消费者”模型:下载线程负责“生产”用户自定义头像;游戏主线程负责“消费”,取走下载好的用户自定义头像,给游戏逻辑发通知。

有了思路后,开始撸代码。过程中了解和巩固了一些c++相关的知识点,包括c++基础数据结构 std::map std::deque的用法;

c++ 11 的 std::thread 线程库;

std::mutex 做线程间公用数据的 互斥锁

关于 std::mutex  互斥锁,需要再整理一下原理和思路:

线程之间如果想要通信,本质就是交换数据,A线程生产 data,B线程消费data。有生产有消费,同时都在操作 data 这一个数据,就有可能在同时操作  data 时乱套,因此 ,就需要给 data 分配一把 mutex 互斥锁,在不同线程操作 data 时 (包括读和写)的前后, 加好 mutex.lock,mutex.release,保证线程不会出问题。加的时候还要小心一些,防止 互相卡死 造成的线程死锁。

道理很简单,但是如果不亲手多进行实际操作,就很难理解透彻。

思路弄清晰了以后,代码写起来就很快了。

由于自己不是特别明白信号量的用法,因此,消费者(游戏主线程)现在的写法,是在游戏主循环里 采用每帧 轮询的方式,找 生产者(下载线程)上门来索要 下载好的结果。看起来有点 low ,有待改进。

不过不管怎么说,还是实现了需求。

中间还有一些细节,比如  std::map 如何安全地一边遍历,一边删除等等。但是最关键的代码,还是线程的使用,和线程的同步,线程之间的通信。

这部分代码跟其他代码有一些耦合,稍微改一改是能够做到独立的。把关键代码贴在这里,做个备忘,就暂时不追求细节了

OSSManager.h

#ifndef __OSS_MANAGER_H__
#define __OSS_MANAGER_H__

#include <string>
#include "aos_http_io.h"
#include "oss_api.h"
#include "oss_auth.h"
#include "oss_define.h"
#include "oss_util.h"
#include "aos_list.h"
#include "aos_status.h"

#include <map>
#include <mutex>
#include <deque>

namespace oss{
	class OSSDownloadRequest
	{
	public:
		OSSDownloadRequest(std::string objectPath, std::string saveAsPath);

		void markStart(bool bStart){ this->bStarted = bStart; }
		void markSucc(bool bSucc){ this->bSucc = bSucc; }
		void markDone(bool bDone){ this->bDone = bDone; }

		bool isStarted() const { return bStarted; }
		bool isSucc() const { return bSucc; }
		bool isDone() const { return bDone; }

		std::string getObjectPath() const { return objectPath; }
		std::string getSavePath() const { return saveAsPath; }
	private:
		bool bStarted;
		bool bSucc;
		bool bDone;
		std::string objectPath;
		std::string saveAsPath;
	};

	class OSSManager
	{
	public:
		static OSSManager* getInstance();
		virtual ~OSSManager();

		void launch();

		bool downloadImage(std::string objectPath, std::string savePath);
		bool uploadImage(std::string filePath, std::string saveAsName);

		bool downloadImage(OSSDownloadRequest* pRequest);
	public:
		typedef std::map<std::string, OSSDownloadRequest*>	RequestMapType;
		typedef std::map<std::string, OSSDownloadRequest*>::iterator RequestMapIter;
		typedef std::deque<OSSDownloadRequest*>::iterator ResultDequeIter;

		bool downloadImageAsync(std::string objectPath, std::string savePath);
		void setAuth(std::string accessKeyID,std::string accessSecret,std::string authToken);

		// requestMap & requestMap's mutex
		RequestMapType* getRequestMap(){ return &_requestMap; }
		std::mutex& getRequestMapMutex(){return _requestMutex;}


		// result & its mutex
		std::mutex& getResultMutex() { return _resultMutex; }
		void insertOneResult(OSSDownloadRequest* pRequest);
	//private:
		//OSSDownloadRequest* getOneRequestResult();
		

	public:
		void updateFrame();

	private:
		OSSManager();
		void init_options_w(oss_request_options_t *options);
		void init_options_r(oss_request_options_t *options);

	private:
		static const std::string END_POINT_W;
		static const std::string END_POINT_R;
		static const std::string BUCKET;

		static const std::string ACCESS_KEY_ID_W;
		static const std::string ACCESS_KEY_SECRET_W;

		static const std::string ACCESS_KEY_ID_R;
		static const std::string ACCESS_KEY_SECRET_R;

		// 使用授权方式
		std::string	_authReadAccessKey;
		std::string _authReadAccessSecret;
		std::string _authSecretToken;
		int _authExpiration;

	private:
		std::mutex	_requestMutex;
		std::map<std::string, OSSDownloadRequest*>		_requestMap;

		std::mutex _resultMutex;
		std::deque<OSSDownloadRequest*>					_resultDeque;
	};
}

#endif // __OSS_MANAGER_H__

OSSManager.cpp

#include "OSSManager.h"
#include <stdio.h>
#include <thread>
#include "../SDKUtil/SDKUtil.h"

/*
	正常流程:
	END_POINT_R + 请求下来的 authAccessID,authAccessSecret,authAccessToken 可以下载

	测试流程:
	END_POINT_W + ACCESS_KEY_ID_W ,ACCESS_KEY_SECRET_W 可上传
	END_POINT_W + ACCESS_KEY_ID_R + ACCESS_KEY_SECRET_R 可下载
*/
using namespace oss;
// end-point,bucket
const std::string OSSManager::END_POINT_W = "https:xxxxxxxxxxxxxxxxxxxxxxxxxxx";		// upload
const std::string OSSManager::END_POINT_R = "https:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";		// download

//const std::string OSSManager::BUCKET = "xxxxxxxxxxxxxxx";
const std::string OSSManager::BUCKET = "????????????????????";
//	AccessKey write
const std::string OSSManager::ACCESS_KEY_ID_W = "xxxxxxxxxxxxxxxxxxxxxxx";				
const std::string OSSManager::ACCESS_KEY_SECRET_W = "xxxxxxxxxxxxxxxxxxxxxxx";
//	AccessKey read
const std::string OSSManager::ACCESS_KEY_ID_R = "xxxxxxxxxxxxxxxxxxxxxxx";
const std::string OSSManager::ACCESS_KEY_SECRET_R = "xxxxxxxxxxxxxxxxxxxxxxx";

void OSSDownloadMainLoop();
void notifyRequestDone(OSSManager* pOSS,OSSDownloadRequest* pRequest);
bool bOSSManagerDestroyed = false;

OSSDownloadRequest::OSSDownloadRequest(std::string objectPath, std::string saveAsPath)
	:bStarted(false)
	, bSucc(false)
	, bDone(false)
{
	this->objectPath = objectPath;
	this->saveAsPath = saveAsPath;
}

OSSManager::OSSManager()
	: _authReadAccessKey("")
	, _authReadAccessSecret("")
	, _authSecretToken("")
	, _authExpiration(0)
{
	if (aos_http_io_initialize(NULL, 0) != AOSE_OK)
	{
		return;
	}
}

OSSManager::~OSSManager()
{	
	aos_http_io_deinitialize();
	bOSSManagerDestroyed = true;
}

OSSManager* OSSManager::getInstance()
{
	static OSSManager instance;
	return &instance;
}

void OSSManager::launch()
{
	//apr_thread_create
	std::thread downloadThread(OSSDownloadMainLoop);
	downloadThread.detach();
}

void OSSManager::init_options_w(oss_request_options_t *options)
{
	options->config = oss_config_create(options->pool);
	aos_str_set(&options->config->endpoint, OSSManager::END_POINT_W.c_str());
	aos_str_set(&options->config->access_key_id, OSSManager::ACCESS_KEY_ID_W.c_str());
	aos_str_set(&options->config->access_key_secret, OSSManager::ACCESS_KEY_SECRET_W.c_str());
	options->config->is_cname = 0;
	options->ctl = aos_http_controller_create(options->pool, 0);
}

void OSSManager::init_options_r(oss_request_options_t *options)
{
	options->config = oss_config_create(options->pool);
	aos_str_set(&options->config->endpoint, OSSManager::END_POINT_R.c_str());
	if (strcmp(_authReadAccessKey.c_str(), "") != 0 
		&& strcmp(_authReadAccessSecret.c_str(), "") != 0
		&& strcmp(_authSecretToken.c_str(), "") != 0)
	{
		aos_str_set(&options->config->access_key_id, _authReadAccessKey.c_str());
		aos_str_set(&options->config->access_key_secret, _authReadAccessSecret.c_str());
		aos_str_set(&options->config->sts_token,_authSecretToken.c_str());
	}
	else
	{
		aos_str_set(&options->config->access_key_id, OSSManager::ACCESS_KEY_ID_R.c_str());
		aos_str_set(&options->config->access_key_secret, OSSManager::ACCESS_KEY_SECRET_R.c_str());
	}

	

	options->config->is_cname = 0;
	options->ctl = aos_http_controller_create(options->pool, 0);
}

void OSSManager::setAuth(std::string accessKeyID, std::string accessSecret,std::string authToken)
{
	_authReadAccessKey = accessKeyID;
	_authReadAccessSecret = accessSecret;
	_authSecretToken = authToken;
}

/*
Example:
pOSS->downloadImage("xxxxxx/xxxxxxxxx","G:\\custom.png");
*/
bool OSSManager::downloadImage(std::string objectPath, std::string savePath)
{
	bool bResult = false;

	aos_pool_t *p;
	oss_request_options_t *options;
	aos_status_t *s;
	aos_table_t *headers;
	aos_table_t *params;
	aos_table_t *resp_headers;

	const char *bucket_name = BUCKET.c_str();
	const char *object_name = objectPath.c_str();
	const char *filepath = savePath.c_str();
	aos_string_t bucket;
	aos_string_t object;
	aos_string_t file;
	aos_pool_create(&p, NULL);
	do
	{
		// create request options
		options = oss_request_options_create(p);	
		init_options_r(options);
		// init request params
		aos_str_set(&bucket, bucket_name);
		aos_str_set(&object, object_name);
		aos_str_set(&file, filepath);
		headers = aos_table_make(p, 0);

		params = aos_table_make(p, 0);
		// do download
		s = oss_get_object_to_file(options, &bucket, &object, headers, params, &file, &resp_headers);
		if (aos_status_is_ok(s)) {
			printf("get object succeeded\n");
			bResult = true;
			break;
		}
		else {
			printf("get object failed\n");
			bResult = false;
			break;
		}
	} while (0);
	// release request res
	aos_pool_destroy(p);
	return bResult;
}

bool OSSManager::downloadImage(OSSDownloadRequest* pRequest)
{
	assert(!pRequest->isStarted(),"Can not send a has started request.");
	if (pRequest->isStarted())
	{
		return false;
	}
	pRequest->markStart(true);
	bool bRet = downloadImage(pRequest->getObjectPath(),pRequest->getSavePath());
	pRequest->markDone(true);
	pRequest->markSucc(bRet);
	return bRet;
}

/*
Example:
pOSS->uploadImage("g:\\custom.png", "custom.png");
*/
bool OSSManager::uploadImage(std::string filePath,std::string saveAsName)
{
	bool bResult = false;
	aos_pool_t *p = NULL;
	aos_string_t bucket;
	aos_string_t object;
	aos_table_t *headers = NULL;
	aos_table_t *resp_headers = NULL;
	oss_request_options_t *options = NULL;

	//char *filename = __FILE__;
	const char *filename = filePath.c_str();
	aos_status_t *s = NULL;
	aos_string_t file;
	aos_pool_create(&p, NULL);
	
	do
	{
		// create and init option
		options = oss_request_options_create(p);
		init_options_w(options);
		// init request params
		headers = aos_table_make(options->pool, 1);

		std::string destFolder = "win32img/";
		std::string destPath = destFolder + saveAsName;

		//apr_table_set(headers, OSS_CONTENT_TYPE, "image/jpeg");
		aos_str_set(&bucket, BUCKET.c_str());
		aos_str_set(&object, destPath.c_str());
		aos_str_set(&file, filename);
		// upload file
		s = oss_put_object_from_file(options, &bucket, &object, &file,
			headers, &resp_headers);
		// wether request success
		if (aos_status_is_ok(s)) {
			printf("put object from file succeeded\n");
			bResult = true;
			break;
		}
		else {
			printf("put object from file failed\n");
			bResult = false;
		}
	} while (0);
	
	// release request res
	aos_pool_destroy(p);
	return bResult;
}

/*
	Download file async
*/
bool OSSManager::downloadImageAsync(std::string objectPath, std::string savePath)
{
	if (_requestMap.count(objectPath) == 1)
		return false;

	/*
		@todo
		这里设计上有缺陷
		这里  new ,但是却需要 依赖 游戏主循环 call this->updateFrame() 里面 delete
		
		有待改善
	*/
	OSSDownloadRequest* pRequest = new OSSDownloadRequest(objectPath,savePath);
	getRequestMapMutex().lock();
	_requestMap[objectPath] = pRequest;
	getRequestMapMutex().unlock();
	return true;
}

void OSSManager::insertOneResult(OSSDownloadRequest* pRequest)
{
	_resultDeque.push_back(pRequest);
}

/*
	Called by game mainloop
*/
void OSSManager::updateFrame()
{
	getResultMutex().lock();
	for (ResultDequeIter iter = _resultDeque.begin(); iter != _resultDeque.end(); iter++)
	{
		OSSDownloadRequest* pDoneRequest = (*iter);
		printf("Notify game main thread download icon success or failed\n");
		std::string cbParam = std::string("{\"filepath\":\"") + std::string(pDoneRequest->getObjectPath()) + std::string("\"}");
		FakeSDK::SDK_STATUS_CODE cbTypeCode = pDoneRequest->isSucc() ? cbTypeCode = FakeSDK::SDK_STATUS_CODE::SDK_ALI_DOWNLOND_SUCCESS : cbTypeCode = FakeSDK::SDK_STATUS_CODE::SDK_ALI_DOWNLOND_FAIL;
		FakeSDK::SDKUtil::getInstance()->sdkCallback(cbTypeCode,cbParam.c_str());
		delete pDoneRequest;
	}
	_resultDeque.clear();
	getResultMutex().unlock();
}


/* 
OSS Download thread
*/
void OSSDownloadMainLoop()
{
	OSSManager* pOSS = OSSManager::getInstance();
	while (!bOSSManagerDestroyed)
	{
		pOSS->getRequestMapMutex().lock();
		//printf("OSSDownloadMainLoop working...\n");

		OSSManager::RequestMapIter iter;
		OSSManager::RequestMapType* allRequestMap = pOSS->getRequestMap();
		for (iter = allRequestMap->begin(); iter != allRequestMap->end();/*DO NOT iter++ HERE*/)
		{
			OSSDownloadRequest* pRequest = iter->second;
			if (pRequest->isDone())
			{
				iter = allRequestMap->erase(iter);
				notifyRequestDone(pOSS,pRequest);
			}
			else
			{
				if (!pRequest->isStarted())
				{
					pOSS->downloadImage(pRequest);
					continue;
				}
				iter++;
			}
		}

		pOSS->getRequestMapMutex().unlock();

		Sleep(1);
	}
}

// Notify download done to game
void notifyRequestDone(OSSManager* pOSS, OSSDownloadRequest* pRequest)
{
	printf("Download image %s done!\n",pRequest->getObjectPath().c_str());
	pOSS->getResultMutex().lock();
	pOSS->insertOneResult(pRequest);
	pOSS->getResultMutex().unlock();
}

由于组里很多同事都是MAC,平时跑游戏也只跑 mac 的 工程(不是 iOS工程),因此本来打算也给 mac 接一遍。毕竟 oss C SDK 的源代码也有,而且还是 c 语言写的,理论上能很方便地移植。但是。。由于 官方给的 oss 库下载后,third_party 的 prebuilt 库文件里,只有 win32 相关的  lib 和  dll,并没有  mac 的 .a .framework .dylib 的,因此 oss 代码编译时所需的 apr,aprutil mxml 等等,还需要自己 编译出来库文件才能使用。这个过程可能会比较麻烦,因此暂时放弃了给  mac 也接入的想法。

明天试试 oss 官方的 iOS SDK 是不是一个 纯  objc 不依赖 iOS 平台的。如果是的话,可能有机会用 ios 的 sdk 在 mac 上接入成功。

 类似资料: