前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分享一种不太完美的接入网关设计

分享一种不太完美的接入网关设计

作者头像
腾讯大讲堂
发布2019-08-09 10:10:20
1.6K0
发布2019-08-09 10:10:20
举报

作者:朱江,腾讯工程师。

写在前面:

如上图所示,客户端通过HTTP+JSON协议请求ProxyServer,由其派发到后端不同服务的不同接口处理,从而获取结果。那么ProxyServer需要具备怎样的特性呢?

1.修改协议、新增接口及服务时,ProxyServer可以做到不修改代码,不重启,只需要增加新服务的配置即可;

2.ProxyServer支持TAF+JCE调用,后端服务只需要专注业务,提供各自的TAF接口即可,这样有个好处是,不管什么语言(C++,node.js,python)平台开发的服务,只要支持TAF协议就可以接入ProxyServer,而不用做任何修改;

本文探讨的方案,基本满足上面两点,但是有个缺点是:当修改新增协议时,ProxyServer需要重新编译JCE发布服务。

限制

基于C++ 98和taf框架实现。众所周知,C++98并没有像JAVA那样的反射机制,也不想引入第三方反射库(RTTR,cpgf...)。

思路

由于C++ 98没有反射机制,那么如何根据客户端传过来的命令字创建对象呢,如果是硬编码在代码里,那么可以这样写:

代码语言:javascript
复制
switch (cmd)
{
    case HEAD: new head;
    case REQ: new req;
}

很容易我们可以看出一个致命缺点,那就是新增或修改协议,代码也需要作相应的修改,这样费时费力,也容易出错。

如何解决这个问题呢,可以利用C++静态类对象自动创建,从而自动执行构造函数的特性,把相关的类型信息注册到map结构里,这样就可以通过命令字得到对应的类对象,就像类工厂一样。

但是这样还不够,因为当新增加一个协议结构体时,需要在ProxyServer代码里增加对应的类型注册代码,如:REGISTER_CLASS(THComm, JceStructBase, _Notification, AddNotiReq)。解决办法是利用JCE2CPP工具,当转换JCE文件为C++代码时,把相应的注册代码也添加到JCE产生的CPP文件中。

通过命令字字符串得到类对象,就可以把请求消息里的JSON数据序列化为JCE对象结构,从而完成参数的JCE序列化,实现TAF接口+JCE调用。

类对象注册及参数注册

我们先来看一个客户端请求消息的例子,如下:

代码语言:javascript
复制
{
    "args": {
        "req": {
            "hospitalId": "10056"
        },
        "head": {
            "requestId": "ooJQ346G2CMqcSujAt8yE8-Stutc",
            "openId": "ooJQ346G2CMqcSujAt8yE8-Stutc"
        }
    },
    "service": "TencentHealthRegister",
    "func": "getHospitalInfo",
    "context": {
        "requestId": "b9bf3541-3753-11e9-8213-e5de4f5e7b53",
        "traceId": "b9bf3540-3753-11e9-8213-e5de4f5e7b53"
    }
}

对应的JCE接口定义,如下:

代码语言:javascript
复制
module TencentHealthMini {
 struct ThHead{
 0 optional string requestId;
 1 require string openId;
 2 optional string channel;
 3 optional string cityCode;
 };
 struct HospitalReq{
 0 require string hospitalId;
 1 optional string platformId;
 };
 struct HospitalRsp{
 0 require Result result;
 1 require HospitalInfo hospitalInfo;
 };
interface ThRegisterMain {
    int getHospitalInfo(ThHead head,HospitalReq req, out HospitalRsp rsp);
}
}

我们可以看到,请求消息里,客户端会在请求消息里告诉ProxyServer,请求的服务是"service": "TencentHealthRegister",调用的接口是"func": "getHospitalInfo",而接口参数通过”req”和”head”的json串和JCE接口定义的req和head结构对应。这里就有一个问题需要我们解决,如何知道req对应的类型呢?

一种方法是通过配置,在我们的服务配置文件上写明某服务某接口的req对应类型是HospitalReq,如下所示,这样做的缺点是协议改动,配置也需要跟着改动。

代码语言:javascript
复制
<getHospitalInfo>
        <args>
            head = ThHead
            req = HospitalReq
	</args>
   	    rsp = HospitalRsp
</getHospitalInfo>

较好的办法是:可以像类对象注册一样,把参数类型也注册到map,同时TAF接口参数JCE序列化是需要按顺序的,所以参数顺序也是需要我们知道的。

1.类对象注册实现

