Windows RPC Demo實現


Windows RPC Demo實現

  本文參考並整理以下相關文章

  1. 《遠程過程調用》 -百度百科

  2. 《RPC 編程》 -http://www.ibm.com/developerworks/cn/aix/library/au-rpc_programming/

  3. 《微軟官方參考教程》 -http://blog.csdn.net/swanabin/article/details/18766209

1 概念

  RPC:全稱是“遠程過程調用協議(Remote Procedure Call Protocol)”,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。RPC協議假定某些傳輸協議的存在,如TCP、UDP或者命名管道,為通信程序之間攜帶信息數據。在OSI網絡通信模型中,RPC跨越了傳輸層應用層。RPC使得開發包括網絡分布式多程序在內的應用程序更加容易。

2 原理

  RPC采用客戶機/服務器模式。請求程序就是一個客戶機,而服務提供程序就是一個服務器。首先,客戶機調用進程發送一個有進程參數的調用信息到服務進程,然后等待應答信息。在服務器端,進程保持睡眠狀態直到調用信息的到達為止。當一個調用信息到達,服務器獲得進程參數,計算結果,發送答復信息,然后等待下一個調用信息,最后,客戶端調用進程接收答復信息,獲得進程結果,然后調用執行繼續進行[1]。

不同的廠商實現了不同的RPC協議,顯然我們此次用的是微軟提供的。

3 實現

  RPC的接口標准使用了IDL(Interface Description Language接口描述語言)語言標准描述,熟悉COM接口的用戶應該一眼就能看出,因為它們的接口風格非常相似。相應微軟的編譯器是MIDL,通過IDL文件來定義RPC客戶端與服務器之間的通信接口,只有通過這些接口客戶端才能訪問服務器。

下面我們就通過一個Demo來具體解釋一下RCP編程的具體過程。

  開發環境:Windows 7 SP1旗艦版

                    Visual Studio 2005

  首先我們需要建立一個工程,我們選擇Win32 Console程序RPCServer.exe,然后定義接口文件:IDL文件。

3.1 IDL文件

  在工程中添加一個IDL文件,IDL文件由一個或多個接口定義組成,每一個接口定義都有一個接口頭和一個接口體。接口頭包含了使用此接口的信息,如UUID 。這此信息封裝一對中括號之內。之后是interface關鍵字和接口名。接口體包含了C 風格的數據類型、函數原型和帶屬性的參數。這此參數的屬性描述了數據如何在網絡上傳送。此例中,定義頭中包含了uuid和版本號。版本號作兼容之用。客戶端、服務端的版本只有兼容,才可以連接。

  RPCServer.idl

 1 import "oaidl.idl";
 2 import "ocidl.idl";
 3 import "SelfDefine.h";
 4 
 5 [
 6     uuid( 551d88b0-b831-4283-a1cd-276559e49f28 ),
 7     version( 1.0 )
 8 ]
 9 
10 interface RPCServer
11 {
12     error_status_t GetServerName( [out,size_is(len)]char *pszName,
13                                      [in]long len );
14     error_status_t Add( [in]long num1,
15                           [in]long num2,
16                           [out]long* rtn );
17     error_status_t ShutDown();
18 }

  定義了IDL文件之后,可以直接編譯該IDL文件,但是編譯該文件之后並沒有自動生成相應的服務端和客戶端的C文件。如果要達到自動生成這些C文件的目的,需要配置ACF文件。

3.2 ACF文件

  ACF文件可以使用戶定義自己的客戶端或服務端的RPC接口。例如,如果你的客戶端程序包含了一個復雜的數據結構,此數據結構只在本地機上有意義,那么你就可以在ACF文件中指定如何描述獨立於機器的數據結構,使用數據結構用於遠程過程調用。下面我們在ACF文件中定義一個handle類型,用來代表客戶端與服務端的連接。[implicit_handle]屬性允許客戶端程序為它的遠程過程調用選擇一個服務端。ACF定義了此句柄為handle_t類型(MIDL基本數據類型)。MIDL編譯器將綁定ACF文件指定的句柄名字RPCServer_IfHandle,放在生成的頭文件RPCServer.h中。

  RPCServer.acf

