分享一种不太完美的接入网关设计
作者:朱江,腾讯工程师。
写在前面:
如上图所示,客户端通过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没有反射机制,那么如何根据客户端传过来的命令字创建对象呢,如果是硬编码在代码里,那么可以这样写:
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调用。
类对象注册及参数注册
我们先来看一个客户端请求消息的例子,如下:
{
“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,如下所示,这样做的缺点是协议改动,配置也需要跟着改动。
head = ThHead
q
rsp = HospitalRsp
<
/getHospitalInfo>
较好的办法是:可以像类对象注册一样,把参数类型也注册到map,同时TAF接口参数JCE序列化是需要按顺序的,所以参数顺序也是需要我们知道的。
1.类对象注册实现
定义一个静态map<std::string, ObjGen* >用于存储命令字、对象的产生类。
template < typename Base>
class ObjGen
{
public :
virtual Base* operator () ()
{
return NULL ;
}
};
template < typename Base>
std :: map < std :: string , ObjGen* >& GetObjMap()
{
static std :: map < std :: string , ObjGen* > 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 \
{\
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().find( #CLASS_NAMESPACE “.” #CLASS_NAME) == GetObjMap ().end())\
GetObjMap().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* >::const_iterator iter = GetObjMap().find(class_name);
if (iter == GetObjMap().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& _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& _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 >::iterator iter = GetParameterSequenceMap().find(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC)); \
if (iter == GetParameterSequenceMap().end()) \
{ \
vector parameterSequence; \
parameterSequence.push_back( #PARAMETER);\
GetParameterSequenceMap().insert(make_pair(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC), parameterSequence)); \
} \
else \
{ \
vector& parameterSequence = iter->second; \
parameterSequence.push_back( #PARAMETER);\
} \
} \
else \
{ \
map<string, vector >::iterator iter = GetOutParameterSequenceMap().find(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC)); \
if (iter == GetOutParameterSequenceMap().end()) \
{ \
vector outParameterSequence; \
outParameterSequence.push_back( #PARAMETER);\
GetOutParameterSequenceMap().insert(make_pair(FUNC_INDEX(CLASS_NAMESPACE, INTERFACE_CLASS, FUNC), outParameterSequence)); \
} \
else \
{ \
vector& outParameterSequence = iter->second; \
outParameterSequence.push_back( #PARAMETER);\
} \
} \
} \
}__
##CLASS_NAMESPACE##INTERFACE_CLASS##FUNC##PARAMETER##Initializer;
对外提供获取参数类型、接口入参顺序、出参顺序的三个接口
//获取参数类型
void PrintParameterTypeMap();
map& 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 )
……
}
后端服务taf接口调用
1.按JCE接口入参顺序,将所有入参JCE序列化填充taf::JceOutputStream对象
bool httpImp::tafAsyncCall(HttpRequestPtr httpReq)
{
RequestInfo& reqInfo = httpReq->reqInfo;
taf::JceOutputStream 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::iterator iter = g_app._proxyMap.find(reqInfo.service);
LOG_DEBUG(httpReq->requestId << “,THProxyServer costime:” <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结构)。
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& is, int ret) ;
virtual int onDispatch (taf::ReqMessagePtr msg) ;
HttpRequestPtr httpReq;
};
typedef
TC_AutoPtr CommCallbackPtr;
Taf接口响应报文结构:tag0表示接口返回值,后面按入参数顺序填充tag1,tag2…tagN,出参同样按接口定义顺序紧跟其后tagN+1,tagN+2…
如下所示,我们就可以得到所有出参的json串,从而可以给客户端回消息。