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