定义一个静态map<std::string, ObjGen<Base>* >用于存储命令字、对象的产生类。

代码语言:javascript
复制
template<typename Base>
class ObjGen
{
public:
    virtual Base* operator()()
    {
        return NULL;
    }
};

template<typename Base>
std::map<std::string, ObjGen<Base>* >& GetObjMap()
{
    static std::map<std::string, ObjGen<Base>* > obj_map;
    return obj_map;
}

ObjGen是一个模板基类,被具体的子类所继承,从而可以new出对应的类对象。而各个参数的静态类对象自动创建时,会把对应的产生类对象插入到obj_map。

代码语言:javascript
复制
#define REGISTER_CLASS(BASE_NAMESPACE, BASE_NAME, CLASS_NAMESPACE, CLASS_NAME)\
class Gen##CLASS_NAMESPACE##CLASS_NAME: public GenObjectFun<BASE_NAMESPACE::BASE_NAME>\
{\
    public:\
           BASE_NAMESPACE::BASE_NAME* operator()()\
  {\
      return new CLASS_NAMESPACE::CLASS_NAME;\
  }\
};\
\
static struct CLASS_NAMESPACE##CLASS_NAME##AutoInit\
{\
    CLASS_NAMESPACE##CLASS_NAME##AutoInit()\
    {\
        if(GetObjMap<BASE_NAMESPACE::BASE_NAME>().find(#CLASS_NAMESPACE"."#CLASS_NAME) == GetObjMap<BASE_NAMESPACE::BASE_NAME>().end())\
            GetObjMap<BASE_NAMESPACE::BASE_NAME>().insert(std::make_pair(#CLASS_NAMESPACE"."#CLASS_NAME, new Gen##CLASS_NAMESPACE##CLASS_NAME));\
    }\
}__##CLASS_NAMESPACE##CLASS_NAME##AutoInit;

通过GetObject,传进类型名字符串就可以得到对应类对象

代码语言:javascript
复制
template <typename Base>
Base* GetObject(const std::string& class_name){
    typename std::map<std::string, ObjGen<Base>* >::const_iterator iter = GetObjMap<Base>().find(class_name);
    if(iter == GetObjMap<Base>().end())
    {
        return NULL;
    }
    return (*iter->second)();
}

来到这里,恭喜你已经可以得到对应的类对象了,但是明显还不够,因为没有类型信息,没办法调用对象的接口,幸好所有的JCE对象都是继承taf::JceStructBase,我们可以利用多态,用基类指针调用虚函数方法来完成json到jce的序列化和序列化(readFromJsonStringV2/writeToJsonStringV2)。

代码语言:javascript
复制
struct HospitalReq : public taf::JceStructBase
{
    public:
        static string className()
        {
            return "TencentHealthMini.HospitalReq";
        }
        static string MD5()
        {
            return "325d87d477a8cf7a6468ed6bb39da964";
        }
    ......
}

但是我们发现taf::JceStructBase并没有定义所需要的虚函数,不想修改TAF框架代码,需要怎么样解决这个问题呢?

代码语言:javascript
复制
namespace taf
{
//////////////////////////////////////////////////////////////////
    struct JceStructBase
    {
    protected:
        JceStructBase() {}

        ~JceStructBase() {}
    };
    struct JceException : public std::runtime_error
    {
        JceException(const std::string& s) : std::runtime_error(s) {}
    };
    struct JceEncodeException : public JceException
    {
        JceEncodeException(const std::string& s) : JceException(s) {}
    };
    struct JceDecodeException : public JceException
    {
        JceDecodeException(const std::string& s) : JceException(s) {}
    };
    ......
}

可以实现自己的基类,声明需要的虚函数方法,并让所有JCE类继承我们的基类,这样基类对象就可以调用子类的虚函数了。

代码语言:javascript
复制
namespace THComm
{
//////////////////////////////////////////////////////////////////
    struct JceStructBase:public taf::JceStructBase
    {
        public:
            JceStructBase() {}
            virtual ~JceStructBase() {}
            virtual void writeTo(taf::JceOutputStream<taf::BufferWriter>& _os, UInt8 tag) const 
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void writeToJson(rapidjson::Value& _jVal, rapidjson::Document::AllocatorType& _jAlloc) const
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual std::string writeToJsonString() 
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual std::string writeToJsonStringV2() const
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void readFrom(taf::JceInputStream<taf::BufferReader>& _is, UInt8 tag)
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void readFromJson(const rapidjson::Value& _jVal, bool isRequire = true)
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void readFromJsonString(const std::string & str)
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void readFromJsonStringV2(const std::string & str)
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void writeToString(std::string &content) 
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual void readFromString(const std::string & str)
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual ostream& display(ostream& _os, int _level=0) const
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
            virtual ostream& displaySimple(ostream& _os, int _level=0) const
            {
                LOG_ERROR("not supported");
                throw new std::runtime_error("not supported.");
            }
    };
}

修改JCE2CPP工具,让每个类继承我们的基类,从而调用子类的虚函数。

代码语言:javascript
复制
struct HospitalReq : public THComm::JceStructBase
{
    public:
        static string className()
        {
            return "TencentHealthMini.HospitalReq";
        }
        static string MD5()
        {
            return "325d87d477a8cf7a6468ed6bb39da964";
        }
    ......
}

修改JCE2CPP工具,在产生的对应CPP文件加上各个接口参数对象的注册代码。

代码语言:javascript
复制
#include "ThRegisterMain.h"
#include "jce/wup.h"
#include "servant/BaseF.h"
using namespace wup;
namespace TencentHealthMini
{
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, HospitalReq)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, HospitalRsp)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, PayAppointReq)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, PayAppointRsp)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, ScheduleReq)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, ScheduleRsp)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, SourceReq)
......
}

