Programming with Remote Procedure Calls (RPC)

The simplified interface to RPC

The easiest interface to RPC does not require the programmer to use the interface at all. ``RPC library-based network services'' describes using functions that hide all details of the RPC package.

Some RPC services are not available as C functions, but are available as RPC programs. ``Remote Procedure Call and registration'' shows how easy it is to use these services, and how easy it is to create new services that are equally simple to use.

Data types passed to and received from remote procedures can be any of a set of predefined types, or can be programmer-defined types. ``Passing arbitrary data types'' explains how such types are declared and used.

RPC library-based network services

Imagine writing a program that needs to know how many users are logged into a remote machine. This can be done by calling an RPC library routine, rusers, as illustrated below:

   #include <stdio.h>

/* * a program that calls rusers() */

main(argc, argv) int argc; char **argv; { int num;

if (argc != 2) { fprintf(stderr, "usage: %s hostname\n", argv[0]); exit(1); } if ((num = rusers(argv[1])) < 0) { fprintf(stderr, "error: rusers\n"); exit(1); } printf("%d users on %s\n", num, argv[1]); exit(0); }

NOTE: For rusers to work, the rusers daemon must be running on the remote host.

RPC library routines such as rusers are in the RPC services library librpcsvc.a. Thus, the program above should be compiled with

cc program.c -lrpcsvc -lnsl

These are some of the RPC service library routines available to the C programmer:

Routine Description
rusers Return information about users on remote machine
rwall Write to specified remote machines
spray Spray packets to a specific machine

Remote Procedure Call and registration

The simplest interface to the RPC functions is based on the routines rpc_call, rpc_reg, and rpc_broadcast. These functions provide direct access to the RPC facilities, and are appropriate for programs that do not require fine levels of control.

Using the simplified interface, the number of remote users can be gotten as follows:

   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <rpcsvc/rusers.h>

/* * a program that calls the RUSERSPROG RPC program */

main(argc, argv) int argc; char **argv; { unsigned long nusers; int clnt_stat;

if (argc != 2) { fprintf(stderr, "usage: rusers hostname\n"); exit(); } if (clnt_stat = rpc_call(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, 0, xdr_u_long, &nusers, "visible") != 0) { clnt_perrno(clnt_stat); exit(1); } printf("%d users on %s\n", nusers, argv[1]); exit(0); }

The rpc_call routine

The simplest way of making remote procedure calls is with the RPC library routine rpc_call. It has nine parameters.

Multiple arguments and results are handled by embedding them in structures. If rpc_call completes successfully, it returns zero; otherwise, it returns a nonzero value. The return codes (of type enum clnt_stat, cast to an int in the previous example) are found in <rpc/clnt.h>.

Because data types may be represented differently on different machines, rpc_call needs both the type of, and a pointer to, the RPC argument (similarly for the result). For RUSERSPROC_NUM, the return value is an unsigned long, so rpc_call has xdr_u_long as its first return parameter, which says that the result is of type unsigned long; and &nusers as its second return parameter, which is a pointer to where the long result will be placed. Because RUSERSPROC_NUM takes no argument, the argument parameter of rpc_call is xdr_void.

If rpc_call gets no answer within a certain time period, it returns with an error code. In the example, it tries all the transports listed in /etc/netconfig that are flagged as visible. Adjusting the number of retries requires use of the lower levels of the RPC library, discussed later in this section. The remote server procedure corresponding to the above might look like this:

   char *
   	static unsigned long nusers;

/* * Code here to compute the number of users * and place result in variable nusers. */ return((char *)&nusers); }

It takes one argument, which is a pointer to the input of the remote procedure call (ignored in our example), and it returns a pointer to the result. In many versions of C, character pointers are the generic pointers, so both the input argument and the return value are cast to char *.

The rpc_reg routine

Normally, a server registers all the RPC calls it plans to handle, and then goes into an infinite loop waiting to service requests. If rpcgen is used to provide this functionality, it will generate much code, including a server dispatch function and support for port monitors. But programmers can also write servers themselves using rpc_reg, and it is appropriate that they do so if they have simple applications, like the one shown as an example here. In this example, there is only a single procedure to register, so the main body of the server would look like this:

   #include <stdio.h>
   #include <rpc/rpc.h>
   #include <rpcsvc/rusers.h>

char *rusers();

main () {

if (rpc_reg(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, rusers, xdr_void, xdr_u_long, "visible") == -1) { fprintf(stderr, "Couldn't Register\n"); exit(1); } svc_run(); /* Never returns */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1); }

The rpc_svc_reg(NS) routine registers a C procedure as corresponding to a given RPC procedure number. The registration is done for each of the transports of the specified type, or if the type parameter is NULL, for all the transports named in NETPATH. The first three parameters, RUSERPROG, RUSERSVERS, and RUSERSPROC_NUM are the program, version, and procedure numbers of the remote procedure to be registered; rusers is the name of the local procedure that implements the remote procedure; and xdr_void and xdr_u_long name the XDR filters for the remote procedure's arguments and results, respectively. (Multiple arguments or multiple results are passed as structures.) The last parameter specifies the desired nettype.

