原文链接 http://www.vckbase.com/document/viewdoc/?id=1846
作者:王树栋
摘要
RMI( Remote Method Invocation),远程方法访问,作为java最早的分布式解决方案给网络程序的设计带来了极大的方便。尤其是在做C/S开发中,程序员不必关心底 层网络通讯细节,即可像调用本地对象方法一样调用远端对象方法,方便了程序员编程。
由于没有类似java的“反射”机制,标准C++下实现RMI似乎有些困难。为C++程序员所熟悉的Boost库虽然有RCF实现了类 似RMI的功能,但RCF本身需依赖于Boost::serlization支持,而serlization需要编译之后方可使用,且有诸多限制。
本文试图通过C++特有的代码复用机制模拟实现具有类似RMI功能的类库,虽然不能完全实现java的RMI功能,但较之以往的C/S编程模式有了很大改观,且文中涉及很多C++代码复用技术例如模板,纯虚函数,方法对象等,希望对C++初学者有一定的帮助。
作者才疏学浅,如有不当之处还请读者指正。
关键字
RMI,反射,远程方法访问,虚函数,函数对象,默认模板参数,宏替换。
由一个例子说起
以下实现一个简单的客户端与服务器通讯的例子。例子采用传统的C/S模式,内容很简单,客户端通过调用服务器端的方法向服务器发送数据并接收返回值。
为方便起见,下面分客户端与服务器分别介绍实现。
服务器端
步骤1
创建一个类“Calculate”。
class Calculate{ public: int sum(int a,int b) { printf("int类型的sum方法被调用\r\n"); t1=a; t2=b; return (int)(a+b); } double sum(double a,double b) { printf("double类型的sum方法被调用\r\n"); t1=a; t2=b; return (a+b); } int GetInput() { int a; printf("请输入一个整数以便传输至客户端:\r\n"); scanf("%d",&a); return a; } Student GetStudent(Teacher tt) {//Student ,Teacher 均为自定义类型 student ts; ts.age=10; printf("GetStuden方法被调用\r\n teacher 的名称以及年龄为: %s %d\r\n",tt.name,tt.age); strcpy(ts.name,"StudentJim"); ts.sex=1; return ts; } int t1; int t2; };
步骤2
调用MYRMI宏实现一个服务器端模板“RMIServer”。
MY_RMI_SERVER_CLASS_DECLARE(RMIServer) MY_RMI_ SERVER _FUNCTION_ADD_P2(int,sum,int,int) MY_RMI_ SERVER _FUNCTION_ADD_P2(double,sum,double,double) MY_RMI_ SERVER _FUNCTION_ADD_P0(int,GetInput) MY_RMI_ SERVER _FUNCTION_ADD_P1(Student,GetStudent,Teacher) MY_RMI_ SERVER _CLASS_END()
函数名称以及返回值,参数列表均与Calculate类中所声明一致。
步骤3
以“Calculate”为模板参数实现一个服务器端实例。
RMIServer < Calculate > ServerCalculate;
步骤4
实现一个Calculate类实例 calcluateObject。
Calculate calcluateObject;
步骤5
将calcluateObject对象添加进ServerCalculate中。
ServerCalculate.AddLocalObject(&calcluateObject)
步骤6
ServerCalculate在指定端口监听客户端请求,启动服务。
ServerCalculate.Listen(663)
至此服务器端部署完毕,当有客户端请求到达后,服务器便会自动启动新线程处理请求,同时不影响服务器端其它正常工作,如想停止服务,直接调用ServerCalculate.Stop()函数即可。
客户端
步骤1
创建一个与服务器端相同的类“Calculate”;
代码略
步骤2
客户端调用MYRMI宏实现一个客户端类模板“RMIClient”。
MY_RMI_CLIENT_CLASS_DECLARE(RMIClient) MY_RMI_CLIENT_FUNCTION_ADD_P2(int,sum,int,int) MY_RMI_CLIENT_FUNCTION_ADD_P2(double,sum,double,double) MY_RMI_CLIENT_FUNCTION_ADD_P0(int,GetInput) MY_RMI_CLIENT_FUNCTION_ADD_P1(Student,GetStudent,Teacher) MY_RMI_CLIENT_CLASS_END()
其中,RMIClient为模板名称,sum ,GetInput,GetStudent均为此模板所具有之方法,且均与Calculate中定义之方法相同(包括返回值,函数名,参数表)
步骤3
客户端以“Calculate”为模板参数,声明一个实例“ClientCalculate”,并调用Connect方法链接到服务器。
RMIClient<Calculate> ClientCalculate; ClientCalculate.Connect("127.0.0.1",663);
步骤4
直接调用ClientCalculate成员方法,即可实现客户端于服务器通讯。
int nResult = ClientCalculate.sum(2,33); double dResult = ClientCalculate.sum(2.2,33.21); nResult = ClientCalculate.GetInput(); Teacher t1; Student s1; s1=ClientCalculate.GetStudent(t1);
以上客户端所调用之方法均自服务器端执行,结果返回给客户端。
主要特点
由以上过程可见,采用MYRMI宏实现客户端与服务器之间的通讯过程有如下特点:
1. 实现简单。客户端与服务器传输不同类型的数据无需用大量switch-case语句判断
2. 可扩展性强。动态添加方法比较容易。只需在Calculate类中添加相应定义,并采用MYMRI宏向客户端以及服务器端分别添加所需之方法,即可由客户端对象ClientCalculate直接调用。
3. 通讯过程可控。如果需要,仅需在服务器端调用AddLocalObject()绑定至其它同类型服务器对象即可。Boost中的RCF库尚不具备此功能!
4. 多线程处理。服务器端采用多线程处理,不影响服务器的其它操作。
5. 线程安全的。多个客户端同时连接时,程序自动完成互斥与同步工作。
6. 采用tcp链接。安全可靠。
7. 采用标准C++语法实现。移植性,跨平台性强。
8. 源码发布。只需向程序中添加相应的源文件即可,无需额外的动态链接库支持。
系统实现
本系统采用大量宏替换技术作为其实现方式,其间涉及模板,纯虚函数,方法对象,联合类型等多种C++代码复用技术,为清晰起见,首先介绍系统中涉及的几种主要数据结构。
主要数据结构
class RMIClientBase:客户端数据传输基类。
负责客户端与服务器之间的数据通讯,管理和维护同服务器的连接。具体功能函数如下:
连接服务器:bool Connect(char* tRomateIP,int tRomatePort);
关闭连接:void StopConnect();
判断链接有效性:bool IsAvailable();
向服务器传递数据接收返回内容:bool CallRemoteFunction(RemoteFunctionStub* tRFStub)
class RMIServerBase:服务器端数据传输基类。
接收客户端所传入之参数。
将本地函数返回值返回到客户端。
负责多个客户端连接管理(启动/关闭链接,多线程间互斥同步等),具体功能函数如下:
开始监听:bool Listen(int tPort);
停止监听:bool Stop();
监听网络端连接请求:static DWORD WINAPI ListenThread(LPVOID pPara);
为不影响服务器正常工作,新启动一个线程负责监听客户端请求。对于每个新产生的客户 端连接请求,再次启动一个线程调用ProcessRequest处理新产生的客户端请求。
处理单个链接请求:static DWORD WINAPI ProcessRequest(LPVOID pPara);
对于每个客户端请求,服务器均启动一个单独线程处理其请求,线程间自动完成互斥以及同步工作。
调用本地函数:virtual void CallLocalFunction(const char* pFuncID, void* pParaList,int tParaListLenght,SOCKET tSocket)=0;
此函数为“纯虚函数”由其基类实现之:根据pFuncID指定的本地函数,以pParaList内保存的参数列表为参数调用本地函数。
class FunctionObject:函数对象模板。
将客户但调用函数之参数列表以及函数ID封装为“函数存根(RemoteFunctionStub)”。
以此函数存根为参数调用RMIServerBase的纯虚函数CallRemoteFunction,将函数ID以及参数列表传输到远端服务器。
接受服务器返回,并将结果强制转换为所调用函数的返回值类型,返回调用者。
struct RemoteFunctionStub:函数存根。
用于客户端与服务器间传递所调用函数之信息。
封装了调用函数ID,参数列表,返回值信息等内容。
class ParaListAnalyser:函数参数表解析器。
此为一模板,将参数表中的变量强制转换为指定类型。
服务器端调用本地函数时需要利用此模板解析客户端所传入的参数列表。
各种数据结构以及相互调用关系如图所示
客户端类图
服务器端类图
主要宏定义
本系统采用大量的宏替换,大体可分为“类定义宏”以及“函数添加宏”,以下分别加以说明。
类定义宏
类定义宏完成客户端以及服务器端模板的定义功能。具体如下。
客户端模板定义:
#define MY_RMI_CLIENT_CLASS_DECLARE(client_class_name) \ template<typename classname> \ class client_class_name:public RMIClientBase \ { \ private: \
说明:
本模板定义比较简单,仅仅声明一个类模板,使之继承自“ RMIClientBase ”。
服务器端模板定义:
#define MY_RMI_SERVER_CLASS_DECLARE(server_class_name) \ template <typename TServer> \ class server_class_name :public RMIServerBase \ { \ public: \ server_class_name() \ { \ mServerClassName=typeid(TServer).name(); \ pMServer=NULL; \ }; \ bool AddLocalObject(TServer* pTServer) \ { \ pMServer=pTServer;return true; \ }; \ bool IsRunning(); \ private: \ std::string mServerClassName; \ TServer* pMServer; \ void CallLocalFunction(const char* pFuncID, void* pParaList,int tParaListLenght,SOCKET tSocket)\ { \ std::string FunTempID;
说明:
1.定义一模板类,并使之继承自“ RMIServerBase ”;
2.添加构造函数,并初始化“本地对象指针”(pMServer),以及“本地对象类型名称”(mServerClassName);
3.定义私有属性:pMServer,mServerClassName;
4.实现基类(RMIServerBase)之纯虚函数 CallLocalFunction;
接下来的函数添加宏中会逐渐完善CallLocalFunction 方法。
函数添加宏
函数添加宏末尾的数字代表所要添加的函数的参数数目。最多允许添加具有9个函数参数的函数。
宏定义中各参数意义依次为:返回值,函数名,参数1,参数2,。。。
例如
MY_RMI_CLIENT_FUNCTION_ADD_P2(double,sum,double,double)
第一个“double” 表示函数返回值为“double”类型。
函数名称为“sum”;
函数具有两个参数,且两者均为“double”类型。
如果所要添加的函数返回值为“void”,则调用含有“_V_”的宏。
例如要添加具有一个参数,且没有返回值的函数:
MY_RMI_SERVER_FUNCTION_ADD_V_P1(MyFunction,int)
此宏添加一个返回值为“void”类型,只有一个“int”类型参数,名称为“MyFunction”的函数。
客户端与服务器端函数添加宏略有不同,以下分开说明。
客户端函数添加宏
分为“有参数”与“无参数”两种。以下均以添加具有两个参数的函数为例来说明。
有参数
#define MY_RMI_CLIENT_FUNCTION_ADD_P2(R,FunName,P1,P2)\ public:\ R FunName(P1 p1,P2 p2)\ {\ return FunctionObject<R,P1,P2>()(JOINSTRING4(R,FunName,P1,P2),this,p1,p2);\ }
1.直接在由“MY_RMI_CLIENT_CLASS_DECLARE”宏定义的类中添加函数名为“FunName”,返回值为“R”,参数分别为“P1”“P2”的函数声明。
2.以内联函数的形式完成函数定义。
3.以所传入的函数返回值类型“R”,以及两个参数类型“P1”“P2”为模板参数,实现一个临时FunctionObject方法对象。
4.通过“JOINSTRING4”宏,将R和FunName以及P1,P2合成生成一个字符串作为所要添加的函数的“函数ID”。有关“函数ID”的详细内容见下文。
5.以生成的函数ID,对象本身的指针以及所要定义的函数两个参数变量作为参数调用方法对象(FunctionObject)。
6.将方法对象的返回值直接返回给调用者。
无参数
#define MY_RMI_CLIENT_FUNCTION_ADD_V_P2(FunName,P1,P2)\ public:\ void FunName(P1 p1,P2 p2)\ {\ FunctionObject<MYVOIDCLASS,P1,P2>()(JOINSTRING4(void,FunName,P1,P2),this,p1,p2);\ }
与有返回值类似,仅仅不需要将函数对象的返回值返回而已。
服务器端函数添加宏
不同于客户端直接向类模板中添加方法定义以及实现,服务器端的方法添加宏仅仅是完善服务器端基类RMIServerBase的纯虚函数:CallLocalFunction。
关于CallLocalFunction
CallLocalFunction函数由服务器端基类:RMIServerBase在收到客户端发来的函数调用请求时调用之。
其函数原型如下:
void CallLocalFunction(const char* pFuncID, void* pParaList,int tParaListLenght,SOCKET tSocket)
其中:
pFuncID 表示客户端所要调用的本地函数ID。
pParaList 内保存着所要调用的函数参数列表。
tParaListLenght 表示参数列表指针pParaList的长度。
tSocket 表示客户端的连接socket,以便通过此socket将函数的返回值发送至客户端。
关于函数ID
在客户端与服务器端的通讯过程中由“函数ID”唯一确定客户端所要调用的服务器端具体函数;
此为字符串,在使用函数添加宏时由JOINSTRINGi()宏根据所要添加的函数返回值,名称,参数类型列表直接拼接而成。
“JOINSTRINGi”宏末尾的“i”表示要拼接的单词数目。
例如一个函数的声明如下:
int sum(int a,int b);
则使用:JOINSTRING4(int,sum,int,int) 即可生成函数ID :“intsumintint”
与客户端类似,服务器端的函数添加宏亦分为“有参数”于“无参数”两种。
以下亦以添加具有两个参数的宏为例子加以说明。
有参数 :
#define MY_RMI_SERVER_FUNCTION_ADD_P2(R,FunName,P1,P2) \ if(strcmp(JOINSTRING4(R,FunName,P1,P2),pFuncID)==0) \ { \ R m##R##FunName##P1##P2=\ pMServer->FunName(ParaListAnalyser<R,P1,P2>(pParaList).GetP1(),\ ParaListAnalyser<R,P1,P2>(pParaList).GetP2());\ SendResponse(&m##R##FunName##P1##P2,\ sizeof(R),tSocket); \ return ; \ }
1. 利用“JOINSTRING4”宏生成函数ID。
2. 生成一个与返回值类型一致的临时变量用于保存本地函数的返回值。
3. 比较宏生成的函数ID与CallLocalFunction方法中传入的pFuncID是否相同,如果相同则以保存的本地对象指针调用指定函数。
4. 用函数返回值,以及参数类型列表作为模板参数实例化一个ParaListAnalyser临时对象。
5. 调用ParaListAnalyser对象方法解析参数表。
6. 调用本地方法。
7. 将要函数返回值返回至客户端。
无参数:
#define MY_RMI_SERVER_FUNCTION_ADD_V_P2(FunName,P1,P2) \ if(strcmp(JOINSTRING4(void,FunName,P1,P2),pFuncID)==0) \ { \ pMServer->FunName(ParaListAnalyser<MYVOIDCLASS,P1,P2>(pParaList).GetP1(),\ ParaListAnalyser<MYVOIDCLASS,P1,P2>(pParaList).GetP2());\ return ; \ }
无参数函数添加宏与有参数函数添加宏大体类似,所不同者有以下几个方面。
1. 指定模板ParaListAnalyser的模板参数时,采用“MYVOIDCLASS”作为第一模板参数。
“MYVOIDCLASS”为一特殊数据类型,用于指定模板参数时,区别有返回值的情况,以便ParaListAnalyser做不同处理
2. 因无返回值,本地函数执行完毕后即直接返回,无需将结果返回至客户端,亦无必要生成一临时变量用于保存本地函数返回值。
详细处理过程
下面以客户端以及服务器端分别加以说明
客户端
客户端采用主动请求方式与服务器通讯,当客户端有方法调用时,即将函数所需之参数发送至服务器,并接收返回值。
下面以客户端调用sum方法“ ClientCalculate.sum(2.2,33.21)”为例介绍处理过程。
在采用 MY_RMI_CLIENT_FUNCTION_ADD_P2(double,sum,double,double) 向RMIClient中添加sum方法后的结果代码如下
public: double sum(double p1,double p2) { return FunctionObject<double ,double ,double >()(JOINSTRING4(double ,sum,double ,double ),this,p1,p2); }
客户端详细处理过程如图所示
服务器端
采用服务器端类定义宏定义RMIServer 模板,并使用方法添加宏完善其CallLocalFunction方法。
CallLocalFunction方法经完善后内容如下(仅以添加的sum方法为例):
void CallLocalFunction(const char* pFuncID, void* pParaList,int tParaListLenght,SOCKET tSocket) { if(strcmp(JOINSTRING4(double,sum,double,double),pFuncID)==0) { Double mdoublesumdoubledouble = pMServer->sum( ParaListAnalyser<double,double,double>(pParaList).GetP1(), ParaListAnalyser<double,double,double>(pParaList).GetP2() ) SendResponse(&mdoublesumdoubledouble,sizeof(double),tSocket); } ... ... ... }
服务器端首先以所要实现RMI的类为模板参数实例化一个对象ServerCalculate
接着调用ServerCalculate.AddLocalObject(&calcluateObject)将所要实现远程方法访问的本地对象 添加到ServerCalculate中,随后调用ServerCalculate.Listen(663)实现在本地663端口监听。至此服务器已经启 动,当有客户端发来方法调用请求后,服务器即可自动启动一单独线程处理请求并返回结果。
服务器端详细处理过程如图所示
不足与改进
由于作者能力有限以及时间仓促,程序尚有许多不尽如人意之处,具体表现在以下几方面。
1。 安全性问题。对于客户端的连接请求,服务器端未做授权检查。任何与服务器端所绑定之类实例有相同成员函数者均可调用之。
2。 不支持指针以及引用。
3。 缺乏对客户端连接的日志统计功能。诸如客户端连接数目,请求时间,退出时间等服务器尚不具备记录统计此等信息之能力。
4。 错误处理不完善。对于连接过程中网络中出现错误,客户端以及服务器端均未作检测,也就未能对错误做出及时有效的反应。
针对以上问题,有兴趣的读者可以自行完善扩充,一来可以大大增强本程序的实用性。二来,文中涉及的很多C++代码复用技术亦不失为各位读者学习领会C++的难度机会。