2.参数类型注册实现

同样,声明一个static map用于存储参数类型,不管入参还是出参都存于此。

Key:命名空间+接口类+接口名+参数名

Value:参数类型,比如JCE接口定义:int getHospitalInfo(ThHead head,HospitalReq req,out HospitalRsp rsp);head变量对应的类型是ThHead,req变量对应的类型是HospitalReq,rsp变量对应的类型是HospitalRsp。

代码语言:javascript
复制
map<string, string>& GetParameterTypeMap()
{
    static map<string, string> parameter_type_map;
    return parameter_type_map;
}

注册代码,和上面同样原理,可以看到,除了插入到参数类型map,我们还根据OUT将参数分别插入到入参和出参的vector,用来存储JCE接口的入参和出参顺序,在调用taf接口序列化参数需要用到。

代码语言:javascript
复制
#define REGISTER_PARAMETER(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC, PARAMETER, PARAMETER_TYPE, OUT)\
static struct CLASS_NAMESPACE##INTERFACE_CLASS##FUNC##PARAMETER##Initializer\
{\
    CLASS_NAMESPACE##INTERFACE_CLASS##FUNC##PARAMETER##Initializer()\
    {\
        if (GetParameterTypeMap().find(PARAMETER_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC, PARAMETER)) == GetParameterTypeMap().end())\
            GetParameterTypeMap().insert(make_pair(PARAMETER_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC, PARAMETER), #PARAMETER_TYPE));\
        \
        if (!OUT)\
        {\
            map<string, vector<string> >::iterator iter = GetParameterSequenceMap().find(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC));\
            if (iter == GetParameterSequenceMap().end())\
            {\
                vector<string> parameterSequence;\
                parameterSequence.push_back(#PARAMETER);\
                GetParameterSequenceMap().insert(make_pair(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC), parameterSequence));\
            }\
            else\
            {\
                vector<string>& parameterSequence = iter->second;\
                parameterSequence.push_back(#PARAMETER);\
            }\
        }\
        else \
        {\
            map<string, vector<string> >::iterator iter = GetOutParameterSequenceMap().find(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC));\
            if (iter == GetOutParameterSequenceMap().end())\
            {\
                vector<string> outParameterSequence;\
                outParameterSequence.push_back(#PARAMETER);\
                GetOutParameterSequenceMap().insert(make_pair(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC), outParameterSequence));\
            }\
            else\
            {\
                vector<string>& outParameterSequence = iter->second;\
                outParameterSequence.push_back(#PARAMETER);\
            }\
        }\
    }\
}__##CLASS_NAMESPACE##INTERFACE_CLASS##FUNC##PARAMETER##Initializer;

对外提供获取参数类型、接口入参顺序、出参顺序的三个接口

代码语言:javascript
复制
//获取参数类型
void PrintParameterTypeMap();
map<string, string>& GetParameterTypeMap();
string GetParameterType(const string& CLASS_NAMESPACE, const string& INTERFACE_CLASS, const string& FUNC, const string& PARAMETER);

//获取入参顺序
void PrintParameterSequence();
map<string, vector<std::string> >& GetParameterSequenceMap();
vector<string> GetParameterSequence(const string& CLASS_NAMESPACE, const string& INTERFACE_CLASS, const string& FUNC);

