Introduction to RPC - Part 2

本文介绍了远程过程调用(RPC)中上下文句柄的概念及其应用。通过一个简单的示例,展示了如何在客户端和服务端使用上下文句柄进行对象级别的通信,并探讨了多线程和服务器崩溃情况下的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Hello Context World!

Contents

Introduction

I have worked with client-server applications for a couple of years now, all of these applications use RPC as the layer of communication between the client and the server. I found it strange that no real article existed on this matter here on CodeProject, so I decided to write one of my own to spread my knowledge on this matter.

The matter is on the other hand a bit big, so I have decided to split it into several articles of different levels of difficulty. This is the second article and it will introduce you to context handles. You should have read the previous article before reading this one.

Introducing contexts

What is a context handle and what is it good for? Well we use context handles all the time when we are programming, but often they have different names in different places. You can think of a context handle as the equivalent to the this-pointer in C++, the HANDLE returned from CreateFile, or the FILE*-pointer returned from fopen. They are all different context handles that behave somewhat different, but they accomplish the same thing: they all connect to an object.

The different context handles you are used to, are more or less opaque, in RPC the context handles are totally opaque as they are actually pointers of type void*.

It is time to expand the Hello World example from my previous article with the use of a rather simple context handle. There is no better time than now.

Hello Context World!

Collapse
// 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);
}
Changes

What has changed in this example from the previous one? We have added a typedef of a context handle named CONTEXT_HANDLE ("Oooh", the masses roar). We have also added two functions to open and close the context handle. The Output function has been changed so that it takes a context handle instead of a string. And that's it, nothing else has changed.

As you can see we are using explicit binding handles, but the Output and the Close functions do not refer to the binding handle directly. They both use the binding handle indirectly through the context handle. A client side context handle contain the binding handle it is connected to, so there is no real need to send both the binding handle and the context handle to the functions.

Context server

It's time to take the generated files and put them to use in our server application.

Hello Server Context World!

Collapse
// 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.

}

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 = RpcServerRegisterIf(
ContextExample_v1_0_s_ifspec, // Interface to register.

NULL, // Use the MIDL generated entry-point vector.

NULL); // Use the MIDL generated entry-point vector.

if (status)
return HandleError("RpcServerRegisterIf", status);

std::clog << "Creating listen thread" << std::endl;
DWORD dwThreadId = 0;
const HANDLE hThread = CreateThread(NULL, 0,
RpcServerListenThreadProc,
NULL, 0, &dwThreadId);
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);
}
Changes

Now the server implementation is rather big compared to our first standalone application, but not so much bigger than the server example in the previous article.

So what has changed? The implementation of the Open and Close functions were added. As you can see the context handle is in fact a pointer to a std::string but disguised as a CONTEXT_HANDLE (that in turn is a pointer of type void*). The server is now more verbose, it writes what it is doing and any errors to the console window. The actual functionality to listen to incoming RPC calls is now implemented in a thread, this in turn allows the server to be shutdown by pressing enter, instead of killing it by closing the window. The memory allocation and de-allocation routines remain the same.

Something that I haven't talked about yet is the rundown routine. What is a rundown routine, you may ask. A rundown routine is a chance for the server to free any resources allocated by a context handle, if the the client is disconnected from the server. The rundown routine is automatically called by the RPC runtime for each open context handle in the client, if the client somehow is disconnected from the server.

Context client

Time has come to write our client application that will connect to the server and use the new context handles.

Hello Client Context World!

Collapse
// 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);

std::clog << "Calling RpcMgmtIsServerListening" << std::endl;
// Check if the server is running.

status = RpcMgmtIsServerListening(hBinding);
if (status)
return HandleError("RpcMgmtIsServerListening", 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);
}
Changes

What has changed since the last time? We are checking the state of the binding handle before we are using it by using RpcEpResolveBinding and RpcMgmtIsServerListening. This will verify that the server is running (or not) before we use any of it's exposed RPC functions. Then we create a context, outputs the string using this context and finally closes the context. The client is now more verbose, it writes what it is doing and any errors to the console window. The memory allocation and de-allocation routines remain the same.

To test the rundown routine in the server, I have added some points in the code where it waits for the user to press enter. If you kill the application by closing the window instead of pressing enter, you will see the rundown in effect.

Appendix

This section describes some techniques useful when using context handles in RPC applications.

Multithreading

The example showed in this article is not thread-safe. If two threads does things in the server at the same time, things could start behaving strange or even crash. In this simple example it does not matter, but it could easily be fixed by using some sort of mutual exclusive synchronization objects (critical sections). This will be addressed in a future article.

Server crashes

If the server becomes unavailable/crashes, the client application should call the function RpcSmDestroyClientContext to free its context data.

Conclusion

Using context handles in RPC is a very powerful thing, it helps you to develop object oriented applications with RPC. An object on the server can be represented by a context handle on the client, and using encapsulation, the implementation of an object on the client can often map functions directly to the server, by using context handles.

Still we have only scratched the surface of the world of RPC and client/server applications. In my future articles, we will dig even deeper.

References

Revision history

  • 2003-08-31:
    • Original article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Anders Dalvander



Occupation: Web Developer
Location: Sweden Sweden


From: http://www.codeproject.com/KB/IP/rpcintro2.aspx
资源下载链接为: https://pan.quark.cn/s/9648a1f24758 这个HTML文件是一个专门设计的网页,适合在告白或纪念日这样的特殊时刻送给女朋友,给她带来惊喜。它通过HTML技术,将普通文字转化为富有情感和创意的表达方式,让数字媒体也能传递深情。HTML(HyperText Markup Language)是构建网页的基础语言,通过标签描述网页结构和内容,让浏览器正确展示页面。在这个特效网页中,开发者可能使用了HTML5的新特性,比如音频、视频、Canvas画布或WebGL图形,来提升视觉效果和交互体验。 原本这个文件可能是基于ASP.NET技术构建的,其扩展名是“.aspx”。ASP.NET是微软开发的一个服务器端Web应用程序框架,支持多种编程语言(如C#或VB.NET)来编写动态网页。但为了在本地直接运行,不依赖服务器,开发者将其转换为纯静态的HTML格式,只需浏览器即可打开查看。 在使用这个HTML特效页时,建议使用Internet Explorer(IE)浏览器,因为一些老的或特定的网页特效可能只在IE上表现正常,尤其是那些依赖ActiveX控件或IE特有功能的页面。不过,由于IE逐渐被淘汰,现代网页可能不再对其进行优化,因此在其他现代浏览器上运行可能会出现问题。 压缩包内的文件“yangyisen0713-7561403-biaobai(html版本)_1598430618”是经过压缩的HTML文件,可能包含图片、CSS样式表和JavaScript脚本等资源。用户需要先解压,然后在浏览器中打开HTML文件,就能看到预设的告白或纪念日特效。 这个项目展示了HTML作为动态和互动内容载体的强大能力,也提醒我们,尽管技术在进步,但有时复古的方式(如使用IE浏览器)仍能唤起怀旧之情。在准备类似的个性化礼物时,掌握基本的HTML和网页制作技巧非常
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值