米大师C++跨平台SDK开发指北

作者: 空同定翁 | 来源:发表于2018-12-19 17:17 被阅读12次

    上半年一直忙于主机平台支付功能的接入,原来基于android开发的sdk并不适用主机平台。为了满足业务需求,先是重新开发了一套unity的sdk,用于unity引擎游戏业务集成。然而该unity sdk并不能满足ue4引擎游戏业务的需求,考虑到后续sdk的兼容和通用,选择重新开发一套c++ sdk,实现跨主机平台接入。

    虽然很早之前也接触过c++开发,但工作以来主要还是做android平台开发,c++很多东西早就忘的一干二净了。相对于早前自己开发的unity sdk,c#语言更类似java,同样采用虚拟机实现,平台工具都比较齐全。而c++自身的语言特性和平台实现差异,决定了重新开发一套c++ sdk的难度。

    外部接口


    c++ sdk的目标是能支持跨平台调用,实现多平台支付接入。如:既能支持switch、ps4、xbox等不同平台支付功能的接入,又能支持unity跨平台调用该sdk。因此,对外暴露的sdk接口入参回调等最好是统一格式,最小化的个性定制。

    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);
            ......      
    
            //函数指针回调
            typedef void(*MCallback)(const char*);
    
    • req:为Midas自身请求参数类,各接口req存在继承组合关系。
      • 外部传入引用,而不是指针
        • sdk内部会统一将引用转成指针,外部不需要关注指针的释放回收。
      • Pay接口通过传入req的不同实现来实现多态
        • 由于c++不能像java支持父类到子类的动态转换,通过三个接口的方式,实现不同业务逻辑的封装。
    • callback:函数指针回调,各接口统一回调json字符串
      • 字符串方便unity跨平台调用

    Unity跨平台调用接口

    为了实现unity跨平台调用该c++ sdk,需要再封装一层协议接口层,实现unity到c++的转换。并通过dllexport,将接口暴露到unity层。

    以初始化接口Initialize为例:

    #define PRX_EXPORT extern "C" __declspec (dllexport)
    
    /*
     * jInitRequest:unity层传入string
     * callback: unity层传入函数指针回调
     */
    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++层转换成对应的类MidasInitRequest。
    • callback:unity层传入对应的函数指针回调

    unity层调用

    unity层调用c++ dll库中接口的方式如下:

        //1、引入c++ dll库中接口
        [DllImport("PS4Channel")]
        public static extern void Initialize(string jInitRequest,DllcallBack callback);
    
        //2、定义委托,用于接收c++层回调
        public delegate void DllcallBack(string response);
        //委托实现,处理c++初始化回调结果
        [MonoPInvokeCallback(typeof(DllcallBack))]
        public static void CSharpCallbackInit(string response){}
        
        //3、调用
        Initialize(jInitReq.ToString(), CSharpCallbackInit);
    

    逻辑层


    这部分内容主要分析c++ sdk的逻辑设计思路。c++ sdk对外提供初始化、支付、补发、查物品信息和查营销信息等功能,对内需要维护复用一套框架,实现不同平台不同支付渠道的接入。

    订单中心

    由于对外提供的接口存在相似性(入参request、回调callback),抽象出订单中心。

    • 每次外部接口调用,生成一笔内部订单(key、参数、回调)
    • 根据key查找获取外部参数、回调外部

    每次各外部接口调用开始到结束,复用同一套订单逻辑。以支付接口调用为例:

    1 每次支付生成订单
        //支付
        void MPayManager::pay(MidasBasePayRequest* req, MCallback _callback)
        {
            //1、生成支付渠道
            ChannelBasePay* payChannel = ChannelFactory::createPayChannel(req->_base.payChannel);
            if (payChannel)
            {
                //2、创建订单
                int orderKey = generateOrderKey();
                MidasOrder order;
                order.request = req;        
                order.callback = _callback;
                addOrder(orderKey, order);
    
                //3、支付,传入订单key
                payChannel->setOrderKey(orderKey);
                ...
    
    2 根据订单key回调外部
    //根据订单key回调外部
    void MPayManager::callback(int key, MidasResponse& response, ChannelBase* instance)
        {
            //1、根据key查找订单
            MidasOrder order = getOrder(key);
            if (order.callback)
            {
                ...
                //2、回调外部
                order.callback(response.toJStr().c_str());
                //3、移除订单
                removeOrder(key);
            ...
    

    渠道

    sdk是以渠道为粒度,提供了一套框架,负责实现各渠道的流程调度。而每个渠道通常存在差异,对外提供的功能不一,从而将渠道的每项功能抽象为具体的渠道实例,如:支付渠道、补发渠道、查物品信息渠道和查营销活动渠道。而sdk再通过基类,抽象出各功能渠道的流程框架,具体实现交由不同子类渠道完成。

    仍以支付为例:

        //支付基类
        class ChannelBasePay : public ChannelBase
        {
            ...
            //1、子类通过该方法实现流程流转
            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实现实例的动态创建。c++ sdk通过渠道工厂类,负责渠道实例的创建回收。

        class ChannelFactory
        {
        public:
            static ChannelBasePay* createPayChannel(string channel);
            static ChannelBaseReprovide* createReprovideChannel(string channel);
            static list<ChannelBase*> gc_list;  //用于存放分配的对象,方便释放
    

    网络

    网络层设计的目标是既能满足sdk自身业务需要,又能跨平台调用。于是将网络抽象成3层:

    • 业务层:满足sdk自身请求加密等需要。
    • 功能层:网络基础逻辑功能,封装https、post等网络请求逻辑。
    • 跨平台层:不同平台具体网络数据收发实现。

    这里讲下跨平台层实现逻辑。

    由于sdk目标是能在多平台上运行,而不同平台如windows、linux和unix中网络库不同;要实现多平台统一,可以采用的方式有:

    • 方式一:自己基于socket,用c手撸一套网络代码
      • 难度大、易出错,手撸https基本绝望
    • 方式二:采用第三方库,如libcurl,再编译成各平台响应的库
      • 前期在x64上通过该方式实现的网络请求,但在移植到主机平台时,各种天坑。
      • 由于主机平台的小众性,libcurl的主机平台编译几乎没见人做过,自己编译时各种问题,修改源码才行。
      • 考虑到sdk还用了openssl等库,采用这种方式适配,工作量巨大。
    • 方式三:sdk封装网络框架,基础的网络收发交由各平台实现
      • 该方式虽需要做不同平台的适配,但由于各平台文档健全,工作量反而最小,也较容易实现。

      • 选择该方式实现

        class NetReqBase
        {
        public:
            ...
            //发起请求
            void start()
            {
                //1、生成平台相关网络类
                PlatformNet* netPlatform = PlatformFactory::createNetPlatform();
                if (netPlatform)
                {
                    //2、具体平台收发网络数据
                    string response = netPlatform->https_post(getUrl(), getParams());
                    if (_observer)
                        _observer->parse(getExtra(), response);
                    delete netPlatform;
                ...
        

    跨平台


    c++ sdk的目标是跨平台,但如何实现跨平台?

    综合sdk实现过程中,需要跨平台实现的主要是sdk的基础功能,如:网络收发、AES加/解密、gzip压缩。正如在上面提到的一样,开始的方案是通过引入第三方平台无关的库来实现各基础功能,再通过编译成不同平台的库来实现跨平台。而在x64上,自己也通过这种方式实现了整套流程,在x64上采用的库为:

    • 网络收发:libcurl
    • AES加/解密:openssl
    • gzip压缩:zlib

    而在移植到ps4平台时,花了大量时间做libcurl的编译,而openssl编译适配更是毫无头绪。于是放弃了这种实现,改而通过将基础功能下沉到具体平台实现,sdk抽象出基类实现跨平台框架。最终在ps4的移植后,采用的方式:

    • 网络收发、AES加解密
      • 下沉到调用ps4平台api实现
    • gzip压缩
      • ps4没有gzip压缩库,通过zlib编译ps4平台包实现
    通过条件编译生成不同平台类
        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();
        }
    
    基础功能具体平台实现
    #ifdef PS4
    ...
    namespace Midas
    {
        class PS4PfNet : public PlatformNet
        {
        public:
            //ps4实现https+post
            string https_post(string url, string params);
        ...
    
    #endif //PS4
    
    #ifdef PS4
    namespace Midas
    {
        //ps4实现AES加/解密
        class PS4PfTool : public PlatformTool
        {
        public:
            string aes_encrypt(string input, string key);
            string aes_decrypt(string input, string key);
        ...
    
    #endif // PS4
    

    总结


    c++ sdk断断续续开发了一个多月,重新捡起c++到完成ps4的移植测试,还是有点成就感的。之前虽然也重新写了一套unity sdk,但相对c++ sdk来说,难度还是低了很多。虽然也是重复造轮子,但在整个项目的过程中,碰到的问题远比预期的多,解决问题的快乐才是继续的源动力。

    相关文章

      网友评论

        本文标题:米大师C++跨平台SDK开发指北

        本文链接:https://www.haomeiwen.com/subject/bhojkqtx.html