|
一、什么是遠(yuǎn)程過程調(diào)用 什么是遠(yuǎn)程過程調(diào)用 RPC(Remote Procedure Call)? 你可能對(duì)這個(gè)概念有點(diǎn)陌生, 而你可能非常熟悉 NFS, 是的, Win32 RPC 編程(一)我們從一個(gè)簡(jiǎn)單的 RPC “Hello, world!”的例子開始。 參考資料:MSDN: Win32 and COM Development -> Networking -> Network Protocols -> Remote Procedure Calls (RPC) 第1步:編寫 IDL(Interface Description Language,接口描述語(yǔ)言)文件 ------------------------------------------------------------------------- IDL 是一個(gè)通用的工業(yè)標(biāo)準(zhǔn)語(yǔ)言,大家應(yīng)該不陌生,因?yàn)?COM 里面也是用它來描述接口的。 Hello.idl: [ uuid("4556509F-618A-46CF-AB3D-ED736ED66477"), // 唯一的UUID,用 GUIDGen 生成 version(1.0) ] interface HelloWorld { // 我們定義的方法 void Hello([in,string]const char * psz); void Shutdown(void); } 一個(gè)可選的文件是應(yīng)用程序配置文件(.acf),它的作用是對(duì) RPC 接口進(jìn)行配置,例如下面的 Hello.acf 文件: Hello.acf: [ implicit_handle(handle_t HelloWorld_Binding) ] interface HelloWorld { } 上面定義了 implicit_handle,這樣客戶端將綁定句柄 HelloWorld_Binding 了,后面的客戶端代碼中我們會(huì)看到。 編譯 IDL 文件: >midl Hello.idl Microsoft (R) 32b/64b MIDL Compiler Version 6.00.0366 Copyright (c) Microsoft Corporation 1991-2002. All rights reserved. Processing ./Hello.idl Hello.idl Processing ./Hello.acf Hello.acf 我們可以看到自動(dòng)生成了 Hello.h, Hello_s.c, Hello_c.c 文件,這些叫做 rpc stub 程序,不過我們可以不管這個(gè)概念, 我們只需要知道 Hello.h 里面定義了一個(gè) extern RPC_IF_HANDLE HelloWorld_v1_0_s_ifspec; 這個(gè) RPC_IF_HANDLE 將在后面用到。 第2步:編寫服務(wù)端程序 ------------------------------------------------------------------------- 第1步中我們已經(jīng)約定了調(diào)用的接口,那么現(xiàn)在我們開始實(shí)現(xiàn)其服務(wù)端。代碼如下: server.c #include #include #include "Hello.h" // 引用MIDL 生成的頭文件 /** * 這是我們?cè)贗DL 中定義的接口方法 * 需要注意一點(diǎn),IDL 里面的聲明是:void Hello([in,string]const char * psz); * 但是這里變成了const unsigned char *,為什么呢? * 參見MSDN 中的MIDL Command-Line Reference -> /char Switch * 默認(rèn)的編譯選項(xiàng),對(duì) IDL 中的char 按照unsigned char 處理 */ void Hello(const unsigned char * psz) { printf("%s/n", psz); } /** 這也是我們?cè)贗DL 中定義的接口方法,提供關(guān)閉server 的機(jī)制*/ void Shutdown(void) { // 下面的操作將導(dǎo)致 RpcServerListen() 退出 RpcMgmtStopServerListening(NULL); RpcServerUnregisterIf(NULL, NULL, FALSE); } int main(int argc,char * argv[]) { // 用Named Pipe 作為RPC 的通道,這樣EndPoint 參數(shù)就是Named Pipe 的名字 // 按照Named Pipe 的命名規(guī)范,/pipe/pipename,其中pipename 可以是除了/ // 之外的任意字符,那么這里用一個(gè)GUID 串來命名,可以保證不會(huì)重復(fù) RpcServerUseProtseqEp((unsigned char *)"ncacn_np", 20, (unsigned char *)"http://pipe//{8dd50205-3108-498f-96e8-dbc4ec074cf9}", NULL); // 注冊(cè)接口,HelloWorld_v1_0_s_ifspec 是在MIDL 生成的Hello.h 中定義的 RpcServerRegisterIf(HelloWorld_v1_0_s_ifspec, NULL, NULL); // 開始監(jiān)聽,本函數(shù)將一直阻塞 RpcServerListen(1,20,FALSE); return 0; } // 下面的函數(shù)是為了滿足鏈接需要而寫的,沒有的話會(huì)出現(xiàn)鏈接錯(cuò)誤 void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len) { return(malloc(len)); } void __RPC_USER midl_user_free(void __RPC_FAR *ptr) { free(ptr); } 編譯: >cl /D_WIN32_WINNT=0x500 server.c Hello_s.c rpcrt4.lib 用于 80x86 的 Microsoft (R) 32 位 C/C++ 優(yōu)化編譯器 14.00.50727.42 版 版權(quán)所有(C) Microsoft Corporation。保留所有權(quán)利。 server.c Hello_s.c 正在生成代碼... Microsoft (R) Incremental Linker Version 8.00.50727.42 Copyright (C) Microsoft Corporation. All rights reserved. /out:server.exe server.obj Hello_s.obj rpcrt4.lib 編譯時(shí)為什么要指定 _WIN32_WINNT=0x500 呢?因?yàn)槿绻麤]有的話會(huì)報(bào)告下面的錯(cuò)誤: Hello_s.c(88) : fatal error C1189: #error : You need a Windows 2000 or later to run this stub because it uses these features: 第3步:編寫客戶端程序 ------------------------------------------------------------------------- 客戶端的代碼: client.c #include #include #include #include "Hello.h" // 引用MIDL 生成的頭文件 int main(int argc, char * argv[]) { unsigned char * pszStringBinding = NULL; if ( argc != 2 ) { printf("Usage:%s return 1; } // 用Named Pipe 作為RPC 的通道。參見server.c 中的RpcServerUseProtseqEp() 部分 // 第3 個(gè)參數(shù)NetworkAddr 如果取NULL,那么就是連接本機(jī)服務(wù) // 否則要取////servername 這樣的格式,例如你的計(jì)算機(jī)名為jack,那么就是//jack RpcStringBindingCompose( NULL, (unsigned char*)"ncacn_np", /*(unsigned char*)"http:////servername"*/ NULL, (unsigned char*)"http://pipe//{8dd50205-3108-498f-96e8-dbc4ec074cf9}", NULL, &pszStringBinding ); // 綁定接口,這里要和 Hello.acf 的配置一致,那么就是HelloWorld_Binding RpcBindingFromStringBinding(pszStringBinding, & HelloWorld_Binding ); // 下面是調(diào)用服務(wù)端的函數(shù)了 RpcTryExcept { if ( _stricmp(argv[1], "SHUTDOWN") == 0 ) { Shutdown(); } else { Hello((unsigned char*)argv[1]); } } RpcExcept(1) { printf( "RPC Exception %d/n", RpcExceptionCode() ); } RpcEndExcept // 釋放資源 RpcStringFree(&pszStringBinding); RpcBindingFree(&HelloWorld_Binding); return 0; } // 下面的函數(shù)是為了滿足鏈接需要而寫的,沒有的話會(huì)出現(xiàn)鏈接錯(cuò)誤 void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len) { return(malloc(len)); } void __RPC_USER midl_user_free(void __RPC_FAR *ptr) { free(ptr); } 編譯: >cl /D_WIN32_WINNT=0x500 client.c Hello_c.c rpcrt4.lib 用于 80x86 的 Microsoft (R) 32 位 C/C++ 優(yōu)化編譯器 14.00.50727.42 版 版權(quán)所有(C) Microsoft Corporation。保留所有權(quán)利。 client.c Hello_c.c 正在生成代碼... Microsoft (R) Incremental Linker Version 8.00.50727.42 Copyright (C) Microsoft Corporation. All rights reserved. /out:client.exe client.obj Hello_c.obj rpcrt4.lib 第4步:測(cè)試: ------------------------------------------------------------------------- 運(yùn)行 server.exe,將彈出一個(gè) console 窗口,等待客戶端調(diào)用。 運(yùn)行客戶端 client.exe: >client hello 可以看到 server.exe 的 console 窗口出現(xiàn) hello 的字符串。 >client shutdown server.exe 退出。 Win32 RPC 編程(二)這部分基本和上一節(jié)一樣,不過上一節(jié)中 RPC 是通過 Named Pipe 調(diào)用的,這里我們?cè)僭囈幌?TCP 的方式。 代碼大部分都是相同的, IDL 接口不用變(無(wú)論是通過什么方式 RPC,接口都是與之無(wú)關(guān)的)。 服務(wù)端要換成 TCP 的方式: --------------------------------- int main(int argc,char * argv[]) { // 用TCP 方式作為RPC 的通道。綁定端口13521。 RpcServerUseProtseqEp( (unsigned char *)"ncacn_ip_tcp", RPC_C_PROTSEQ_MAX_REQS_DEFAULT, (unsigned char *)"13521", NULL); // 注意:從Windows XP SP2 開始,增強(qiáng)了安全性的要求,如果用 RpcServerRegisterIf() 注冊(cè) // 接口,客戶端調(diào)用時(shí)會(huì)出現(xiàn) RpcExceptionCode() == 5,即Access Denied 的錯(cuò)誤. 因此,必 // 須用 RpcServerRegisterIfEx 帶 RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH 標(biāo)志允許客戶端直 // 接調(diào)用。 // RpcServerRegisterIf(HelloWorld_v1_0_s_ifspec, NULL, NULL); RpcServerRegisterIfEx( HelloWorld_v1_0_s_ifspec, // Interface to register. NULL, NULL, // Use the MIDL generated entry-point vector. RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, 0, NULL); // 后面都相同 ... return 0; } 客戶端的調(diào)用方式也要換: --------------------------------- int main(int argc, char * argv[]) { // 前面都相同 ... // 用 TCP 方式作為 RPC 的通道。服務(wù)器端口 13521。第3個(gè) // 參數(shù) NetworkAddr 如果取 NULL,那么就是連接本機(jī)服務(wù), // 也可以取IP, 域名, servername 等 RpcStringBindingCompose( NULL, (unsigned char*)"ncacn_ip_tcp", (unsigned char*)"localhost" /*NULL*/, (unsigned char*)"13521", NULL, &pszStringBinding ); // 后面都相同 ... } 別的地方都是一樣的。 我們?cè)谏弦还?jié)的基礎(chǔ)上,討論如何實(shí)現(xiàn)異步的 RPC 調(diào)用。前兩節(jié)演示的函數(shù)調(diào)用都是同步的,即調(diào)用函數(shù) Hello() 時(shí), 客戶端將阻塞住直到服務(wù)端的 Hello() 函數(shù)返回。如果服務(wù)端函數(shù)需要進(jìn)行一些費(fèi)時(shí)的操作,例如復(fù)雜的計(jì)算、查詢, 客戶端只能一直阻塞在那里。這種情況下,我們可以使用異步的 RPC 提高客戶端的性能。 異步的RPC是通過配置文件(.acf)來啟用的: -------------------------------------------- Hello.acf: [ implicit_handle(handle_t HelloWorld_Binding) ] interface HelloWorld { [async] Hello(); // 增加了 [async] 表明這是異步調(diào)用 } 原來的接口 HelloWorld 有兩個(gè)方法,Hello() 和 Shutdown(),Shutdown() 我們?nèi)匀蛔屗峭秸{(diào)用,所以在.acf文 件中不用列出。IDL 接口文件還是可以不用修改。 服務(wù)端的代碼 server.c 中的 Hello() 要改成下面的樣子: ------------------------------------------------------ void Hello(PRPC_ASYNC_STATE rpcAsyncHandle, const unsigned char * psz) { // 模擬一個(gè)長(zhǎng)時(shí)間的操作 printf("Sleep 5 seconds.../n"); Sleep(5000); printf("%s/n", psz); // 表明調(diào)用已經(jīng)完成 RpcAsyncCompleteCall(rpcAsyncHandle, NULL); } 服務(wù)端的其它代碼不用修改。 客戶端client.c中的調(diào)用方式也要換: --------------------------------- int main(int argc, char * argv[]) { // 前面都相同 ... // 下面是調(diào)用服務(wù)端的函數(shù) RpcTryExcept { if ( _stricmp(argv[1], "SHUTDOWN") == 0 ) { Shutdown(); } else { // 初始化異步調(diào)用 RPC_ASYNC_STATE async; RpcAsyncInitializeHandle( &async, sizeof(async) ); async.UserInfo = NULL; async.NotificationType = RpcNotificationTypeNone; // 本函數(shù)能立即返回 Hello( &async, (unsigned char*)argv[1]); // 查詢調(diào)用的狀態(tài) while ( RpcAsyncGetCallStatus(&async) == RPC_S_ASYNC_CALL_PENDING ) { printf("Call Hello() pending, wait 1s.../n"); Sleep(1000); } // 通知調(diào)用已經(jīng)完成 RpcAsyncCompleteCall( &async, NULL ); } } RpcExcept(1) { printf( "RPC Exception %d/n", RpcExceptionCode() ); } RpcEndExcept // 后面都相同 ... } 這樣客戶端就實(shí)現(xiàn)了異步調(diào)用! Win32 RPC 編程(四)這節(jié)我們來談?wù)?Windows NT 下 RPC 的高性能模式 - LPC。 很多 Windows 編程入門的書里面講 Windows 的進(jìn)程間通信,都會(huì)講 WM_COPYDATA,講匿名管道,講命名管道,講共享內(nèi)存等等, 但是很少有講 RPC 的,為什么呢?因?yàn)?RPC 看名字,就叫“Remote Procedure Call”,一看就是給分布式系統(tǒng)通信用的,雖然 也可以作為本機(jī)進(jìn)程間通信用,但是性能上總是讓人懷疑。所以很多人設(shè)計(jì)的進(jìn)程間通信模型,都是用 WM_COPYDATA,或者管道, 或者干脆共享內(nèi)存,相當(dāng)于自己造輪子,一切從頭做起。但 RPC 確實(shí)好用啊,調(diào)用起來就像調(diào)用庫(kù)函數(shù)一樣,通信的細(xì)節(jié)全給你 封裝起來了。那 RPC 有沒有性能好一點(diǎn)的模式呢?這就是下面要講的 LPC 模式了。 LPC(Local Procedure Call)是 Windows NT 內(nèi)部的高性能的通信模式。它是在內(nèi)核中實(shí)現(xiàn)的,主要用于 Win32 子系統(tǒng)內(nèi)部的 通信,比如 csrss, lsass 都大量的用到了 LPC。在前面演示的代碼中,只需要改一行代碼,我們就可使用 LPC 了,其實(shí) RPC 就 是內(nèi)部使用 LPC 來進(jìn)行通信,性能大大提高。 服務(wù)端代碼: server.c -------------- // 用LPC 方式通信 RpcServerUseProtseqEp( (unsigned char *)"ncalrpc", RPC_C_PROTSEQ_MAX_REQS_DEFAULT, (unsigned char *)"AppName", NULL); 客戶端代碼: client.c -------------- // 用LPC 方式通信 // 第3 個(gè)參數(shù)NetworkAddr 只能取NULL RpcStringBindingCompose( NULL, (unsigned char*)"ncalrpc", NULL, (unsigned char*)"AppName", NULL, &pszStringBinding ); |
|
|