基于MIDL的Windows RPC实现
1 概述
RPC的全称Remote Procedure Call 是一种基于进程间(可以跨域)通信,客户端远程调用服务端的一种机制,客户端和服务端可以是不同的系统平台,也可以是无关的应用程序,设计上实现了应用程序的解耦。RPC的通信机制对上层透明,应用客户端只需要关注RPC接口的入参和返回结果即可,无需过多关注RPC接口内部实现原理。RPC接口调用流程见下图:
从上图可知,客户端调用Func()后,由RPC接口库实现将请求发送到服务端,服务端执行Func()的函数实现,并将运行结果返回给客户端。从而可知RPC的实现应该基于以下核心技术:
- 参数的封包
- 进程间通信
- 参数的解包
MIDL是微软的接口定义语言,开发人员能够创建远程过程调用(RPC)接口和COM/DCOM接口所需的接口定义语言(IDL)文件和应用程序配置文件(ACF)。Windows中包含适用于MIDL的适当运行时库,MIDL编译器和RPC开发环境的组件在安装Windows SDK时安装。微软已经给我们提供了SDK,并且也在Windows 2000及以后操作系统都支持该机制,所以在win平台,我们只需要遵循微软提供的实现,无需引入第三方的RPC库就可以实现RPC的机制。
2 MIDL和RPC的优点
从章节1可知,微软已经提供了RPC的基础框架,开发者只需要根据微软提供的SDK,定义自己的接口和远程过程处理过程即可,这里对比其他RPC库的功能,列举一下使用MIDL+RPC的优点:
- Win 2000及以后系统都支持该机制,无需引入第三方实现库
- 开发者定义好接口,MIDL自动解析并生成客户端和服务端stub源码
- 进程间通信协议(PIP、TCP、UDP)可参数配置,无需自实现服务端和客户端通信代码
- 无需关心参数的封包和解包
综上所述:Win平台实现RPC,没有理由不优先选择使用该实现方案。
3 MIDL实现RPC的步骤
3.1 关键函数依赖
3.1.1 RPC客户端调用
RpcStringBindingCompose(
TCHAR* ObjUuid, //要绑定的UUID的值
TCHAR* ProtSeq, //***打包使用的协议
TCHAR* NetworkAddr, //服务端的网络地址,值的形式与使用的ProtSeq协议类型对应
TCHAR* EndPoint, //端点格式和内容与协议序列相关联
TCHAR* Options, //指向网络选项的以空字符结尾的字符串表示形式。
TCHAR** StringBinding)//返回绑定字符串UUID
RpcBindingFromStringBinding(
unsigned char* StringBinding, //RpcStringBindingCompose返回的绑定字符
RPC_BINDING_HANDLE* Binding) //返回一个指向服务器绑定句柄的指针
其中Binding是ACF文件中定义的关联句柄。
3.1.2 RPC服务端调用
//告知RPC运行时库,使用特定的序列化标识和服务地址用于远程调用
RpcServerUseProtseqEp(
unsigned char* Protseq, //指向要在RPC运行时库中注册的协议序列的字符串标识符。应与客户端的使用标识
unsigned int MaxCalls, //ncacn_ip_tcp协议序列的积压队列长度
unsigned char* Endpoint, //用于为Protseq参数中指定的协议序列创建绑定
void* SecurityDescriptor) //指向为安全子系统提供的可选参数的指针
//用于向系统注册一个RPC Server
RpcServerRegisterIf(
RPC_IF_HANDLE IfSpec, //MIDL自动产生,用于注册使用。
UUID* MgrTypeUuid, //指向与MgrEpv参数关联的类型UUID的指针
RPC_MGR_EPV* MgrEpv) //要使用MIDL生成的默认EPV
//开始监听客户端的远程调用请求
RpcServerListen(
unsigned int MinimumCallThreads, // 指定Server处理请求的最小服务线程数。各个版本的系统可能对该值的解释不一
unsigned int MaxCalls, //服务器可以执行的并发远程过程调用的最大数目
unsigned int DontWait) //非0表示函数处理后立即返回,值为0表示,
在调用RpcMgmtStopServerListening函数并完成所有远程调用之前,
RpcServerListen不应返回。
//等待关联在RpcServerListen中的请求处理
RpcMgmtWaitServerListen(void)
3.2 一个简单的例子(Hello World!),使用vs2008
-
1 接口定义和数据类型
- 基础数据类型
- 数组Array
- 枚举类型
- 其他
-
2 编写IDL文件
首先产生一个hello.idl文件,打开 开始->所有程序->Microsoft Visual Studio 2008-> Visual Studio Tools –> Visual Studio 2008 命令提示,输入:
uuidgen /i /ohello.idl //注意/o和hello.idl之前没有空格
生成的hello.idl文件内容如下:
增加两个函数接口,修改后的文件如下:
OK,到此我们的idl文件已经编码结束了。
-
3 编写ACF文件
如果没有此文件,所有idl中的接口将自动添加[in]handle_t IDL_handle参数,该参数由RpcBindingFromStringBinding函数调用生成,当我们定义了ACF文件后,MIDL将自动为我们关联到acf文件中定义的句柄参数。
-
4 产生存根文件
输入 midl hello.idl 命令后,midl将为我们自动生成hello.h、hello_c.c(客户端使用)和hello_s.c(服务端使用)三个文件
到这里,我们已经完成了RPC编写的基础部分(接口的定义和序列化),可以将以上自动生成的部分集成到客户端和服务端的程序当中。
还需要实现注册RPC服务端和远程调用函数的实现。
-
5 编写实现代码
客户端代码实现:
int _tmain(int argc, _TCHAR* argv[]) { RPC_STATUS status; unsigned char *pszString = (unsigned char *)"Hello World!\0"; unsigned char *pszBindStr = NULL; status = RpcStringBindingCompose(NULL, (unsigned char *)PROTOCOL_SEQUENCE, NULL, (unsigned char *)END_POINT, NULL, &pszBindStr); if(status) { exit(GetLastError()); } status = RpcBindingFromStringBinding(pszBindStr, &hello_IfHandle); if(status) { exit(GetLastError()); } RpcTryExcept { HelloProc(pszString); // 远程调用 Shutdown(); // 服务端关闭 } RpcExcept(1) { printf("远程调用发生异常,异常错误码:%ld", RpcExceptionCode()); } RpcEndExcept //执行远程内存释放 status = RpcStringFree(&pszBindStr); // remote calls done; unbind if (status) { exit(status); } //执行unbind status = RpcBindingFree(&hello_IfHandle); // remote calls done; unbind if (status) { exit(status); } return 0; } /*********************************************************************/ /* MIDL allocate and free */ /*********************************************************************/ 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); }
从上面的代码可以看出,客户端的代码很简单,只需要实现RPC的绑定及接口调用即可。
服务端代码:
#include "stdafx.h" #include "hello.h" void HelloProc(unsigned char * pszString) { printf("%s\n", pszString); } void Shutdown(void) { RPC_STATUS status; status = RpcMgmtStopServerListening(NULL); if (status) { exit(status); } status = RpcServerUnregisterIf(NULL, NULL, FALSE); if (status) { exit(status); } } int _tmain(int argc, _TCHAR* argv[]) { RPC_STATUS status; unsigned int cMinCalls = 1; unsigned int cMaxCalls = 20; status = RpcServerUseProtseqEp((unsigned char *)PROTOCOL_SEQUENCE, 20, (unsigned char *)END_POINT, NULL); // Security descriptor if(status) { exit(GetLastError()); } status = RpcServerRegisterIf(hello_v1_0_s_ifspec, NULL, NULL); if(status) { exit(GetLastError()); } status = RpcServerListen(1, 20, FALSE); if(status) { exit(GetLastError()); } getchar(); return 0; } /*********************************************************************/ /* MIDL allocate and free */ /*********************************************************************/ 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); }
从上面的代码可以看到,服务端实现了HelloProc(unsigned char * pszString)和void Shutdown(void)函数。main函数中实现了告知系统RPC使用的协议,并注册绑定RPC服务端,监听客户端的请求。
编译错误
1 > 没有找到midl_user_allocate和midl_user_free函数实现,常见客户端和服务端代码中,由于midl自动代码,内存分配和释放的功能交由应用程序完成,方便应用程序实现统一的内存管理。
2> 没有找到idl我们定义的接口实现,该错误常见于RPC服务端编译时,因为idl只帮我们生成接口的序列化和反序列化,并没有帮我们生成实现,所以服务端需要自实现接口。客户端接口调用端,midl已经帮我们生成好了远程调用接口,详细见hello_c.c代码。