65.9K
CodeProject 正在变化。 阅读更多。
Home

RPC 入门 - 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (111投票s)

2003年8月23日

CPOL

6分钟阅读

viewsIcon

1183619

downloadIcon

21962

RPC编程入门。介绍了一个简单的RPC客户端/服务器应用程序。

Hello RPC World!

目录

引言

我从事客户端-服务器应用程序已有几年时间了,所有这些应用程序都使用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) 的接口,名为Example1Example1接口定义了一个名为Output的函数,该函数接受一个名为szOutputconst char*参数作为输入(in),该参数以零结尾(string)。

接口的implicit_handle属性稍后会讨论,暂时先不管它。

下一步?

为了在我们的应用程序中使用IDL,我们需要运行它通过一个编译器(midl.exe),它将把IDL翻译成C语言的客户端代理和服务器存根。代理/存根稍后将使用您喜欢的编译器(在我的例子中是cl.exe)进行编译。

How files are generated with midl.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客户端/服务器应用程序。

Putting it all together

附录

本节介绍了一些编写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_handleexplicit_handle,但这属于Microsoft的扩展。通常需要使用单独的应用程序配置文件来包含这些。zip文件中的示例代码确实使用了单独的ACF文件,但我认为在文章中写出来只会让您更加困惑。

不要乱动生成的代码

您不应该为了使生成的代码能够编译而对其进行修改,它们是(应该是)正确的。如果您觉得midl.exe的开关不正确,请检查它。在编译它们时,您可能会收到很多警告,但当将警告级别降低到2时,它们就会保持沉默。

关闭服务器

示例服务器将一直运行,直到通过某种方式关闭它。这不是最好的方法,另一种更好的方法是调用RpcMgmtStopServerListening函数。但是您该如何调用它呢?您可以在接口中添加另一个函数(也许命名为Shutdown?)来调用RpcMgmtStopServerListening,或者您可以在调用RpcServerListen之前在服务器中创建一个新线程,该线程将在大约一分钟后调用RpcMgmtStopServerListening。更多关于这方面的内容将在另一篇文章中介绍。

结论

这只是通往RPC和客户端/服务器应用程序世界的大门。如果您继续阅读我关于此事的其他(未来的)文章,您将完全准备好应对眼前的世界。

这是我写的第一篇CodeProject文章,希望您读得和我写得一样开心。

参考文献

修订历史

  • 2012-12-22
    • 终于更新为与Visual Studio 2010兼容,并修复了安全回调
  • 2003-08-25
    • 更新了演示和源代码
    • 从命名管道改为TCP/IP(Hector Santos的建议)
  • 2003-08-23
    • 原文
© . All rights reserved.