前段时间项目有个需求:游戏中接入自定义头像。
我们项目采用的方案,是把用户自定义的头像存储到阿里云的 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 上接入成功。