由於沒有類似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;
步驟4
ClientCalculate.Connect("127.0.0.1",663);
直接調用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++的難度機會。
後記
本文模擬boost RCF使用方式,實現了一個類似RMI功能的開發包,距離真正的應用尚有很長的路要走,作者意在通過本文展示一下C++強大的代碼復用技術,以期拋磚引玉,博方家一笑。
文中涉及之代碼在windowsXP vc6.0 下編譯通過,有意者可發送郵件至[email protected]索取。
關於作者:本文作者王樹棟,北方工業大學在校研究生,平時熱衷於計算機底層開發,對Linux情有獨鐘。
本文配套源碼