RPC 简介——第 2 部分






4.90/5 (49投票s)
介绍 RPC 中的上下文句柄。本文解释了一个简单的 RPC 客户端/服务器应用程序,该应用程序使用了上下文句柄。
目录
引言
我从事客户端-服务器应用程序的工作已有几年了,所有这些应用程序都使用 RPC 作为客户端和服务器之间的通信层。我发现 CodeProject 上竟然没有一篇关于此主题的真正文章,这让我感到很奇怪,所以我决定自己写一篇来分享我的知识。
另一方面,这个主题有点大,所以我决定将其分成几篇不同难度的文章。这是第二篇文章,它将向您介绍上下文句柄。在阅读本文之前,您应该已经阅读了上一篇文章。
介绍上下文
什么是上下文句柄?它有什么用?嗯,我们在编程时无时无刻不在使用上下文句柄,但它们在不同地方通常有不同的名称。您可以将上下文句柄视为 C++ 中的 this
指针、从 CreateFile
返回的 HANDLE
,或者从 fopen
返回的 FILE*
指针的等价物。它们都是不同的上下文句柄,行为略有不同,但它们都完成了同一件事:它们都连接到一个对象。
您习惯使用的不同上下文句柄或多或少是半透明的,而在 RPC 中,上下文句柄是完全不透明的,因为它们实际上是 void*
类型的指针。
现在是时候将我上一篇文章中的“Hello World”示例扩展到使用一个相当简单的上下文句柄了。现在是最佳时机。
你好,上下文世界!
// File ContextExample.idl
[
// A unique identifier that distinguishes this
// interface from other interfaces.
uuid(00000003-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 ContextExample // The interface is named ContextExample
{
// To fully use context handles we need to do a typedef.
typedef [context_handle] void* CONTEXT_HANDLE;
// Open a context on the server.
CONTEXT_HANDLE Open(
// Explicit server binding handle.
[in] handle_t hBinding,
// String to be output on the server.
[in, string] const char* szString);
// Output the context string on the server.
void Output(
// Context handle. The binding handle is implicitly
// used through the explicit context handle.
[in] CONTEXT_HANDLE hContext);
// Closes a context on the server.
void Close(
// Context handle. The binding handle is implicitly
// used through the explicit context handle.
[in, out] CONTEXT_HANDLE* phContext);
}
变更
与上一篇文章相比,此示例中有什么变化?我们添加了一个名为 CONTEXT_HANDLE
的上下文句柄的 typedef
(“哦哦”,群众欢呼)。我们还添加了两个用于打开和关闭上下文句柄的函数。Output
函数已更改,使其接受上下文句柄而不是 string
。仅此而已,没有其他任何变化。
如您所见,我们正在使用显式绑定句柄,但 Output
和 Close
函数并不直接引用绑定句柄。它们都通过上下文句柄间接使用绑定句柄。客户端上下文句柄包含它所连接的绑定句柄,因此实际上不需要同时将绑定句柄和上下文句柄发送到函数。
上下文服务器
现在是将生成的文件应用到我们的服务器应用程序的时候了。
你好,服务器上下文世界!
// File ContextExampleServer.cpp
#include <iostream>
#include <string>
#include "ContextExample.h"
// Write a formatted error message to std::cerr.
DWORD HandleError(const char* szFunction, DWORD dwError);
CONTEXT_HANDLE Open(
/* [in] */ handle_t hBinding,
/* [string][in] */ const char* szString)
{
std::string* pContext = new std::string(szString);
CONTEXT_HANDLE hContext = pContext;
std::clog << "Open: Binding = " << hBinding
<< "; Context = " << hContext << std::endl;
return hContext;
}
void Output(
/* [in] */ CONTEXT_HANDLE hContext)
{
std::clog << "Output: Context = " << hContext << std::endl;
std::string* pContext = static_cast<std::string*>(hContext);
std::cout << *pContext << std::endl;
}
void Close(
/* [out][in] */ CONTEXT_HANDLE* phContext)
{
std::clog << "Close: Context = " << *phContext << std::endl;
std::string* pContext = static_cast<std::string*>(*phContext);
delete pContext;
// We must set the context handle to NULL, or else we will get
// a rundown later anyway.
*phContext = NULL;
}
// The RPC runtime will call this function if the connection to the client
// is lost.
void __RPC_USER CONTEXT_HANDLE_rundown(CONTEXT_HANDLE hContext)
{
std::clog << "CONTEXT_HANDLE_rundown: Context =
" << hContext << std::endl;
Close(&hContext);
}
// The thread that will listen for incoming RPC calls.
DWORD WINAPI RpcServerListenThreadProc(LPVOID /*pParam*/)
{
// Start to listen for remote procedure calls
// for all registered interfaces.
// This call will not return until
// RpcMgmtStopServerListening is called.
return RpcServerListen(
1, // Recommended minimum number of threads.
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Recommended maximum number of threads.
FALSE); // Start listening now.
}
// 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;
std::clog << "Calling RpcServerUseProtseqEp" << std::endl;
// 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)
return HandleError("RpcServerUseProtseqEp", status);
std::clog << "Calling RpcServerRegisterIf" << std::endl;
// Registers the ContextExample interface.
status = RpcServerRegisterIf2(
ContextExample_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)
return HandleError("RpcServerRegisterIf", status);
std::clog << "Creating listen thread" << std::endl;
const HANDLE hThread = CreateThread(NULL, 0, RpcServerListenThreadProc,
NULL, 0, NULL);
if (!hThread)
return HandleError("CreateThread", GetLastError());
std::cout << "Press enter to stop listening" << std::endl;
std::cin.get();
std::clog << "Calling RpcMgmtStopServerListening" << std::endl;
status = RpcMgmtStopServerListening(NULL);
if (status)
return HandleError("RpcMgmtStopServerListening", status);
std::clog << "Waiting for listen thread to finish";
while (WaitForSingleObject(hThread, 1000) == WAIT_TIMEOUT)
std::clog << '.';
std::clog << std::endl << "Listen thread finished" << std::endl;
DWORD dwExitCodeThread = 0;
GetExitCodeThread(hThread, &dwExitCodeThread);
CloseHandle(hThread);
if (dwExitCodeThread)
return HandleError("RpcServerListen", dwExitCodeThread);
std::cout << "Press enter to exit" << std::endl;
std::cin.get();
}
// 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);
}
变更
现在,与我们的第一个独立应用程序相比,服务器实现相当庞大,但与上一篇文章中的服务器示例相比,并没有大多少。
那么有什么变化呢?添加了 Open
和 Close
函数的实现。如您所见,上下文句柄实际上是指向 std::string
的指针,但被伪装成了 CONTEXT_HANDLE
(它本身是指向 void*
类型的指针)。服务器现在更加详细,它将正在执行的操作以及任何错误写入控制台窗口。监听传入 RPC 调用的实际功能现在在一个线程中实现,这反过来允许通过按 Enter 键来关闭服务器,而不是通过关闭窗口来终止它。内存分配和去分配例程保持不变。
我还没有讨论的一个问题是注销例程。您可能会问,什么是注销例程?注销例程是服务器有机会释放上下文句柄分配的任何资源的机制,如果客户端与服务器断开连接。如果客户端以某种方式与服务器断开连接,RPC 运行时会自动为客户端的每个打开的上下文句柄调用注销例程。
上下文客户端
现在是时候编写我们的客户端应用程序了,它将连接到服务器并使用新的上下文句柄。
你好,客户端上下文世界!
// File ContextExampleClient.cpp
#include <iostream>
#include "ContextExample.h"
// Write a formatted error message to std::cerr.
DWORD HandleError(const char* szFunction, DWORD dwError);
int main()
{
RPC_STATUS status;
unsigned char* szStringBinding = NULL;
std::clog << "Calling RpcStringBindingCompose" << std::endl;
// 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)
return HandleError("RpcStringBindingCompose", status);
handle_t hBinding = NULL;
std::clog << "Calling RpcBindingFromStringBinding" << std::endl;
// 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.
&hBinding); // Put the result in the explicit binding handle.
if (status)
return HandleError("RpcBindingFromStringBinding", status);
std::clog << "Calling RpcStringFree" << std::endl;
// Free the memory allocated by a string.
status = RpcStringFree(
&szStringBinding); // String to be freed.
if (status)
return HandleError("RpcStringFree", status);
std::clog << "Calling RpcEpResolveBinding" << std::endl;
// Resolves a partially-bound server binding handle into a
// fully-bound server binding handle.
status = RpcEpResolveBinding(hBinding, ContextExample_v1_0_c_ifspec);
if (status)
return HandleError("RpcEpResolveBinding", status);
RpcTryExcept
{
std::clog << "Calling Open" << std::endl;
// Open the context handle.
CONTEXT_HANDLE hContext = Open(hBinding, "Hello Context World!");
std::cout << "Press enter to call Output" << std::endl;
std::cin.get();
std::clog << "Calling Output" << std::endl;
// Calls the RPC function. The hBinding binding handle
// is used explicitly.
Output(hContext);
std::cout << "Press enter to call Close" << std::endl;
std::cin.get();
std::clog << "Calling Close" << std::endl;
// Close the context handle.
Close(&hContext);
}
RpcExcept(1)
{
HandleError("Remote Procedure Call", RpcExceptionCode());
}
RpcEndExcept
std::clog << "Calling RpcBindingFree" << std::endl;
// Releases binding handle resources and disconnects from the server.
status = RpcBindingFree(
&hBinding); // Frees the explicit binding handle.
if (status)
return HandleError("RpcBindingFree", status);
std::cout << "Press enter to exit" << std::endl;
std::cin.get();
}
// 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);
}
变更
与上次相比有什么变化?我们通过使用 RpcEpResolveBinding
在使用绑定句柄之前检查其状态。这将尝试在实际使用服务器暴露的任何 RPC 函数之前验证服务器是否正在运行(或未运行)。然后,我们创建一个上下文,使用此上下文输出 string
,最后关闭上下文。客户端现在更加详细,它将正在执行的操作以及任何错误写入控制台窗口。内存分配和去分配例程保持不变。
为了测试服务器中的注销例程,我在代码中添加了一些等待用户按 Enter 键的点。如果您通过关闭窗口而不是按 Enter 键来终止应用程序,您将看到注销生效。
附录
本节介绍了一些在使用 RPC 应用程序中的上下文句柄时很有用的技术。
多线程
本文中展示的示例不是线程安全的。如果两个线程同时在服务器中执行操作,事情可能会开始出现异常甚至崩溃。在这个简单的示例中这无关紧要,但可以通过使用某种互斥同步对象(临界区)轻松解决。这将在未来的文章中讨论。
服务器崩溃
如果服务器变得不可用/崩溃,客户端应用程序应调用 RpcSmDestroyClientContext
函数来释放其上下文数据。
结论
在 RPC 中使用上下文句柄是一件非常强大的事情,它有助于您使用 RPC 开发面向对象的应用程序。服务器上的对象可以由客户端上的上下文句柄表示,通过封装,客户端上的对象的实现通常可以通过使用上下文句柄直接映射到服务器上的函数。
尽管如此,我们只触及了 RPC 和客户端/服务器世界的表面。在我未来的文章中,我们将深入研究。
参考文献
- MSDN - 远程过程调用 [^]
- MSDN - 远程过程调用:上下文句柄 [^]
修订历史
- 2012-12-22
- 最后更新以支持 Visual Studio 2010 并修复了安全回调
- 2003-08-31
- 原文