1 [
2     implicit_handle (handle_t RPCServer_IfHandle)
3 ]
4 
5 interface RPCServer
6 {
7 }

  當編譯這些文件RPCServer.idl時,確保RPCServer.idl和RPCServer.acf在同一文件夾中。編譯完成會生成三個文件:RPCServer.h、RPCServer_s.c、RPCServer_c.c,其中RPCServer.h、RPCServer_s.c供服務端使用,RPCServer_h.h、RPCServer_c.c供客戶端使用。

3.3 SelfDefine.h文件

  使用MIDL單獨編譯idl文件沒有問題,但是在vs2005整個工程中編譯時會提示如下所示錯誤:

    fatal error C1189: #error :  You need a Windows 2000 or later to run this stub because it uses these features:

  該錯誤的官方原因現還不太清楚,解決該錯誤的就需要新建SelfDefine.h文件並定義如下宏:

    #undef TARGET_IS_NT50_OR_LATER

    #define TARGET_IS_NT50_OR_LATER 1

  然后在idl文件中導入該頭文件(import "SelfDefine.h";)。

SelfDefine.h

1 #ifndef _SELF_DEFINE_H_
2 #define _SELF_DEFINE_H_
3 
4 #undef TARGET_IS_NT50_OR_LATER
5 #define TARGET_IS_NT50_OR_LATER 1
6 
7 typedef long MyHandle;
8 
9 #endif

  SelfDefine.h文件中還多了一句typedef long MyHandle;,如果不加入類似語句,MIDL編譯器就會提示如下錯誤,真不知道這是什么原因。

    error MIDL2183 : unexpected end of file found : SelfDefine.h

  有了RPC接口文件,我們就可以編寫服務程序,提供接口中定義的服務。

3.4 服務器端

  根據用戶的編碼風格,可以在一個或多個獨立的文件中實現這些遠程過程。Demo中使用RPCServer.cpp源文件編寫主要的服務端代碼。

  服務端調用RPC運行庫函數RpcServerUseProtseqEp和RpcServerRegisterIf來建立有效的服務端。

  服務端必須包含兩個內存分配函數。這兩個函數被服務端程序調用:midl_user_allocate和midl _user_free。這兩個函數在服務端分配和釋放內存,一般情況下midl _user_allocate和midl_user_free都會簡單地封裝C庫函數malloc和free。

  RPCServer.cpp

 1 #include "stdafx.h"
 2 #include <Windows.h>
 3 #include <stdlib.h>
 4 #include <stdio.h>
 5 #include "RPCServer_h.h"
 6 
 7 #pragma comment(lib, "Rpcrt4.lib")
 8 
 9 long StartService();
10 
11 int main( int argc, char* argv[] )
12 {
13     StartService();
14     return 0;
15 }
16 
17 void __RPC_FAR * __RPC_USER midl_user_allocate( size_t len )
18 {
19     return (malloc(len) );
20 }
21 void __RPC_USER midl_user_free( void __RPC_FAR *ptr )
22 {
23     free( ptr );
24 }
25 
26 long StartService()
27 {
28     unsigned char pszProtocolSequence[] = "ncacn_np";
29     unsigned char *pszSecurity = NULL;
30     unsigned char pszEndPoint[] = "\\pipe\\RPCServer"; //命名管道
31 
32     RPC_STATUS rpcStats = RpcServerUseProtseqEp( pszProtocolSequence,
33         RPC_C_LISTEN_MAX_CALLS_DEFAULT,
34         pszEndPoint,
35         pszSecurity );
36     if ( rpcStats )
37         exit( rpcStats );
38 
39     rpcStats = RpcServerRegisterIf( RPCServer_v1_0_s_ifspec, NULL, NULL ); 
40     if ( rpcStats ) 
41         exit( rpcStats );
42 
43     unsigned int fDontWait = false;
44     rpcStats = RpcServerListen( 1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, fDontWait );
45     if ( rpcStats )
46         exit( rpcStats );
47     
48     return 0;
49 }
50 
51 void StopService()
52 {
53     RPC_STATUS rpcStatus;
54     rpcStatus = RpcMgmtStopServerListening( NULL );
55     rpcStatus = RpcServerUnregisterIf( NULL, NULL, FALSE );
56 }
57 
58 error_status_t GetServerName( 
59                              /* [size_is][out] */ unsigned char *pszName,
60                              /* [in] */ long len)
61 {
62     strncpy( (char*)pszName, "RPCServer", len );
63     pszName[len-1] = '\0';
64     printf( "服務已經啟動!\n" );
65     return 0;
66 }
67 
68 error_status_t Add( 
69                    /* [in] */ long num1,
70                    /* [in] */ long num2,
71                    /* [out] */ long *rtn)
72 {
73     *rtn = num1 + num2;
74     return 0;
75 }
76 
77 error_status_t ShutDown( void)
78 {
79     StopService();
80     printf( "服務已經關閉!\n" );
81     return 0;
82 }