When using rpc_reg, programmers are not required to write their own dispatch routines. Also, the dispatcher in rpc_reg takes care of decoding remote procedure arguments and encoding results, using the XDR filters specified when the remote procedure was registered.

After registering the local procedure, the server program's main procedure calls svc_run, the RPC library's remote procedure dispatcher, which is described on the rpc_svc_reg(NS) manual page. It is this function that calls the remote procedures in response to RPC call messages.

NOTE: The svc_run routine is used at all levels of RPC programming. Strictly speaking, it does not ``belong'' to this or to any other level.

CAUTION: The svc_run routine is not thread-safe and should not be called from multiple threads of execution. See ``Writing multithreaded RPC procedures'' and ``Multithreaded network programming'' for more information about using RPC with the Threads Library.

Passing arbitrary data types

In the previous example, the RPC call returned a single unsigned long. RPC can handle arbitrary data structures, regardless of different machines' byte orders or structure layout conventions, by always converting them to a standard transfer syntax called External Data Representation (XDR) before sending them over the transport. The process of converting from a particular machine representation to XDR format is called serializing, and the reverse process is called deserializing.

The type field parameters of rpc_call and rpc_reg can name an XDR primitive procedure, like xdr_u_long in the previous example, or a programmer supplied procedure (that may take a maximum of two parameters). XDR has these ``built-in'' primitive type routines: xdr_bool, xdr_char, xdr_enum, xdr_int, xdr_long, xdr_short, xdr_u_char, xdr_u_int, xdr_u_long, xdr_u_short, and xdr_wrapstring.

NOTE: The routine xdr_string exists, but takes more than two parameters. It cannot, therefore, be used with rpc_call and rpc_reg, which only pass two parameters to their XDR routines. xdr_wrapstring has only two parameters, and is thus acceptable. It, in turn, calls xdr_string.

As an example of a user-defined type routine, if a programmer wanted to send the structure:

   struct simple {
   	int a;
   	short b;
   } simple;
then rpc_call would be called as:
   rpc_call(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_simple, &simple ...);
where xdr_simple is written as:
   #include <rpc/rpc.h>
   #include "simple.h"

bool_t xdr_simple(xdrsp, simplep) XDR *xdrsp; struct simple *simplep; { if (!xdr_int(xdrsp, &simplep->a)) return (FALSE); if (!xdr_short(xdrsp, &simplep->b)) return (FALSE); return (TRUE); }

An XDR routine returns nonzero (true in the C sense) if it completes successfully, and zero otherwise. A complete description of XDR is provided in the ``XDR/RPC protocol specification''. Note that the above routine could have been generated automatically by using the rpcgen compiler.

In addition to the built-in primitives, there are also some prefabricated building blocks: xdr_array, xdr_bytes, xdr_opaque, xdr_pointer, xdr_reference, xdr_string, xdr_union, and xdr_vector.

To send a variable array of integers, the array might be packaged as a structure like this:

   struct varintarr {
   	int *data;
   	int arrlnth;
   } arr;
and sent by an RPC call such as:
   rpc_call(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_varintarr, &arr...);
with xdr_varintarr defined as:
   xdr_varintarr(xdrsp, arrp)
   	XDR *xdrsp;
   	struct varintarr *arrp;
   	return (xdr_array(xdrsp, &arrp->data, &arrp->arrlnth,
   		MAXLEN, sizeof(int), xdr_int));
The xdr_array routine takes as parameters the XDR handle, a pointer to the array, a pointer to the size of the array, the maximum allowable array size, the size of each array element, and an XDR routine for handling each array element.

If the size of the array is known in advance, one can use xdr_vector, which serializes fixed-length arrays.

   int intarr[SIZE];

bool_t xdr_intarr(xdrsp, intarr) XDR *xdrsp; int intarr[]; { return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int)); }

XDR always converts quantities to 4-byte multiples when serializing. Thus, if either of the examples above involved characters instead of integers, each character would occupy 32 bits. That is the reason for the XDR routine xdr_bytes, which is like xdr_array except that it packs characters; xdr_bytes has four parameters, similar to the first four parameters of xdr_array.

For null-terminated strings, there is the xdr_string routine, which is the same as xdr_bytes without the length parameter. On serializing it gets the string length from strlen, and on deserializing it creates a null-terminated string.

This is a final example that calls the previously written xdr_simple as well as the built-in functions xdr_string and xdr_reference, which chases pointers:

   struct finalexample {
   	char *string;
   	struct simple *simplep;
   } finalexample;

bool_t xdr_finalexample(xdrsp, finalp) XDR *xdrsp; struct finalexample *finalp; { if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN)) return (FALSE); if (!xdr_reference(xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple)) return (FALSE); return (TRUE); }

Note that we could as easily call xdr_simple here instead of xdr_reference.
© 2005 The SCO Group, Inc. All rights reserved.
SCO OpenServer Release 6.0.0 -- 02 June 2005