RPC 入门 - 第 1 部分






4.85/5 (111投票s)
RPC编程入门。介绍了一个简单的RPC客户端/服务器应用程序。
目录
引言
我从事客户端-服务器应用程序已有几年时间了,所有这些应用程序都使用RPC作为客户端和服务器之间的通信层。我发现CodeProject上关于此事的真正文章很少,这让我感到奇怪,所以我决定写一篇自己的文章来传播我的知识。
另一方面,这件事有点大,所以我将把它分成几个难度不同的文章。这第一篇是入门级的。
IDL、RPC和你
为了使用RPC,你需要对IDL有所了解,但别担心,这篇文章会帮助你。
接口定义语言 (IDL)
IDL是Interface Definition Language(接口定义语言)的缩写,它是一种用于定义接口的语言(没错)。编写IDL文件有点像编写一个C头文件,但包含一些额外的关键字和结构。IDL是一种属性编程语言,因此它比C能够更详细地描述参数、函数和接口。
远程过程调用 (RPC)
远程过程调用 (RPC) 定义了一种强大的技术,用于创建分布式的客户端/服务器程序。RPC运行时库管理着大部分与网络协议和通信相关的细节。这使得你可以专注于应用程序的细节,而不是网络的细节。
使用RPC,客户端可以连接到运行在另一平台上的服务器。例如:理论上,服务器可以用Linux编写,客户端可以用Win32编写。现实情况会更复杂一些。
你害怕了吗?
如果你不怕,我认为是时候举个例子了。
独立应用程序
这个独立的应用程序将不使用RPC,它是一个简单的HelloWorld
应用程序,我们稍后将将其转换为RPC客户端/服务器应用程序。
孤独的你好!
// File Standalone.cpp
#include <iostream>
// Future server function.
void Output(const char* szOutput)
{
std::cout << szOutput << std::endl;
}
int main()
{
// Future client call.
Output("Hello Lonely World!");
}
我不认为有人会对上述应用程序有什么异议。它将字符串"Hello Lonely World!"
输出到标准输出。
IDL文件
是时候在IDL中定义我们的接口了。
Hello IDL World!
// File Example1.idl
[
// A unique identifier that distinguishes this
// interface from other interfaces.
uuid(00000001-EAF3-4A7A-A0F2-BCE4C30DA77E),
// This is version 1.0 of this interface.
version(1.0),
// This interface will use an implicit binding
// handle named hExample1Binding.
implicit_handle(handle_t hExample1Binding)
]
interface Example1 // The interface is named Example1
{
// A function that takes a zero-terminated string.
void Output(
[in, string] const char* szOutput);
}
这里,您可以看到IDL使用的属性编程。上面的例子定义了一个具有通用唯一标识符 (uuid
) 和版本 (version
) 的接口,名为Example1
。Example1
接口定义了一个名为Output
的函数,该函数接受一个名为szOutput
的const char*
参数作为输入(in
),该参数以零结尾(string
)。
接口的implicit_handle
属性稍后会讨论,暂时先不管它。
下一步?
为了在我们的应用程序中使用IDL,我们需要运行它通过一个编译器(midl.exe),它将把IDL翻译成C语言的客户端代理和服务器存根。代理/存根稍后将使用您喜欢的编译器(在我的例子中是cl.exe)进行编译。
我该如何为你服务?
是时候使用生成的代码来构建我们的服务器应用程序了。
Hello Server World!
// File Example1Server.cpp
#include <iostream>
#include "Example1.h"
// Server function.
void Output(const char* szOutput)
{
std::cout << szOutput << std::endl;
}
// Naive security callback.
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE /*hInterface*/, void* /*pBindingHandle*/)
{
return RPC_S_OK; // Always allow anyone.
}
int main()
{
RPC_STATUS status;
// Uses the protocol combined with the endpoint for receiving
// remote procedure calls.
status = RpcServerUseProtseqEp(
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"), // Use TCP/IP protocol.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Backlog queue length for TCP/IP.
reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
NULL); // No security.
if (status)
exit(status);
// Registers the Example1 interface.
status = RpcServerRegisterIf2(
Example1_v1_0_s_ifspec, // Interface to register.
NULL, // Use the MIDL generated entry-point vector.
NULL, // Use the MIDL generated entry-point vector.
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // Forces use of security callback.
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls.
(unsigned)-1, // Infinite max size of incoming data blocks.
SecurityCallback); // Naive security callback.
if (status)
exit(status);
// Start to listen for remote procedure
// calls for all registered interfaces.
// This call will not return until
// RpcMgmtStopServerListening is called.
status = RpcServerListen(
1, // Recommended minimum number of threads.
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Recommended maximum number of threads.
FALSE); // Start listening now.
if (status)
exit(status);
}
// Memory allocation function for RPC.
// The runtime uses these two functions for allocating/deallocating
// enough memory to pass the string to the server.
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}
// Memory deallocation function for RPC.
void __RPC_USER midl_user_free(void* p)
{
free(p);
}
这与独立应用程序有点不同,但差别不大。它包含一些用于注册接口的初始化代码,但Output
函数保持不变。
让我成为你的客户
是时候编写我们的客户端应用程序了,它将连接到服务器。
Hello Client World!
// File Example1Client.cpp
#include <iostream>
#include "Example1.h"
int main()
{
RPC_STATUS status;
unsigned char* szStringBinding = NULL;
// Creates a string binding handle.
// This function is nothing more than a printf.
// Connection is not done here.
status = RpcStringBindingCompose(
NULL, // UUID to bind to.
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"),// Use TCP/IP protocol.
reinterpret_cast<unsigned char*>("localhost"), // TCP/IP network address to use.
reinterpret_cast<unsigned char*>("4747"), // TCP/IP port to use.
NULL, // Protocol dependent network options to use.
&szStringBinding); // String binding output.
if (status)
exit(status);
// Validates the format of the string binding handle and converts
// it to a binding handle.
// Connection is not done here either.
status = RpcBindingFromStringBinding(
szStringBinding, // The string binding to validate.
&hExample1Binding); // Put the result in the implicit binding
// handle defined in the IDL file.
if (status)
exit(status);
RpcTryExcept
{
// Calls the RPC function. The hExample1Binding binding handle
// is used implicitly.
// Connection is done here.
Output("Hello RPC World!");
}
RpcExcept(1)
{
std::cerr << "Runtime reported exception " << RpcExceptionCode()
<< std::endl;
}
RpcEndExcept
// Free the memory allocated by a string.
status = RpcStringFree(
&szStringBinding); // String to be freed.
if (status)
exit(status);
// Releases binding handle resources and disconnects from the server.
status = RpcBindingFree(
&hExample1Binding); // Frees the implicit binding handle defined in the IDL file.
if (status)
exit(status);
}
// Memory allocation function for RPC.
// The runtime uses these two functions for allocating/deallocating
// enough memory to pass the string to the server.
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}
// Memory deallocation function for RPC.
void __RPC_USER midl_user_free(void* p)
{
free(p);
}
除了连接到服务器和一些清理工作外,实际的调用是相同的,只是我改变了要输出的字符串。
整合
首先,我们需要编译IDL文件以获得客户端代理、服务器存根和通用头文件。代理和存根被编译,客户端和服务器实现也一样。我们将这两个应用程序链接起来并运行它们。如果一切正常,我们就可以非常高兴了,因为我们已经完成了第一个RPC客户端/服务器应用程序。
附录
本节介绍了一些编写RPC应用程序的有用技术。
调试与RPC
如果在调试过程中遇到问题,并且问题似乎出在MIDL生成的文件中,那么真正的问题很可能出在客户端或服务器端。我有时会遇到指针问题,但在后续文章中,我将更详细地描述这些问题。
隐式和显式句柄
在使用RPC时,绑定句柄可以是隐式的(如本文示例中的那样)或显式的。我总是使用显式句柄,因为我有时会连接到多个服务器,而隐式句柄无法做到这一点。要使用显式句柄,您必须更改IDL文件、服务器和客户端。
// File Example1Explicit.idl
[
// A unique identifier that distinguishes this
// interface from other interfaces.
uuid(00000002-EAF3-4A7A-A0F2-BCE4C30DA77E),
// This is version 1.0 of this interface.
version(1.0),
// This interface will use explicit binding handle.
explicit_handle
]
interface Example1Explicit // The interface is named Example1Explicit
{
// A function that takes a binding handle and a zero-terminated string.
void Output(
[in] handle_t hBinding,
[in, string] const char* szOutput);
}
// File Example1ExplicitServer.cpp
#include <iostream>
#include "Example1Explicit.h"
// Server function.
void Output(handle_t hBinding, const char* szOutput)
{
std::cout << szOutput << std::endl;
}
// main - same as before.
// File Example1ExplicitClient.cpp
#include "Example1Explicit.h"
int main()
{
// Call to RpcStringBindingCompose - same as before.
handle_t hExample1ExplicitBinding = NULL;
// Validates the format of the string binding handle and converts
// it to a binding handle.
// Connection is not done here either.
status = RpcBindingFromStringBinding(
szStringBinding, // The string binding to validate.
&hExample1ExplicitBinding); // Put the result in the explicit binding handle.
if (status)
exit(status);
RpcTryExcept
{
// Calls the RPC function. The hExample1ExplicitBinding binding handle
// is used explicitly.
// Connection is done here.
Output(hExample1ExplicitBinding, "Hello RPC World!");
}
RpcExcept(1)
{
std::cerr << "Runtime reported exception " << RpcExceptionCode()
<< std::endl;
}
RpcEndExcept
// Call to RpcStringFree - same as before.
// Releases binding handle resources and disconnects from the server.
status = RpcBindingFree(
&hExample1ExplicitBinding); // Frees the binding handle.
if (status)
exit(status);
}
还有一种叫做auto_handle
的东西,但我从来没用过。它似乎会自动处理与服务器的连接。
应用程序配置文件 (ACF)
在本例中,我在IDL文件中直接使用了implicit_handle
和explicit_handle
,但这属于Microsoft的扩展。通常需要使用单独的应用程序配置文件来包含这些。zip文件中的示例代码确实使用了单独的ACF文件,但我认为在文章中写出来只会让您更加困惑。
不要乱动生成的代码
您不应该为了使生成的代码能够编译而对其进行修改,它们是(应该是)正确的。如果您觉得midl.exe的开关不正确,请检查它。在编译它们时,您可能会收到很多警告,但当将警告级别降低到2时,它们就会保持沉默。
关闭服务器
示例服务器将一直运行,直到通过某种方式关闭它。这不是最好的方法,另一种更好的方法是调用RpcMgmtStopServerListening
函数。但是您该如何调用它呢?您可以在接口中添加另一个函数(也许命名为Shutdown
?)来调用RpcMgmtStopServerListening
,或者您可以在调用RpcServerListen
之前在服务器中创建一个新线程,该线程将在大约一分钟后调用RpcMgmtStopServerListening
。更多关于这方面的内容将在另一篇文章中介绍。
结论
这只是通往RPC和客户端/服务器应用程序世界的大门。如果您继续阅读我关于此事的其他(未来的)文章,您将完全准备好应对眼前的世界。
这是我写的第一篇CodeProject文章,希望您读得和我写得一样开心。
参考文献
- MSDN - Microsoft Interface Definition Language [^]
- MSDN - MIDL and RPC [^]
- MSDN - Remote Procedure Call [^]
- The OSF Distributed Computing Environment [^]
修订历史
- 2012-12-22
- 终于更新为与Visual Studio 2010兼容,并修复了安全回调
- 2003-08-25
- 更新了演示和源代码
- 从命名管道改为TCP/IP(Hector Santos的建议)
- 2003-08-23
- 原文