Want to learn how to write distributed applications on the client/server architecture? This article, the first of a series, discusses how to use RPC on Win32 platforms.
Recently I was working on a Smart Card Reader remoting project for which I needed a client-server applications structure to make the client believe that a Smart Card reader is attached to the local machine when the reader will probably be on the other side of the globe. I have been developing application for some time that have similar requirements using RPC as the layer of communication between the client and the server. So I thought it would be a good idea to write an article about a technology that is so powerful and yet so simple.
It’s easy to learn how to use RPC on Win32 platforms, but for better understanding we should first emphasize the basics and then move to advanced topics. So I have divided the whole topic in five articles that you’ll get here on ASP Free. But don’t you worry…I shall provide a working sample application and code with most of them, so you may try it for yourself. For this article you may download the code here.
So let’s get started!
What is this RPC anyway?
Remote Procedure Call (RPC) is a powerful technology that allows you to write distributed applications on the Client/Server architecture. Most of the routine chores of making a connection and communicating between two endpoints are managed by RPC runtime libraries. That means one has more liberty to spend more time on the problem domain (or the problem at hand) rather than the details of the underlying network protocol and communication between client and server.
The title of the article has been chosen carefully because RPC is not a technology by Microsoft; it’s Microsoft’s implementation of the open concept of RPC. This means that if implementations on two platforms exist as per the specification, a client will be able to connect to a server running on another platform. Maybe the server could be written for Linux and the client could be written for Win32.
If we’re to write applications using RPC on Windows, we must surely be aware of a language that can describe the interface that the server will expose to the client -- basically a channel that sets an agreement between the client and the server. IDL is just that such a thing!
IDL stands for Interface Definition Language. It's a language for defining interfaces to which the client and server should adhere. You might be thinking, "what the heck? A new language just to define interfaces?! Can’t I just make a pure abstract class in C++ and use it?" I too wish that to be true but the fact of life is that the syntax of C or C++ doesn’t allow one to clearly mark a method quite precisely –- I mean in a way that both client and server running across process boundaries, or maybe machine boundaries, may communicate. Fortunately the syntax of IDL is very intuitive and similar to C/C++, and therefore is easy to grasp. So writing an IDL file is somewhat similar to writing a C header file with some added keywords and constructs. IDL is basically an attributed programming language and thus can describe parameters, functions and interfaces in a more precise and descriptive way than C.
Do you “C” the application?
Let’s start with an example so that we grasp the most essential concepts as we encounter them in the process. First we shall take up a simple program and then add to it the Client/Server functionality using RPC. This will let you see clearly the role RPC plays.
// File NoRPC.cpp using namespace std; #include <iostream>
// our would be server function. void Foo(const char* szMsg) { std::cout << szMsg << std::endl; }
int main() { // There is no server // Call local Foo("I can’t RPC Foo!"); }
This small code fragment does nothing but print ‘I can’t RPC Foo!’ to the console window.
Now in order to make the above program use RPC we need to define an interface first that this program may use. So let’s type into a plain text file the interface definition using IDL and save it as DoRPC.idl. I’ll explain the meaning of each line with a comment.
// File DoRPC.idl [ // A unique identifier that distinguishes this // interface from other interfaces. uuid(F0C37BD0-0D1B-4513-8C51-EC0D699740C0),
// This is version 1.0 of this interface. version(1.0),
// This interface will use an implicit binding // handle named hDoRPCBinding. implicit_handle(handle_t hDoRPCBinding) ]
interface DoRPC // The interface is named DoRPC { // A function that takes a zero-terminated string. void Show( [in, string] const char* szMsg); }
As a companion to this IDL file we also need to write an Application Configuration File (.acf) with the following text and save it as DoRPC.acf. in the same directory as the IDL file.
// File DoRPC.acf [ // This interface will use an implicit binding handle named hDoRPCBinding. implicit_handle(handle_t hDoRPCBinding) ] interface DoRPC // The interface is named DoRPC { }
Now you may ask the MIDL compiler to generate the source code for you that you may use in your client/server application. To generate the files invoke the tool MIDL.exe as follows:
MIDL /app_config /no_cpp DoRPC.idl
If this doesn’t work, you may have to run the vcvars32.bat file to setup the environment variables. This will generate three files for you: DoRPC.h, DoRPC_c.c, and DoRPC_s.c. The files with .c extensions are the source code files generated for the client and server side implementation of the interface; they can be distinguished by the _c and the _s appended to filename. These filenames are, however, configurable through command line switches. This situation can be explained as in the diagram below.
With the help of the code generated with MIDL compiler we can easily write a server application. You may write the code as below and read the comments to follow what’s happening around.
// Uses the protocol combined with the endpoint for receiving // remote procedure calls. status = RpcServerUseProtseqEp( (unsigned char*)("ncacn_ip_tcp"),// Use TCP/IP protocol RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Backlog q length for TCP/IP. (unsigned char*)("9191"), // TCP/IP port to use. NULL); // No security.
if(status) { exit(status); }
// Registers the DoRPC interface. status = RpcServerRegisterIf( DoRPC_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) 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); }
return 0; }
// 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); }
// Now we implement our server function. void Show(const unsigned char* szMsg) { printf("%s\n",szMsg); }
int main() { RPC_STATUS status; unsigned char* szStringBinding = NULL;
// Creates a string binding handle. // This function formats the passed values in a // predefined format for use by RPC. Just like printf // Connection is not done here. status = RpcStringBindingCompose( NULL, // UUID to bind to. (unsigned char*)("ncacn_ip_tcp"), // Use TCP/IP protocol. (unsigned char*)("localhost"), // TCP/IP network // the same machine as server (unsigned char*)("9191"), // 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. &hDoRPCBinding); // Put the result in the implicit binding
// handle defined in the IDL file.
if(status) { exit(status); }
RpcTryExcept { // Calls the RPC function. The hDoRPCBinding binding handle // is used implicitly. // Connection is done here. const unsigned char szMsg[] = "Client: I Can RPC Now!"; Show(szMsg); } RpcExcept(1) { printf("Runtime exception occured: %d\n",RpcExceptionCode()); } 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( &hDoRPCBinding); // Frees the implicit binding handle defined in // the IDL file.
if (status) { exit(status); }
return 0; }
// 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); }
See it happening
Now open up two command prompt consoles and execute server on one and Client on the other and you’ll see what we’ve accomplished with the help of MIDL and RPC. Wasn’t that easy?
The figure below shows the demo running on my computer:
Figure 2: The output
Looking at the whole picture
Let’s go back and recall the steps that we performed to achieve this. First we need to compile the IDL file to get the client proxy(DoRPC_c.c), the server stub (DoRPC_s.c) and the common header file (DoRPC.h). Next the proxy and the stub are compiled, with the client and server implementations of the interface producing standalone client and server executables. If nothing goes wrong and the client and server applications can communicate, we just completed our first RPC client/server application.
Here are a few points of interest that you might like to know before developing any other RPC applications.
Debugging RPC Applications
If you encounter problems when debugging and you end up finding that the problem is in a MIDL generated file, start over; the real problem is in the client or in the server and not in the MIDL generated code. This article is meant to get you started using RPC, but in a future article I will describe these topics in detail.
Types of binding handles
When using RPC, the binding handles can be implicit (implicit_handle) or explicit (explicit_handle). I always use explicit handles since I'm sometimes connected to multiple servers that do not work with the implicit handle. To use explicit handles, you'll have to change the IDL file, the server and the client. The difference will be in the interface member definition. Now each method defined in the interface will have a handle as its first argument.
// File DoRPCExp.idl [ // A unique identifier that distinguishes this // interface from other interfaces. // this is version 1.0 of this interface. uuid(2BC900B0-9E3F-451c-A134-581B6328E2CA),
version(1.0), // this interface uses explicit binding handle.
explicit_handle ]
interface DoRPCExplicit // The interface is named DoRPCExplicit { // A function that takes a binding handle and a char string. void Show( [in] handle_t hBinding, [in, string] const char* szMsg); }
There is also a handle type known as auto_handle, that allows you to connect and invoke RPC functions on the server automatically without maintaining a RPC handle.
The Application Configuration File (ACF)
The DoRPC example uses an implicit_handle that is a Microsoft extension. You may use the explicit_handle directly in the IDL file, and in that case you won’t need an ACF file because each interface method will absorb the handle as its first argument (as shown above). You may try that by modifying the code provided with this article just to see it working. In case of implicit handles, one usually needs to use a separate Application Configuration File that contains the handles that you wish to use. We’ll see more on ACF files later.
Trust MIDL
One should not modify the MIDL generated files to make them compile; they ought to be correct. Please do check the switches to midl.exe if you feel that they are incorrect and you need some results other than default. Sometimes when compiling MIDL generated source code, you may get a lot of warning messages. You may lower the warning level to two in the C/C++ tab in Project Settings to make them silent.
Shutting down the server
The server we just wrote runs indefinitely until it is forced to shut down by closing the console. That truly isn’t the best way of doing it. A better way is to call the RpcMgmtStopServerListening function that makes the server stop listening. You might be thinking: “If I don’t get back from the RpcServerListen function, how am I supposed to call this function?” You could add another function to the interface (StopServer() is a good candidate!!) that will call RpcMgmtStopServerListening or you could create another thread in the server before calling RpcServerListen that will call RpcMgmtStopServerListening after a predefined time interval or on a particular event.
Summary
This article introduced you to the world of RPC and using it to develop client/server applications. If you really want to take advantage of this technology, just glance through the examples in the Platform SDK samples. They’re a great place to start. I shall discuss more complex issues in the RPC world in my future articles.