Table of Contents
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 will split it into several articles of different levels of difficulty. This first one is at the beginner level.
In order to use RPC, you need to have some knowledge about IDL, but don't you worry, this article will help you.
IDL stands for Interface Definition Language and it's a language for defining interfaces (no kidding). Writing an IDL file is somewhat like writing a C header file with some additional keywords and constructs. IDL is an attributed programming language and thus it can describe parameters, functions and interfaces in more detail than C.
Remote Procedure Call (RPC) defines a powerful technology for creating distributed client/server programs. The RPC runtime libraries manage most of the details relating to network protocols and communication. This enables you to focus on the details of the application rather than the details of the network.
With RPC, a client can connect to a server running on another platform. For example: In theory, the server could be written for Linux and the client could be written for Win32. The reality is somewhat more complicated.
If you're not scared, I think it's a good time for an example.
This standalone application will not use RPC and it is a simple HelloWorld
application that we later will transform into a RPC client/server application.
#include <iostream>
void Output(const char* szOutput)
{
std::cout << szOutput << std::endl;
}
int main()
{
Output("Hello Lonely World!");
}
I don't think anyone can think of anything to say about the above application. It outputs the string
"Hello Lonely World!"
to the standard output.
It is time to define our interface in IDL.
[
uuid(00000001-EAF3-4A7A-A0F2-BCE4C30DA77E),
version(1.0),
implicit_handle(handle_t hExample1Binding)
]
interface Example1
{
void Output(
[in, string] const char* szOutput);
}
Here, you see the attribute programming used by IDL. The above example defines an interface with an universally unique identifier (uuid
) and version (version
) that is named Example1
. The Example1
interface defines one function named Output
that takes a const char*
argument named szOutput
as input (in
) that is zero-terminated (string
).
The implicit_handle
attribute for the interface will be discussed later, leave it there for now.
In order to use the IDL in our application, we need to run it through a compiler (midl.exe) that will translate the IDL to a client proxy and a server stub in C. The proxy/stub will later be compiled using your favorite compiler (cl.exe in my case).
It's time to take the generated files and put them to use in our server application.
#include <iostream>
#include "Example1.h"
void Output(const char* szOutput)
{
std::cout << szOutput << std::endl;
}
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE , void* )
{
return RPC_S_OK;
}
int main()
{
RPC_STATUS status;
status = RpcServerUseProtseqEp(
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"),
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
reinterpret_cast<unsigned char*>("4747"),
NULL);
if (status)
exit(status);
status = RpcServerRegisterIf2(
Example1_v1_0_s_ifspec,
NULL,
NULL,
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
(unsigned)-1,
SecurityCallback);
if (status)
exit(status);
status = RpcServerListen(
1,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
FALSE);
if (status)
exit(status);
}
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}
void __RPC_USER midl_user_free(void* p)
{
free(p);
}
This is a bit different from the standalone application, but not much. It has some initialization code for registering the interface but the Output
function remains the same.
Time has come to write our client application that will connect to the server.
#include <iostream>
#include "Example1.h"
int main()
{
RPC_STATUS status;
unsigned char* szStringBinding = NULL;
status = RpcStringBindingCompose(
NULL,
reinterpret_cast<unsigned char*>("ncacn_ip_tcp"),
reinterpret_cast<unsigned char*>("localhost"),
reinterpret_cast<unsigned char*>("4747"),
NULL,
&szStringBinding);
if (status)
exit(status);
status = RpcBindingFromStringBinding(
szStringBinding,
&hExample1Binding);
if (status)
exit(status);
RpcTryExcept
{
Output("Hello RPC World!");
}
RpcExcept(1)
{
std::cerr << "Runtime reported exception " << RpcExceptionCode()
<< std::endl;
}
RpcEndExcept
status = RpcStringFree(
&szStringBinding);
if (status)
exit(status);
status = RpcBindingFree(
&hExample1Binding);
if (status)
exit(status);
}
void* __RPC_USER midl_user_allocate(size_t size)
{
return malloc(size);
}
void __RPC_USER midl_user_free(void* p)
{
free(p);
}
Other than connecting to the server and some cleanup, the actual call is the same, except that I've changed the string
to be outputted.
First, we need to compile the IDL file to get the client proxy, the server stub and the common header file. The proxy and the stub is compiled, as are the client and server implementation. We link the two applications and run them. If everything works, we can be really glad as we have done our first RPC client/server application.
This section describes some techniques useful when writing RPC applications.
If you get into trouble when debugging and the problem seems to be in a MIDL generated file, the real problem will most likely be in the client or in the server. I have sometime run into problems with pointers, but in a follow up article, I will describe these things more thoroughly.
When using RPC, the binding handles can be implicit (as in the example in this article) or explicit. I always use explicit handles as I sometimes am connected to multiple servers, and that does not work with the implicit handle. To use explicit handles, you'll have to change the IDL file, the server and the client:
[
uuid(00000002-EAF3-4A7A-A0F2-BCE4C30DA77E),
version(1.0),
explicit_handle
]
interface Example1Explicit
{
void Output(
[in] handle_t hBinding,
[in, string] const char* szOutput);
}
#include <iostream>
#include "Example1Explicit.h"
void Output(handle_t hBinding, const char* szOutput)
{
std::cout << szOutput << std::endl;
}
#include "Example1Explicit.h"
int main()
{
handle_t hExample1ExplicitBinding = NULL;
status = RpcBindingFromStringBinding(
szStringBinding,
&hExample1ExplicitBinding);
if (status)
exit(status);
RpcTryExcept
{
Output(hExample1ExplicitBinding, "Hello RPC World!");
}
RpcExcept(1)
{
std::cerr << "Runtime reported exception " << RpcExceptionCode()
<< std::endl;
}
RpcEndExcept
status = RpcBindingFree(
&hExample1ExplicitBinding);
if (status)
exit(status);
}
There are also something called auto_handle
, but I've never ever used that. It somehow takes care about connecting to the server automatically.
In this example, I used implicit_handle
and explicit_handle
directly in the IDL file, but that is a Microsoft extension. One usually needs to use a separate Application Configuration File that contain these. The sample code in the zip file does use a separate ACF file, but I felt like writing that in the article would only confuse you even more.
You should not fiddle with the generated files to make them compile, they are (should be) correct. Check the switches to midl.exe if you feel that they are incorrect. When compiling them, you may on the other hand get a lot of warnings, but when lowering the warning level to 2, they are silent.
The example server will run until it is shut down by closing it somehow. That ain't the best way of doing it, another better way is to call the RpcMgmtStopServerListening
function. But how would you call it? You could add another function in the interface (perhaps named Shutdown
?) that will call RpcMgmtStopServerListening
or you could create another thread in the server before calling RpcServerListen
that will call RpcMgmtStopServerListening
after a minute or so. More on this in another article.
This is only the entry door to the world of RPC and client/server applications. If you step through and read my other (future) articles on this matter, you'll be fully prepared for the world at hand.
This was my first ever article on CodeProject, I hope you enjoyed reading it as much as I enjoyed writing it.
- 2012-12-22
- Finally updated to work with Visual Studio 2010 and fixed security callback
- 2003-08-25
- Updated demo and source
- Changed from Named Pipe to TCP/IP (suggestion by Hector Santos)
- 2003-08-23