3.5 客戶端

  類似服務器,我們也需要新建一個Win32 Console程序-RPCClient.exe,作為客戶端。

  客戶端程序調用運行時函數來建立一個指向服務端的句柄,在遠程過程調用完成后,釋放這個句柄。函數RpcStringBindingCompose()生成一個以字符串表示的綁定句柄(bindinghandle)並為此字符串分配內存。因為服務指定了使用本地命名管道作為網絡通信介質,所以客戶端在建立於服務器的連接時也必須指定相同的管道參數。

  函數RpcBindingFromStringBinding()從字符串表示的綁定句柄,創建了一個服務綁定句柄RPCServer_IfHandle。綁定之后客戶端便可以請求服務器的服務(調用服務器的函數)。

  RPCClient.cpp

 1 #include "stdafx.h"
 2 #include <windows.h>
 3 #include <stdlib.h>
 4 #include <stdio.h>
 5 #include "..\RPCServer\RPCServer_h.h"
 6 #include "..\RPCServer\RPCServer_c.c"
 7 
 8 #pragma comment( lib, "Rpcrt4.lib" )
 9 
10 void CallServerFuntions();
11 
12 int main(int argc, char* argv[])
13 {
14     unsigned char *pszUuid = NULL;
15     unsigned char pszProtocolSequence[] = "ncacn_np";
16     unsigned char *pszNetworkAddress = NULL;
17     unsigned char pszEndpoint[] = "\\pipe\\RPCServer";
18     unsigned char *pszOptions = NULL;
19     unsigned char *pszStringBinding = NULL;
20 
21     RPC_STATUS rpcStatus = RpcStringBindingCompose( pszUuid,
22                                                     pszProtocolSequence,
23                                                     pszNetworkAddress,
24                                                     pszEndpoint,
25                                                     pszOptions,
26                                                     &pszStringBinding );
27     if ( rpcStatus )
28         exit( rpcStatus );
29 
30     rpcStatus = RpcBindingFromStringBinding( pszStringBinding,
31                                              &RPCServer_IfHandle );
32     if ( rpcStatus )
33         exit( rpcStatus );
34 
35     RpcTryExcept
36     {
37         CallServerFuntions();
38     }
39     RpcExcept( 1 )
40     {
41         unsigned long ulCode = RpcExceptionCode();
42         printf( "拋出異常0x%lx = %ld。\n", ulCode, ulCode );
43     }
44     RpcEndExcept
45 
46     rpcStatus = RpcStringFree( &pszStringBinding );
47     if ( rpcStatus )
48         exit( rpcStatus );
49 
50     rpcStatus = RpcBindingFree( &RPCServer_IfHandle );
51     if ( rpcStatus )
52         exit( rpcStatus );
53     return 0;
54 
55     return 0;
56 }
57 
58 void __RPC_FAR * __RPC_USER midl_user_allocate( size_t len )
59 {
60     return (malloc(len) );
61 }
62 void __RPC_USER midl_user_free( void __RPC_FAR *ptr )
63 {
64     free( ptr );
65 }
66 
67 void CallServerFuntions()
68 {
69     unsigned char pszName[64] = {0};
70     GetServerName( pszName, 64 ); //獲取服務名稱
71     printf( "Server Name is: %s\n", pszName );
72     
73     long num1 = 2;
74     long num2 = 2;
75     long nSum = 0;
76     Add( num1, num2, &nSum ); //加法求值c
77     printf( "%d + %d = %d\n", num1, num2, nSum );
78     
79     ShutDown(); //關閉服務
80 }

  具體函數的詳細解釋還需要參考MSDN,參政正確掌握RPC變成技巧。

3.6 運行結果

  客戶端分別調用了服務器的GetServerName()、Add()和Shutdown()函數,獲取了服務器的名稱、計算了兩個數據的和,最后關閉了服務器。小小的Demo完成了RPC通信的整個過程,下圖是Demo運行的效果圖。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM