我們通常會面對這樣的問題:整合不同平台或不同類庫,這些類庫可能來自不同的語言,甚至不同的操作系統。 如何解決這類棘手的問題呢?
一.方案介紹
解決不同語言交互的方法有不少,對我了解的windows系統和.NET平台,有以下幾種做法:
- P/Invoke: 調用native cpp的方法,處在同一個內存區間,訪問方便,但包裝困難,可能拋出運行時異常。
- 讀寫文件:通過一頭讀文件,一頭寫文件的方式實現交互。諸位別笑,本科時候我就用過這種方式解決問題。
- 命名管道/socket: 通過字節數組的方式實現交互,命名管道是windows系統提供的功能,可提供安全快捷的程序間交互。socket不依賴於操作系統,只要給定包格式,在任何支持socket的語言平台下都能支持。但缺點也很明顯,如果交互復雜,那么解析這種byte[]數組將會非常復雜而且難以維護。
- RPC: 又稱之為遠程過程調用,也是我們今天的主角。
數據即程序,RPC說白了依舊是傳遞數據的過程,只是過程在代碼上更像函數調用。如下圖:
目前主流的RPC有兩種: XML和JSON。 XML是曾經的主角,兼容性更好。但如今移動互聯網要求數據流量要小,而XML的缺點也隨之暴露出來,JSON由於節省數據(大大減少了包頭和標記的開銷),如今變得更受歡迎。新浪微博API,如今全部升級為JSON了。 RPC的實質是http協議,它封裝了底層實現的細節,能讓我們將注意力放在應用邏輯的實現,而非建立連接這樣的問題。
RPC的優點很多,其中我最喜歡的是它的容器,聲明一個Array,里面可以塞任何你想要的數據,int,string,double,struct甚至另外一個array都可以。當然,不能傳遞抽象類或接口,畢竟不是同一內存區域。
本文我們只介紹XML-RPC實現C++和C#兩個應用程序之間的交互。JSON的C#版本Jayrock對RPC的支持,尤其是對非ASP.NET環境幾乎沒有,連一篇像樣的文檔都找不到,所以我們僅僅討論XML-RPC。
二. 方案實現
我們打算將C#作為客戶端,C++作為服務器端。
1. C++的服務器實現
我們在VS2010中新建C++工程,將附件中的XMLRPC.LIB靜態庫拷入當前工程文件夾,設置當前工程為release模式。同時在C++工程設置中,添加兩個lib引用: xmlrpc.lib, ws2_32.lib
之所以用release模式,是因為在debug模式下xmlrpc.lib庫會出現如下的編譯錯誤:花了很久時間都沒解決,如果有大神能幫助解決這個問題,請一定留言.
1> All outputs are up-to-date. 1>XmlRpc.lib(XmlRpcServer.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcUtil.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcServerMethod.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcValue.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcDispatch.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcServerConnection.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcSocket.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj 1>XmlRpc.lib(XmlRpcSource.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in HelloServer.obj
再將對應的庫頭文件加入到項目中,完成后如下圖所示:
現在C#程序員普遍已經忘掉了怎么編譯和使用C++靜態庫,我也是其中之一,如果你在上面幾步遇到困難,不妨查看相關資料。
接下來,我們將開始編寫工作代碼:
首先是添加頭文件引用,一般只要引用XMLRPC.H和XMLRPCVALUE.H即可, 同時聲明一個全局的XmlRpcServer 服務。(實在太不習慣C++中不new就能創建實例的語法了)
#include "RPC/XmlRpc.h" #include "RPC/XmlRpcValue.h" #include <iostream> #include <stdlib.h> using namespace XmlRpc; // The server XmlRpcServer s;
添加主函數:
int main() { /*if (argc != 2) { std::cerr << "Usage: HelloServer serverPort\n"; return -1; }*/ //int port = atoi(argv[1]); int port=2567; XmlRpc::setVerbosity(3); // Create the server socket on the specified port s.bindAndListen(port); // Enable introspection s.enableIntrospection(true); // Wait for requests indefinitely s.work(-1.0); return 0; }
-
設置端口,綁定端口和啟動.這些都沒什么好說的。值得注意的是調試等級,setVerbosity。等級越高,輸出信息越詳細,最高等級可輸出完整的xml交互文件供調試,但會嚴重拖累系統速度的。在你測試功能完畢后,不妨將其設為0。
接下來我們創建幾個類,來實現服務端功能:
// A variable number of arguments are passed, all doubles, result is their sum. class Sum : public XmlRpcServerMethod { public: Sum(XmlRpcServer* s) : XmlRpcServerMethod("Sum", s) {} void execute(XmlRpcValue& params, XmlRpcValue& result) { int nArgs = params.size(); double sum = 0.0; for (int i=0; i<nArgs; ++i) sum += double(params[i]); result = sum; } } sum(&s);
所有功能都以繼承於XmlRpcServerMethod類,同時改寫其execute函數。 有點意思的是,這個XmlRPCValue數據類型,是類似C#的Dictionary,或JAVA的hashset。 你可以通過類似C#索引器(字典)的方式,添加或讀取該結構中的內容。比如上面的params[i]。由於代碼簡單,就不多做詳細解釋。
類后跟了一個實例,sum(&s),這樣就在服務器中注冊了該功能。
再創建一個類,來實現字符串操作:
// One argument is passed, result is "Hello, " + arg. class HelloName : public XmlRpcServerMethod { public: HelloName(XmlRpcServer* s) : XmlRpcServerMethod("HelloName", s) {} void execute(XmlRpcValue& params, XmlRpcValue& result) { std::string resultString = "Hello, "; resultString += std::string(params[0]); result = resultString; } } helloName(&s);
也不多做解釋了。
我們再看一下,怎么存取RPC中的字典和數組,這才是精髓部分:
class StructData : public XmlRpcServerMethod { public: StructData(XmlRpcServer* s) : XmlRpcServerMethod("GetStruct", s) {} void execute(XmlRpcValue& params, XmlRpcValue& result) { XmlRpcValue A; A.setSize(2); A[0]["a"]=123; A[0]["b"]=456; A[1]["a"]=43; A[1]["b"]=425; result["a"]=A; result["b"]=123; } } structData(&s);
設置字典時,是不需要指定其size的,但若設定的是數組,則必須使用setsize方法設定其大小。字典的值也可包含另外一個XmlRpcValue 結構體。
RPC中可以很好的處理字符串,int,double, datetime類型,但枚舉類型的支持並不好,我建議直接傳int.
完成了這三個服務后,我們來編寫C#的客戶端。
2. C# RPC客戶端實現:
新建一個C#工程,創建以下的接口類:
public interface IRPCMethod { [XmlRpcMethod("HelloName")] string HelloName(string Name); [XmlRpcMethod("Sum")] double Sum(double a,double b); [XmlRpcMethod("GetStruct")] XmlRpcStruct GetStruct(); }
當然,要引用CookComputing.XmlRpc庫。 使用接口類的作用是剝離RPC對系統的影響,讓系統可以“透明的”調用RPC代碼。值得注意的是,名稱必須與C++中的名稱一致,否則會出現找不到方法的異常。
我們注冊客戶端服務:
var chnl = new HttpChannel(null, new XmlRpcClientFormatterSinkProvider(), null); ChannelServices.RegisterChannel(chnl, false); var svr = (IRPCMethod)Activator.GetObject(typeof(IRPCMethod), "http://localhost:2567/"); Console.WriteLine("成功注冊信道"); string ret = svr.HelloName("haha"); Console.WriteLine("調用helloName方法:" + ret); double result = svr.Sum(23, 18); Console.WriteLine("調用Sum方法:" + result.ToString()); XmlRpcStruct result2 = svr.GetStruct();
至於result2結構體,你可以通過調試來查看具體的運行結果。
在服務器端,可以看到調用所花費的流量和方法名稱。
三. 其他
這里我們關注一些額外的問題:
1.流量
RPC的一種場景是本地不同程序調用,這種情況下速度很快。但在跨機器或是移動設備上,就必須考慮流量因素了。XML的“性價比”並不高:
<?xml version="1.0"?> <methodCall> <methodName>echo</methodName> <params> <param><value><string>Hello Chris</string></value></param> <param><value><i4>123</i4></value></param> </params> </methodCall>
實際的有用數據,僅占所有字節數的5%,甚至更少,除非是大批量的傳輸本文數據。如果是流量敏感,推薦使用JSON.
2. 性能
我們當然要關心,RPC在本機調用會有多快?和哪些因素敏感? 筆者配置是i7 2600K, 8GB DDR, Gbps網絡適配器,VS的debug模式。在執行Sum操作時,一千次耗時4.3ms。 在執行更復雜的結構體傳遞(大概有20個double,三個string,兩個int時), 千次耗時5.8ms。
因此,可以得知,XML的轉換和解析幾乎不耗時,建立連接后,執行一次在ms量級,對數據結構復雜程度不敏感,因此,若是實時性敏感應用,建議一次性多傳些數據。
3. 兼容性
筆者發現,RPC的兼容性並不太好,在和JAVA采用RPC交互時,就遇到了困難,”XML解析異常”。JSON的的兼容性不見得比XML更好,實際操作更是問題多多。涉及RPC的社區,普遍文檔較少,例子不全,出現問題也不好排查,更沒有太多跨語言的RPC實例。因此,如果你能用P/Invoke, 還是推薦用直接調用的做法。 RPC肯定還是比byte流的socket方便很多。
四 .總結和源代碼下載
RPC將原本復雜的數據傳輸問題簡化了,使我們從復雜的數據包結構,JAVA和C的double編碼和socket傳輸中脫離出來,提供了更簡單方便的方案。但必須看到,它並不完善,我們只能一步步的探索。
另外想問一句,Unity3D的RPC是何種格式?
有任何問題,歡迎隨時交流。
源代碼下載: