45

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

 4 years ago
source link: https://www.tuicool.com/articles/NnuEZ3Y
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

640?wx_fmt=jpeg

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

写在前面:

640?wx_fmt=png

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

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

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

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

640?wx_fmt=jpeg

限制

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

640?wx_fmt=jpeg

思路

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

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调用。

640?wx_fmt=jpeg

类对象注册及参数注册

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

{

"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接口定义,如下:

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,如下所示,这样做的缺点是协议改动,配置也需要跟着改动。

<getHospitalInfo>

<args>

head = ThHead

req = HospitalRe

q

rsp = HospitalRsp

<

/getHospitalInfo>

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

1.类对象注册实现

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

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。

# 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,传进类型名字符串就可以得到对应类对象

template < typename Base>

Base* Get Object ( 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)。

struct HospitalReq : public taf::JceStructBase

{

public :

static string className ()
{

return "TencentHealthMini.HospitalReq" ;

}

static string MD5 ()
{

return "325d87d477a8cf7a6468ed6bb39da964" ;

}

......

}

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

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类继承我们的基类,这样基类对象就可以调用子类的虚函数了。

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工具,让每个类继承我们的基类,从而调用子类的虚函数。

struct HospitalReq : public THComm::JceStructBase

{

public :

static string className ()
{

return "TencentHealthMini.HospitalReq" ;

}

static string MD5 ()
{

return "325d87d477a8cf7a6468ed6bb39da964" ;

}

......

}

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

# 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。

map < string , string >& GetParameterTypeMap()

{

static map < string , string > parameter_type_map;

return parameter_type_map;

}

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

# define

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;

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

//获取参数类型

void PrintParameterTypeMap();

map<string, string>& GetParameterTypeMap();

//获取入参顺序

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);

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代码,添加注册代码。

# 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 )

......

}

640?wx_fmt=jpeg

后端服务taf接口调用

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

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接口,填入调用服务接口名,参数序列化数据,回调类对象(见下面)。

...... 

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 ;

}

640?wx_fmt=jpeg

处理接口响应

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

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串,从而可以给客户端回消息。

640?wx_fmt=png

640?wx_fmt=png

一种分布式布隆过滤器设计

那些熟悉却说不出的设计法则

微信大更新!支持多任务操作,还有超好用的 10 大新功能

年度好文:腾讯工程师的自我修炼

640?wx_fmt=gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK