欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于MIDL的Windows RPC实现

程序员文章站 2022-06-26 13:27:31
...

1 概述

  RPC的全称Remote Procedure Call 是一种基于进程间(可以跨域)通信,客户端远程调用服务端的一种机制,客户端和服务端可以是不同的系统平台,也可以是无关的应用程序,设计上实现了应用程序的解耦。RPC的通信机制对上层透明,应用客户端只需要关注RPC接口的入参和返回结果即可,无需过多关注RPC接口内部实现原理。RPC接口调用流程见下图:

基于MIDL的Windows 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

        - 枚举类型

        - 数据结构struct

        - 其他 

  • 2 编写IDL文件

首先产生一个hello.idl文件,打开 开始->所有程序->Microsoft Visual Studio 2008-> Visual Studio Tools –> Visual Studio 2008 命令提示,输入:

uuidgen /i /ohello.idl  //注意/o和hello.idl之前没有空格

生成的hello.idl文件内容如下:

基于MIDL的Windows RPC实现

增加两个函数接口,修改后的文件如下:

基于MIDL的Windows RPC实现

OK,到此我们的idl文件已经编码结束了。

  • 3 编写ACF文件

基于MIDL的Windows RPC实现

如果没有此文件,所有idl中的接口将自动添加[in]handle_t IDL_handle参数,该参数由RpcBindingFromStringBinding函数调用生成,当我们定义了ACF文件后,MIDL将自动为我们关联到acf文件中定义的句柄参数。

  • 4 产生存根文件

输入 midl hello.idl 命令后,midl将为我们自动生成hello.h、hello_c.c(客户端使用)和hello_s.c(服务端使用)三个文件

基于MIDL的Windows RPC实现

到这里,我们已经完成了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的Windows RPC实现 没有找到midl_user_allocate和midl_user_free函数实现,常见客户端和服务端代码中,由于midl自动代码,内存分配和释放的功能交由应用程序完成,方便应用程序实现统一的内存管理。

2> 基于MIDL的Windows RPC实现没有找到idl我们定义的接口实现,该错误常见于RPC服务端编译时,因为idl只帮我们生成接口的序列化和反序列化,并没有帮我们生成实现,所以服务端需要自实现接口。客户端接口调用端,midl已经帮我们生成好了远程调用接口,详细见hello_c.c代码。

3>基于MIDL的Windows RPC实现 找不到RPC内部的实现库, 需要在link library中添加Rpcrt4.lib静态库。 

4 参考资料