本文比较详细地介绍了远程过程调用(RPC)的OSF标准在Microsoft VC++中的实现原理, 以及如何使用它们来开发应用程序. 阅读本文你将了解RPC的基本原理, 并将看到如何开发使用RPC进行异种机网络分布式处理的客户机应用程序和服务器应用程序.
Para 1. RPC工作原理
RPC是把传统本地过程调用的概念加以扩充后引入分布式环境的一种形式. RPC的形式和行为与传统本地过程调用极为相似, 差别仅在于被调用的procedure(过程)实际运行在与调用者的场点不同的场点上(如图1). 也正是由于这一差别, 我们得通过编写程序来实现两场地之间的连接和信息沟通.
RPC机制的实质是实现OSI七层模型中的会话层功能. 它在两个试图进行通信的场点之间建立一条逻辑信道(即会话连接), 并利用这个信道交换信息, 不用时就释放连接. 下面我们就来看看RPC的通信模型(如下图2):
Client端:
1) 发送远程过程调用的消息(以消息包形式)给远程的server端;
2) 等待, 直到收到server端对该请求的回复;
3) 一旦接收到来自server端的返回执行结果, 就继续执行后面的程序.
Server端:
1) 倾听状态, 等待client端发送过程调用消息;
2) 一旦接收到过程调用消息, server就抽取参数并分析它, 然后执行所请求的过程;
3) 将执行结果以消息包形式回送给client.
至于RPC的具体实现, 我们可以借助下图3来理解:
其中stub是一组RPC机制的操作原语, 这些原语构成了RPC的实现细节, 它可以独立于client、server编程. 下面我们来解说图3的执行过程:
1) 调用者调用本地stub中的一个过程(开始远程过程调用请求).
2) 这个stub过程把有关的参数组装成一个消息包或一组消息包, 形成一条消息. 运行此执行过程的远程场点的IP地址和执行该过程的进程ID号也包含在这条消息中.
3) 将这条消息发送给对应的RPC runtime(RPC运行库)子程序, 由这个子程序将消息发送到远程场点.
4) 在接收到这条消息时, server端的RPC runtime子程序引用与被调用者对应的stub中的一个子程序, 并让它来处理消息.
5) 与被调用者对应的stub中的这个子程序撤卸消息, 解析出相关参数, 并用本地调用方式执行所指定的过程.
6) 返回调用结果, 调用者对应的stub子程序执行return语句返回到用户, 整个RPC过程结束.
实际上, 从上面这个执行过程中, 我们可以看到RPC的实现主要有两个问题需要解决. 一个是在远程过程调用时, 如何定位远程场点; 另外一个就是相关的两个场点必须能协同工作, 所有这些工作对用户都是透明的, 依次执行.
通常在实际编程中, 程序设计者主要负责设计计算过程并实现计算过程体, 而对应的stub由系统生成. 后面我们就要说到Microsoft的RPC实现机制,看看它是如何产生stub的。
Para2. 如何设计好的RPC
对调用双方来说, 传递RPC参数包括辅助处理本地数据表示和网络数据表示的相互转换. 此外, 输入输出参数需要一些存储分配. 同时, RPC中的等待时间也不能忽略.
所以, 一般来说, 应该尽可能降低调用次数. 例如,如果要对一个大数组的每个元素都执行计算, 我们就可以一次调用处理一整行或者整个数组, 而不用每次调用传递一个元素. 这样可以降低有RPC引入的额外开销.
Para3. 应用程序的组件
为了在client和server端使用RPC, 当然少不了client进程和server进程.
开发过程大致是这样的:
1) 任何RPC调用都使用一个定义在IDL(interface definition language, 接口定义语言)文件中的接口, 然后MIDL(Microsoft IDL)编译器对IDL文件进行编译, 编译之后会自动生成一个.h文件, 同时生成一个 client stub 和一个server stub. 关于这个你可以在dos下运行midl.exe/?得到更详细的信息.
2) Client端应用程序使用client stub调用RPC runtime以实现网络上的调用. 接下来RPC运行时使用一组DLL中的一个来实现被使用的特定网络协议.
3) Server端也与RPC runtime连接. 不过server端应用程序使用一些其它的函数来将自己作为一个特殊接口的服务器进行注册(向谁注册?), 并开始侦听接口的请求.
Para4. 一个计算阶层的RPC实例开发过程
Microsoft提供了分布式计算的模型支持. RPC标准最初是作为OSF(开放软件基础)﹑DCE(分布式计算环境)规范的一部分. RPC的Microsoft实现与其它的RPC的DCE实现兼容, 例如Unix服务器. 本节将介绍RPC的Microsoft实现. 文中涉及的一些概念如IDL(interface definition language)等请朋友们自己查找资料理解.
本程序由以下几个文件组成:
RpcFact.idl
RpcFact.acf
RpcFact.h
RpcFact_c.c
RpcFact_s.c
RpcFact.c
memstub.h
下面结合实现原理一一道来:
1. 定义接口 (得到RpcFact.idl文件)
创建一个.idl文件, 为应用程序的远地函数定义一个接口.
// File: RpcFact.idl
// interface header
[
uuid (C16F6562-520D-11D0-B338-444553540000), // universally unique identifier,唯一识别这个接// 口, 它可以被服务器用来注册接口, 以使客户机可以定位这个特殊接口. uuid由5位数字组
// 成,你可以用Microsoft的uuidgen.exe来得到它.
version (1.0), // 指定版本号, 那么在同一个网络上就可以同时拥有一个RPC接口的不同版本
endpoint(“ncalrpc: [myFactorial]”) // 接口的端点, 它给出了将会使用的网络协议的类型和用来接
// 收接口的请求的地址和端口, ncalrpc 表示Local procedure call
]
// interface text, 指定了组成这个接口的函数, 函数类型不支持int 和void*
interface rpcfactorial
{
long RpcFactorial([in] long nVal); // 出现[ ]的部分表示属性
}
// end File RpcFact.idl
2. 应用程序属性配置文件 (得到RpcFact.acf文件)
.acf文件用来指定应用程序的选项, 这些属性告诉client stub如何对接口请求的捆绑进行处理, 这个问题稍后介绍.
// RpcFact.acf
[
auto_handle // 相关属性
]
interface rpcfactorial
{
}
// end File RpcFact.acf
3. 用MIDL编译器编译RpcFact.idl文件
第一种方式, midl.exe /c_text /ms_text /app_config RpcFact.idl (详细了解请在dos下运行midl.exe /?获取).
第二种方式, 先将RpcFact.idl插入你的Project中, 在Projects—Settings中选取左边框内的RpcFact.idl文件, 选择右边的Custom Build页面, 位IDL文件指定一个用户定制的编译器. 可以键入如下命令:
midl.exe /c_text /ms_text /app_config $ (InputPath)
经过编译之后, 会生成这几个文件: RpcFact.h、RpcFact_s.c 和RpcFact_c.c
4. 开发RPC服务器
定义好了一个接口之后, 就需要创建一个响应接口请求的进程, 执行请求的操作并返回所有结果给client. 这些任务就由RPC服务器来完成.
(1) 执行计算的过程
本例中就是阶层函数的实现, 我把它单独放在一个RpcFact.c文件中.
// File: RpcFact.c
#include <Windows.h>
#include “RpcFact.h” // 用MIDL编译器生成
long Factorial (long nVal)
{
long nResult = 1;
for (; nVal>0; nVal--)
nResult *= nVal;
return nResult;
}
// end File RpcFact.c
(2) RPC服务器实体
使RpcFact.c真正对RPC有用的工作是在服务器实体(RpcFact_s.c)中实现的. 实体内的代码负责建立RPC接口的捆绑, 并用名称服务程序来注册它们及侦听RPC请求.
// File: RpcFact_s.c
#include <Windows.h>
#include <iostream.h>
#include <rpc.h>
#include “RpcFact.h”
#include “memstub.h” // 实现RPC函数的存储分配, 稍后介绍
main()
{
RPC_BINDING_VECTOR* bindVector;
RPC_STATUS lRetVal;
// 注册所有可行的协议序列, 所支持的协议由在RpcFact.idl的端点定义中的字符串指定
// 参数rpcfactorial_v1_0_s_ifspec定义在RpcFact.h中, 由MIDL编译生成
if (lRetVal = RpcServerUseAllProtseqsIf(1, rpcfactorial_v1_0_s_ifspec, NULL))
{
cout<<”Error in RpcServerUseAllProtseqsIf”<<lRetVal<<endl;
return 1;
}
// 注册所支持的接口
if (RpcServerReGISterIf(rpcfactorial_v1_0_s_ifspec, NULL, NULL))
{
cout<<”Error in RpcServerRegister”<<endl;
return 1;
}
// 将接口的捆绑输出到名称服务程序
if (RpcServerInqBindings(&bindVector))
{
cout<<”Error in RpcServerInqBindings”<<endl;
return 1;
}
if (RpcServerBindingExport(RPC_C_NS_SYNTAX_DEFAULT, (UCHAR*)”/.: /autorpc”, rpcfactorial_v1_0_s_ifspec, bindVector, NULL))
{
cout<<”Error in RpcServerBidingExport”<<endl;
return 1;
}
// 侦听RPC请求
cout<<”Calling RpcServerListen”<<endl;
if (RpcServerListen(1, 5, FALSE))
{
cout<<”Error in RpcServerListen”<<endl;
return 1;
}
return 0;
}// end main
// end File RpcFact_s.c
// File: memstub.h
// 以下两个函数在RpcFact.h中已经声明
void __RPC_FAR * __RPC_USER MIDL_user_allocate(size_t len)
{
return (new (unsigned char [len]));
}
void __RPC_USER MIDL_user_free( void __RPC_FAR* ptr)
{
delete ptr;
}
// end File memstub.h
(3) 建立服务器
建立服务器时, 你需要确保连接server stub函数(即RpcFact_s.c)编译后的.obj文件. 此外还得添加两个RPC运行库(rpcns4.lib和rpcrt4.lib).
5. 开发RPC客户机
// File: RpcFact_c.c
#include <windows.h>
#include <iostream.h>
#include <rpc.h>
#include <stdlib.h>
#include “RpcFact.h”
#include “memstub.h”
main()
{
long nVal;
cout<<”Calling RPC Factorial”<<endl;
nVal = RpcFactorial(5);
cout<<”RpcFact returns: “<<nVal<<endl;
return 0;
}
// end File RpcFact_c.c
至此, 一个简单的RPC调用示例已经完成, 文中没讲清楚的地方请参看[1][2][3]列出的书籍, 或者你也可以与我联系.
本文的后续篇将继续介绍RPC中的参数传递﹑捆绑到RPC服务器的几种方式以及RPC的异常处理.
参考书籍:
[1] David Bennett等著. <<Visual C++ 5开发人员指南>>. 机械工业出版社
[2] <<分布式操作系统>>
[3] MSDN 6.0