上半年一直忙于主機(jī)平臺支付功能的接入骗污,原來基于android開發(fā)的sdk并不適用主機(jī)平臺。為了滿足業(yè)務(wù)需求躏精,先是重新開發(fā)了一套u(yù)nity的sdk李丰,用于unity引擎游戲業(yè)務(wù)集成。然而該unity sdk并不能滿足ue4引擎游戲業(yè)務(wù)的需求叠纹,考慮到后續(xù)sdk的兼容和通用季研,選擇重新開發(fā)一套c++ sdk,實(shí)現(xiàn)跨主機(jī)平臺接入誉察。
雖然很早之前也接觸過c++開發(fā)与涡,但工作以來主要還是做android平臺開發(fā),c++很多東西早就忘的一干二凈了持偏。相對于早前自己開發(fā)的unity sdk驼卖,c#語言更類似java,同樣采用虛擬機(jī)實(shí)現(xiàn)鸿秆,平臺工具都比較齊全酌畜。而c++自身的語言特性和平臺實(shí)現(xiàn)差異,決定了重新開發(fā)一套c++ sdk的難度卿叽。
外部接口
c++ sdk的目標(biāo)是能支持跨平臺調(diào)用桥胞,實(shí)現(xiàn)多平臺支付接入。如:既能支持switch考婴、ps4贩虾、xbox等不同平臺支付功能的接入,又能支持unity跨平臺調(diào)用該sdk沥阱。因此缎罢,對外暴露的sdk接口入?yún)⒒卣{(diào)等最好是統(tǒng)一格式,最小化的個性定制。
C++接口
c++對外接口如下所示:
//Midas初始化
void Initialize(const MidasInitRequest& req, MCallback callbck);
//Midas支付
void Pay(const MidasGameRequest& req, MCallback callback);
void Pay(const MidasGoodsRequest& req, MCallback callback);
void Pay(const MidasSubscribeRequest& req, MCallback callback);
......
//函數(shù)指針回調(diào)
typedef void(*MCallback)(const char*);
-
req:為Midas自身請求參數(shù)類策精,各接口req存在繼承組合關(guān)系舰始。
- 外部傳入引用,而不是指針
- sdk內(nèi)部會統(tǒng)一將引用轉(zhuǎn)成指針咽袜,外部不需要關(guān)注指針的釋放回收丸卷。
- Pay接口通過傳入req的不同實(shí)現(xiàn)來實(shí)現(xiàn)多態(tài)
- 由于c++不能像java支持父類到子類的動態(tài)轉(zhuǎn)換,通過三個接口的方式询刹,實(shí)現(xiàn)不同業(yè)務(wù)邏輯的封裝及老。
- 外部傳入引用,而不是指針
-
callback:函數(shù)指針回調(diào),各接口統(tǒng)一回調(diào)json字符串
- 字符串方便unity跨平臺調(diào)用
Unity跨平臺調(diào)用接口
為了實(shí)現(xiàn)unity跨平臺調(diào)用該c++ sdk范抓,需要再封裝一層協(xié)議接口層骄恶,實(shí)現(xiàn)unity到c++的轉(zhuǎn)換。并通過dllexport匕垫,將接口暴露到unity層僧鲁。
以初始化接口Initialize為例:
#define PRX_EXPORT extern "C" __declspec (dllexport)
/*
* jInitRequest:unity層傳入string
* callback: unity層傳入函數(shù)指針回調(diào)
*/
PRX_EXPORT void Initialize(const char* jInitRequest, MCallback callback)
{
Document doc;
if (!doc.Parse(jInitRequest).HasParseError())
{
MidasInitRequest _initReq;
if (doc.HasMember("idc"))
_initReq.idc = doc["idc"].GetString();
if (doc.HasMember("env"))
_initReq.env = doc["env"].GetString();
...
//初始化
MidasPayService::instance()->Initialize(_initReq, callback);
}
}
- jInitRequest:unity層傳入string,c++層轉(zhuǎn)換成對應(yīng)的類MidasInitRequest象泵。
- callback:unity層傳入對應(yīng)的函數(shù)指針回調(diào)
unity層調(diào)用
unity層調(diào)用c++ dll庫中接口的方式如下:
//1寞秃、引入c++ dll庫中接口
[DllImport("PS4Channel")]
public static extern void Initialize(string jInitRequest,DllcallBack callback);
//2、定義委托偶惠,用于接收c++層回調(diào)
public delegate void DllcallBack(string response);
//委托實(shí)現(xiàn)春寿,處理c++初始化回調(diào)結(jié)果
[MonoPInvokeCallback(typeof(DllcallBack))]
public static void CSharpCallbackInit(string response){}
//3、調(diào)用
Initialize(jInitReq.ToString(), CSharpCallbackInit);
邏輯層
這部分內(nèi)容主要分析c++ sdk的邏輯設(shè)計思路忽孽。c++ sdk對外提供初始化绑改、支付、補(bǔ)發(fā)兄一、查物品信息和查營銷信息等功能厘线,對內(nèi)需要維護(hù)復(fù)用一套框架,實(shí)現(xiàn)不同平臺不同支付渠道的接入出革。
訂單中心
由于對外提供的接口存在相似性(入?yún)equest造壮、回調(diào)callback),抽象出訂單中心骂束。
- 每次外部接口調(diào)用耳璧,生成一筆內(nèi)部訂單(key、參數(shù)展箱、回調(diào))
- 根據(jù)key查找獲取外部參數(shù)旨枯、回調(diào)外部
每次各外部接口調(diào)用開始到結(jié)束,復(fù)用同一套訂單邏輯析藕。以支付接口調(diào)用為例:
1 每次支付生成訂單
//支付
void MPayManager::pay(MidasBasePayRequest* req, MCallback _callback)
{
//1召廷、生成支付渠道
ChannelBasePay* payChannel = ChannelFactory::createPayChannel(req->_base.payChannel);
if (payChannel)
{
//2、創(chuàng)建訂單
int orderKey = generateOrderKey();
MidasOrder order;
order.request = req;
order.callback = _callback;
addOrder(orderKey, order);
//3账胧、支付竞慢,傳入訂單key
payChannel->setOrderKey(orderKey);
...
2 根據(jù)訂單key回調(diào)外部
//根據(jù)訂單key回調(diào)外部
void MPayManager::callback(int key, MidasResponse& response, ChannelBase* instance)
{
//1、根據(jù)key查找訂單
MidasOrder order = getOrder(key);
if (order.callback)
{
...
//2治泥、回調(diào)外部
order.callback(response.toJStr().c_str());
//3筹煮、移除訂單
removeOrder(key);
...
渠道
sdk是以渠道為粒度,提供了一套框架居夹,負(fù)責(zé)實(shí)現(xiàn)各渠道的流程調(diào)度败潦。而每個渠道通常存在差異,對外提供的功能不一准脂,從而將渠道的每項(xiàng)功能抽象為具體的渠道實(shí)例劫扒,如:支付渠道、補(bǔ)發(fā)渠道狸膏、查物品信息渠道和查營銷活動渠道沟饥。而sdk再通過基類,抽象出各功能渠道的流程框架湾戳,具體實(shí)現(xiàn)交由不同子類渠道完成贤旷。
仍以支付為例:
//支付基類
class ChannelBasePay : public ChannelBase
{
...
//1、子類通過該方法實(shí)現(xiàn)流程流轉(zhuǎn)
void notifyAsyncFinish(int type);//異步操作完成
//2砾脑、基類模板方法
virtual void init();
virtual void prePay();
virtual void pay();
virtual void postPay() {}
virtual bool needOrder(){return false;}
virtual bool needProvide(){return false;}
virtual void dispose() {}
...
由于c++不支持反射幼驶,不能像java實(shí)現(xiàn)實(shí)例的動態(tài)創(chuàng)建。c++ sdk通過渠道工廠類韧衣,負(fù)責(zé)渠道實(shí)例的創(chuàng)建回收盅藻。
class ChannelFactory
{
public:
static ChannelBasePay* createPayChannel(string channel);
static ChannelBaseReprovide* createReprovideChannel(string channel);
static list<ChannelBase*> gc_list; //用于存放分配的對象,方便釋放
網(wǎng)絡(luò)
網(wǎng)絡(luò)層設(shè)計的目標(biāo)是既能滿足sdk自身業(yè)務(wù)需要畅铭,又能跨平臺調(diào)用萧求。于是將網(wǎng)絡(luò)抽象成3層:
- 業(yè)務(wù)層:滿足sdk自身請求加密等需要。
- 功能層:網(wǎng)絡(luò)基礎(chǔ)邏輯功能顶瞒,封裝https夸政、post等網(wǎng)絡(luò)請求邏輯这难。
- 跨平臺層:不同平臺具體網(wǎng)絡(luò)數(shù)據(jù)收發(fā)實(shí)現(xiàn)粱胜。
這里講下跨平臺層實(shí)現(xiàn)邏輯财剖。
由于sdk目標(biāo)是能在多平臺上運(yùn)行立由,而不同平臺如windows舌镶、linux和unix中網(wǎng)絡(luò)庫不同哑梳;要實(shí)現(xiàn)多平臺統(tǒng)一另玖,可以采用的方式有:
-
方式一:自己基于socket忠售,用c手?jǐn)]一套網(wǎng)絡(luò)代碼
- 難度大袱贮、易出錯仿便,手?jǐn)]https基本絕望
-
方式二:采用第三方庫,如libcurl,再編譯成各平臺響應(yīng)的庫
- 前期在x64上通過該方式實(shí)現(xiàn)的網(wǎng)絡(luò)請求嗽仪,但在移植到主機(jī)平臺時荒勇,各種天坑。
- 由于主機(jī)平臺的小眾性闻坚,libcurl的主機(jī)平臺編譯幾乎沒見人做過沽翔,自己編譯時各種問題,修改源碼才行窿凤。
- 考慮到sdk還用了openssl等庫仅偎,采用這種方式適配,工作量巨大雳殊。
-
方式三:sdk封裝網(wǎng)絡(luò)框架橘沥,基礎(chǔ)的網(wǎng)絡(luò)收發(fā)交由各平臺實(shí)現(xiàn)
該方式雖需要做不同平臺的適配,但由于各平臺文檔健全夯秃,工作量反而最小威恼,也較容易實(shí)現(xiàn)。
-
選擇該方式實(shí)現(xiàn)
class NetReqBase { public: ... //發(fā)起請求 void start() { //1寝并、生成平臺相關(guān)網(wǎng)絡(luò)類 PlatformNet* netPlatform = PlatformFactory::createNetPlatform(); if (netPlatform) { //2箫措、具體平臺收發(fā)網(wǎng)絡(luò)數(shù)據(jù) string response = netPlatform->https_post(getUrl(), getParams()); if (_observer) _observer->parse(getExtra(), response); delete netPlatform; ...
跨平臺
c++ sdk的目標(biāo)是跨平臺,但如何實(shí)現(xiàn)跨平臺衬潦?
綜合sdk實(shí)現(xiàn)過程中斤蔓,需要跨平臺實(shí)現(xiàn)的主要是sdk的基礎(chǔ)功能,如:網(wǎng)絡(luò)收發(fā)镀岛、AES加/解密弦牡、gzip壓縮。正如在上面提到的一樣漂羊,開始的方案是通過引入第三方平臺無關(guān)的庫來實(shí)現(xiàn)各基礎(chǔ)功能驾锰,再通過編譯成不同平臺的庫來實(shí)現(xiàn)跨平臺。而在x64上走越,自己也通過這種方式實(shí)現(xiàn)了整套流程椭豫,在x64上采用的庫為:
- 網(wǎng)絡(luò)收發(fā):libcurl
- AES加/解密:openssl
- gzip壓縮:zlib
而在移植到ps4平臺時,花了大量時間做libcurl的編譯旨指,而openssl編譯適配更是毫無頭緒赏酥。于是放棄了這種實(shí)現(xiàn),改而通過將基礎(chǔ)功能下沉到具體平臺實(shí)現(xiàn)谆构,sdk抽象出基類實(shí)現(xiàn)跨平臺框架裸扶。最終在ps4的移植后,采用的方式:
- 網(wǎng)絡(luò)收發(fā)搬素、AES加解密
- 下沉到調(diào)用ps4平臺api實(shí)現(xiàn)
- gzip壓縮
- ps4沒有g(shù)zip壓縮庫呵晨,通過zlib編譯ps4平臺包實(shí)現(xiàn)
通過條件編譯生成不同平臺類
PlatformNet* PlatformFactory::createNetPlatform()
{
#ifdef PS4
return new PS4PfNet();
#endif // ps4
return new PlatformNet();
}
PlatformTool* PlatformFactory::createToolPlatform()
{
#ifdef PS4
return new PS4PfTool();
#endif //ps4
return new PlatformTool();
}
基礎(chǔ)功能具體平臺實(shí)現(xiàn)
#ifdef PS4
...
namespace Midas
{
class PS4PfNet : public PlatformNet
{
public:
//ps4實(shí)現(xiàn)https+post
string https_post(string url, string params);
...
#endif //PS4
#ifdef PS4
namespace Midas
{
//ps4實(shí)現(xiàn)AES加/解密
class PS4PfTool : public PlatformTool
{
public:
string aes_encrypt(string input, string key);
string aes_decrypt(string input, string key);
...
#endif // PS4
總結(jié)
c++ sdk斷斷續(xù)續(xù)開發(fā)了一個多月魏保,重新?lián)炱餭++到完成ps4的移植測試,還是有點(diǎn)成就感的摸屠。之前雖然也重新寫了一套u(yù)nity sdk谓罗,但相對c++ sdk來說,難度還是低了很多餐塘。雖然也是重復(fù)造輪子,但在整個項(xiàng)目的過程中皂吮,碰到的問題遠(yuǎn)比預(yù)期的多戒傻,解決問題的快樂才是繼續(xù)的源動力。