Programming with Remote Procedure Calls (RPC)

Converting local procedures into remote procedures

Assume an application that runs on a single machine. Suppose we want to convert it to run over the network. Here we will show such a conversion by way of a simple example program that prints a message to the console. The source file for the original program might look like this:

   /* printmsg.c: print a message on the console */

#include <stdio.h>

main(argc, argv) int argc; char *argv[]; { char *message;

if (argc != 2) { fprintf(stderr, "usage: %s <message>\n", argv[0]); exit(1); } message = argv[1];

if (!printmessage(message)) { fprintf(stderr, "%s: couldn't print your message\n", argv[0]); exit(1); } printf("Message delivered!\n"); exit(0); } /* Print a message to the console. */ /* Return a boolean indicating whether the message was actually printed. */

printmessage(msg) char *msg; { FILE *f;

f = fopen("/dev/console", "w"); if (f == NULL) { return (0); } fprintf(f, "%s\n", msg); fclose(f); return (1); }

For local use on a single machine, this program could be compiled and executed as follows:

cc printmsg.c -o printmsg
printmsg "Hello there."

This should return the response ``Message delivered!''.

If the printmessage function were turned into a remote procedure, it could be called from anywhere in the network. It is not difficult to make a procedure remote.

NOTE: In the context of RPC programming, it has become acceptable to use the term procedure to refer to a C-language function. The terms are used interchangeably in this guide.

In general, it is necessary to figure out what the types are for all procedure inputs and outputs. Here, the procedure printmessage takes a string as input, and returns an integer as output. Knowing this, we can write a protocol specification in RPC language that describes the remote version of printmessage. The RPC language source code for such a specification might look like this:

   /* msg.x: Remote message printing protocol */

program MESSAGEPROG { version MESSAGEVERS { int PRINTMESSAGE(string) = 1; } = 1; } = 0x20000001;

Remote procedures are always declared as being part of remote programs. The above is actually a declaration for an entire remote program, one that contains the single procedure PRINTMESSAGE.

NOTE: In the context of RPC programming, the term ``remote program'' actually refers to a collection of (related) procedures.

In this example, the PRINTMESSAGE procedure is declared to be procedure 1, in version 1 of the remote program whose number is 0x20000001. (Refer to ``Program number assignment'' for guidance on choice of program numbers.)

By convention, all RPC services provide for a procedure 0; a call to a remote program's procedure 0 should do nothing (a ``no-op'') except succeed. To ping means to call procedure 0 of a remote program. Pinging is used to verify the existence and accessibility of a remote program.

Using rpcgen, no null procedure (procedure 0) need be written because rpcgen generates it automatically.

Notice that the program and procedure names are declared with all capital letters. This is not required, but is a good convention to follow.

Notice also that the argument type is string and not char * as it would be in C. This is because a char * in C is ambiguous. Programmers usually intend it to mean a null-terminated string of characters, but it could also represent a pointer to a single character or a pointer to an array of characters. In RPC language, a null-terminated string is unambiguously called a string.

There are two more things to write:

The following is one possible definition of a remote procedure to implement the PRINTMESSAGE procedure we declared above:
    * msg_proc.c: implementation of the remote procedure "printmessage"

#include <stdio.h> #include <rpc/rpc.h> /* always needed */ #include "msg.h" /* msg.h will be generated by rpcgen */

/* * Remote version of "printmessage" */ int * printmessage_1(msg) char **msg; { static int result; /* must be static! */ FILE *f;

f = fopen("/dev/console", "w"); if (f == NULL) { result = 0; return (&result); } fprintf(f, "%s\n", *msg); fclose(f); result = 1; return (&result); }

Notice that the declaration of the remote procedure printmessage_1 differs from that of the local procedure printmessage in three ways:

The last thing to do is declare the main client program that will call the remote procedure. This is one possibility:

    * rprintmsg.c: remote version of "printmsg.c"
   #include <stdio.h>
   #include <rpc/rpc.h>     /* always needed  */
   #include "msg.h"         /* msg.h will be generated by rpcgen */

main(argc, argv) int argc; char *argv[]; { CLIENT *cl; int *result; char *server; char *message;

if (argc != 3) { fprintf(stderr, "usage: %s host message\n", argv[0]); exit(1); }

/* * Save values of command line arguments */ server = argv[1]; message = argv[2];

/* * Create client "handle" used for calling MESSAGEPROG on the * server designated on the command line. */ cl = clnt_create(server, MESSAGEPROG, MESSAGEVERS, "visible"); if (cl == NULL) { /* * Couldn't establish connection with server. * Print error message and die. */ clnt_pcreateerror(server); exit(1); } /* * Call the remote procedure "printmessage" on the server */ result = printmessage_1(&message, cl); if (result == NULL) { /* * An error occurred while calling the server. * Print error message and die. */ clnt_perror(cl, server); exit(1); }

/* * Okay, we successfully called the remote procedure. */ if (*result == 0) { /* * Server was unable to print our message. * Print error message and die. */ fprintf(stderr, "%s: %s couldn't print your message\n", argv[0], server); exit(1); }

/* * The message got printed on the server's console */ printf("Message delivered to %s!\n", server); exit(0); }

There are four things to note here: This is how to put all the pieces together:

rpcgen msg.x
cc rprintmsg.c msg_clnt.c -o rprintmsg -lnsl
cc msg_proc.c msg_svc.c -o msg_server -lnsl

Two programs are compiled here: the client program rprintmsg and the server program msg_server. Before doing this, rpcgen was used to fill in the missing pieces.

This is what rpcgen (called without any flags) did with the input file msg.x:

  1. It created a header file called msg.h that contained #define statements for MESSAGEPROG, MESSAGEVERS and PRINTMESSAGE for use in the other modules.

  2. It created the client ``stub'' routines in the msg_clnt.c file. Here there is only one, the printmessage_1 routine, that was called from the rprintmsg client program. If the name of an rpcgen input file is FOO.x, the client stubs output file is called FOO_clnt.c.

  3. It created the server program in msg_svc.c that calls printmessage_1 from msg_proc.c. The rule for naming the server output file is similar to the previous one: for an input file called FOO.x, the output server file is named FOO_svc.c.

NOTE: If invoked with the -T argument, rpcgen creates an additional output file that contains index information used for the dispatching of service routines.

Once created, the server should be copied to a remote machine and run. (If the machines are homogeneous, the server can be copied as a binary. Otherwise, the source files will need to be copied to and compiled on the remote machine.) For this example, the remote machine is called remote and the local machine is called local. The server is started from the shell on the remote system:

   remote$ msg_server

NOTE: Server processes, like msg_server, created with rpcgen always run in the background. It is not necessary to follow the server's invocation with an ampersand (&). Servers generated by rpcgen can also be invoked with port monitors like listen and inetd, instead of from the command line.

Thereafter, a user on local can print a message on the console of system remote as follows:

rprintmsg remote "Hello there."

Using rprintmsg, a user can print a message on any system console (including the local console) if the server msg_server is running on the target system.

© 2005 The SCO Group, Inc. All rights reserved.
SCO OpenServer Release 6.0.0 -- 02 June 2005