|
|
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.
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); }
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 |
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 simplest way of making remote procedure calls is with the RPC library routine rpc_call. It has nine parameters.
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 * rusers() { static unsigned long 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 *./* * Code here to compute the number of users * and place result in variable nusers. */ return((char *)&nusers); }
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>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; andchar *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); }
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.
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.
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"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.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); }
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:
bool_t 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];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.bool_t xdr_intarr(xdrsp, intarr) XDR *xdrsp; int intarr[]; { return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int)); }
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;Note that we could as easily call xdr_simple here instead of xdr_reference.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); }