//出参顺序
void PrintOutParameterSequence();
map<string, vector<string> >& GetOutParameterSequenceMap();
vector<string> GetOutParameterSequence(const string& CLASS_NAMESPACE, const string& INTERFACE_CLASS, const string& FUNC);
代码语言:javascript
复制
map<string, vector<string> >& GetParameterSequenceMap()
{
    static map<string, vector<string> > parameter_sequence_map;
    return parameter_sequence_map;
}

map<string, vector<string> >& GetOutParameterSequenceMap()
{
    static map<string, vector<string> > out_parameter_sequence_map;
    return out_parameter_sequence_map;
}

修改JCE2CPP代码,添加注册代码。

代码语言:javascript
复制
#include "ThRegisterMain.h"
#include "jce/wup.h"
#include "servant/BaseF.h"
using namespace wup;
namespace TencentHealthMini
{
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, HospitalReq)
    REGISTER_CLASS(THComm, JceStructBase, TencentHealthMini, HospitalRsp)
    ......

    REGISTER_PARAMETER(TencentHealthMini, ThRegisterMain, getHospitalInfo, head, TencentHealthMini::ThHead, 0)
    REGISTER_PARAMETER(TencentHealthMini, ThRegisterMain, getHospitalInfo, req, TencentHealthMini::HospitalReq, 0)
    REGISTER_PARAMETER(TencentHealthMini, ThRegisterMain, getHospitalInfo, rsp, TencentHealthMini::HospitalRsp, 1)
    ......
}

后端服务taf接口调用

1.按JCE接口入参顺序,将所有入参JCE序列化填充taf::JceOutputStream对象

代码语言:javascript
复制
bool httpImp::tafAsyncCall(HttpRequestPtr httpReq)
{
    RequestInfo& reqInfo = httpReq->reqInfo;
    taf::JceOutputStream<taf::BufferWriter> os;

    for (size_t i = 0; i < reqInfo.argsSequence.size(); i++)
    {
        string& argName = reqInfo.argsSequence[i];
        Arg& arg = reqInfo.args[argName];
        if (NULL == arg._pJceStr)
        {
            LOG_ERROR(httpReq->requestId << ",argName:" << argName << ",jce struct is null");
            return false;
        }

        arg._pJceStr->readFromJsonStringV2(arg.data);
        arg._pJceStr->writeTo(os, i+1);
    }
    ......
}

2.调用taf框架提供的异步回调RPC接口,填入调用服务接口名,参数序列化数据,回调类对象(见下面)。

代码语言:javascript
复制
    ...... 
    CommCallbackPtr callback = new CommCallback;
    callback->httpReq = httpReq;
    map<string, taf::ServantPrx>::iterator iter = g_app._proxyMap.find(reqInfo.service);
    LOG_DEBUG(httpReq->requestId << ",THProxyServer costime:" << TNOWMS - httpReq->acceptReqTime);
    
    if (iter != g_app._proxyMap.end())
    {
        LOG_DEBUG(httpReq->requestId
            << ",service:" << reqInfo.service
            << ",func:" << reqInfo.func
            << "," << reqInfo.reqStr
            << ",context:" << contextStr);
        taf::ServantPrx proxy = iter->second;
        proxy->taf_invoke_async(taf::JCENORMAL, reqInfo.func, os.getByteBuffer(), context, mStatus, callback);
    }
    else
    {
        LOG_ERROR(httpReq->requestId << ",service:" << reqInfo.service << ",");
        return false;
    }

处理接口响应

我们需要实现一个通用的回调类,在onDispatch回调处理后端服务的返回数据(JCE结构)。

代码语言:javascript
复制
class CommCallback: public taf::ServantProxyCallback
{
public:
    virtual ~CommCallback(){}

    void done()
    {

    }

    void exception(taf::Int32 ret)
    {
        LOG_ERROR("ret:" << ret);
    }

    int procResponse(taf::ReqMessagePtr msg ,taf::JceInputStream<taf::BufferReader>& is, int ret);
    
    virtual int onDispatch(taf::ReqMessagePtr msg);

    HttpRequestPtr httpReq;
};
typedef TC_AutoPtr<CommCallback> CommCallbackPtr;

Taf接口响应报文结构:tag0表示接口返回值,后面按入参数顺序填充tag1,tag2...tagN,出参同样按接口定义顺序紧跟其后tagN+1,tagN+2...

如下所示,我们就可以得到所有出参的json串,从而可以给客户端回消息。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯大讲堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档