Developing distributed applications using ONC RPC and XDR

Using the intermediate layer

The simplest interface, which explicitly makes RPC calls, uses the functions callrpc and registerrpc. Here is another way to get the number of remote users:

   #include <stdio.h>
   #include <utmp.h>
   #include <rpc/types.h>
   #include <rpc/xdr.h>
   #include <rpcsvc/rusers.h>

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

if (argc < 2) { fprintf(stderr, "usage: nusers hostname\n"); exit(-1); } if (callrpc(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, 0, xdr_u_long, &nusers) != 0) { fprintf(stderr, "error: callrpc\n"); exit(1); } printf("number of users on %s is %d\n", argv[1], nusers); exit(0); }

A program number, version number, and procedure number define each RPC procedure. The program number defines a group of related remote procedures, each of which has a different procedure number. Each program also has a version number, so when a minor change is made to a remote service (adding a new procedure, for example) a new program number does not have to be assigned.

When you call a procedure to find the number of remote users, the appropriate program, version and procedure numbers are looked up in a manual, in a similar manner to looking up the name of the memory allocator when memory is to be allocated.

The simplest routine in the RPC library used to make remote procedure calls is callrpc. It has eight parameters:

If callrpc completes successfully, it returns zero; nonzero otherwise. The exact meaning of each return code is found in <rpc/clnt.h>, and each return code is in fact an enum clnt_stat cast into an integer.

Because data types may be represented differently on different machines, callrpc needs both the type of the RPC argument and a pointer to the argument itself, and needs similar information for the result.

For RUSERSPROC_NUM, the return value is an unsigned long. This means that callrpc has xdr_u_long as its first return parameter, which says that the result is of type unsigned long , and has &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 type parameter of callrpc is xdr_void and the pointer to the argument parameter variable is NULL.

The callrpc procedure uses the User Datagram Protocol (UDP) to send a message over the network and wait for a response. If UDP receives no response, it again sends the message and waits for a response. After trying several times to deliver a message and receiving no response, callrpc returns with an error code. Methods for adjusting the number of retries or for using a different protocol require the use of the lower layer of the RPC library.

The remote server procedure corresponding to callrpc might look like this:

   char *
           char *indata;
           static int nusers;

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

The procedure takes one argument, which is a pointer to the input of the remote procedure call (ignored in the above example) and it returns a pointer to the result. In the current version of C, character pointers are the generic pointers, so both the input argument and the return value are cast to char \(*. Normally, a server registers all of the RPC calls it plans to handle, and then goes into an infinite loop waiting to service requests. 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 <rpcsvc/rusers.h>

char *nuser();

main() { registerrpc(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, nuser, xdr_void, xdr_u_long); svc_run(); /* never returns */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1); }

The registerrpc routine establishes which C procedure corresponds to each RPC procedure number. The parameters are:

Only the UDP transport mechanism can use registerrpc; thus, it is always safe in conjunction with calls generated by callrpc.

NOTE: The UDP transport mechanism can deal only with arguments and results less than 8K bytes in length.

Assigning program numbers

Program numbers are assigned in groups of 0x20000000 (536870912) according to the following chart:

0 - 1fffffff defined by Sun Microsystems
20000000 - 3fffffff defined by user
40000000 - 5fffffff transient
60000000 - 7fffffff reserved
80000000 - 9fffffff reserved
a0000000 - bfffffff reserved
c0000000 - dfffffff reserved
e0000000 - ffffffff reserved

Using XDR to pass arbitrary data types

In the previous example, the RPC call passes a single unsigned long. RPC can handle arbitrary data structures, regardless of the byte order or structure layout conventions of different machine architectures. It does this by always converting the data to a network standard, eXternal Data Representation (XDR), before sending the data over the wire. 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 callrpc and registerrpc can be a built-in procedure like xdr_u_long in the previous example, or a user-supplied one. XDR has these built-in type routines:

xdr_int() xdr_u_int() xdr_enum()
xdr_long() xdr_u_long() xdr_bool()
xdr_short() xdr_u_short() xdr_string()

As an example of a user-defined type routine, assume that you want to send the following structure:

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

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

An XDR routine returns nonzero (TRUE in the sense of C) if it completes successfully, and zero otherwise. A complete description of XDR is in the section ``Using the XDR protocol'', so this section only gives a few examples of XDR implementation.

In addition to the built-in primitives, there are also the prefabricated building blocks:

xdr_array() xdr_bytes()
xdr_reference() xdr_union()

To send a variable array of integers, you can package them in a structure like this:

   struct varintarr {
           int *data;
           int arrlnth;
   } arr;
and make an RPC call such as:
   callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_varintarr, &arr...);
with xdr_varintarr defined as:
   xdr_varintarr(xdrsp, varintarr)
           XDR *xdrsp;
           struct varintarr *arrp;
           return (xdr_array(xdrsp, &arrp->data, &arrp->arrlnth, MAXLEN,
                   sizeof(int), xdr_int));
This 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, then the following could also be used to send out an array of length SIZE:
   int intarr[SIZE];

xdr_intarr(xdrsp, intarr) XDR *xdrsp; int intarr[]; { int i;

for (i = 0; i < SIZE; i++) { if (!xdr_int(xdrsp, &intarr[i])) return (0); } return (1); }

XDR always converts quantities to 4-byte multiples when deserializing. 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. It has four parameters which are the same as the first four parameters of xdr_array. For null-terminated strings, there is also the xdr_string routine, which is the same as xdr_bytes without the length parameter. On serializing, it gets the string length from strlen; on deserializing, it creates a null-terminated string.

Here 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;
   xdr_finalexample(xdrsp, finalp)
           XDR *xdrsp;
           struct finalexample *finalp;
           int i;

if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN)) return (0); if (!xdr_reference(xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple)) return (0); return (1); }

Next topic: Using the lower layers
Previous topic: Using the highest layer

© 2003 Caldera International, Inc. All rights reserved.
SCO OpenServer Release 5.0.7 -- 11 February 2003