This example generates four output files using the default file names: PROTO.H, PROTO_CLNT.C, PROTO_SVC.C, and PROTO_XDR.C. INETd starts the server and the server exits after 20 seconds of inactivity. #3
RPCGEN /HEADER_FILE /TABLE PROTO.X
#4This example sends the header file (with support for dispatch tables) to the default output file PROTO.H.
RPCGEN /TRANSPORT=TCP PROTO.X
#5This example sends the server skeleton file for the transport TCP to the default output file PROTO_SVC.C.
RPCGEN /HEADER_FILE /TABLE /OUTPUT=PROTO_TABLE.H PROTO.X
This example sends the header file (with support for dispatch tables) to the output file PROTO_TABLE.H.
For most applications, you do not need the information in this chapter;
you can simply use the automatic features of the RPCGEN protocol
compiler (described in Chapter 2). This chapter requires an
understanding of network theory; it is for programmers who must write
customized network applications using remote procedure calls, and who
need to know about the RPC mechanisms hidden by RPCGEN.
3.1 RPC Layers
The ONC RPC interface consists of three layers: highest, middle, and lowest. For ONC RPC programming, only the middle and lowest layers are of interest. For a complete specification of the routines in the remote procedure call library, see Chapter 5 through Chapter 8.
The middle layer routines are adequate for most applications. This layer is "RPC proper" because you do not need to write additional programming code for network sockets, the operating system, or any other low-level implementation mechanisms. At this level, you simply make remote procedure calls to routines on other systems. For example, you can make simple ONC RPC calls by using the following RPC routines:
The middle layer is not suitable for complex programming tasks because it sacrifices flexibility for simplicity. Although it is adequate for many tasks, the middle layer does not provide the following:
The lowest layer is suitable for programming tasks that require greater efficiency or flexibility. The lowest layer routines include client creation routines such as:
The middle layer is the simplest RPC program interface; from this layer
you make explicit RPC calls and use the functions callrpc and
registerrpc.
3.2.1 Using callrpc
The simplest way to make remote procedure calls is through the RPC
library routine callrpc. The programming code in Examples
3-1,
which obtains the number of remote users, shows the usage of
callrpc.
The callrpc routine has eight parameters. In Examples
3-1, the
first parameter, argv[1], is the name of the remote server
system as specified in the command line which invoked the
rnusers program. The next three, RUSERSPROG,
RUSERSVERS, and RUSERSPROC_NUM, are the program,
version, and procedure numbers that together identify the procedure to
be called (these are defined in rusers.h). The fifth and sixth
parameters are an XDR filter (xdr_void) and an argument
(0) to be encoded and passed to the remote procedure. You
provide an XDR filter procedure to encode or decode system-dependent
data to or from the XDR format.
The final two parameters are an XDR filter, xdr_u_long, for decoding the results returned by the remote procedure and a pointer, &nusers, to the storage location of the procedure results. Multiple arguments and results are handled by embedding them in structures.
If callrpc completes successfully, it returns zero; otherwise it returns a non-zero value. The return codes are found in <rpc/clnt.h>. The callrpc routine needs the type of the RPC argument, as well as a pointer to the argument itself (and similarly for the result). For RUSERSPROC_NUM, the return value is an unsigned long. This is why callrpc has xdr_u_long as its first return parameter, which means that the result is of type unsigned long, and &nusers as its second return parameter, which is a pointer to the location that stores the long result. RUSERSPROC_NUM takes no argument, so the argument parameter of callrpc is xdr_void. In such cases, the argument must be NULL.
If callrpc gets no answer after trying several times to deliver a message, it returns with an error code. Methods for adjusting the number of retries or for using a different protocol require you to use the lowest layer of the RPC library, discussed in Section 3.3.
The remote server procedure corresponding to the callrpc usage
example might look like the one in Examples
3-2.
This procedure takes one argument---a pointer to the input of the remote procedure call (ignored in the example)---and returns a pointer to the result. In the current version of C, character pointers are the generic pointers, so the input argument and the return value can be cast to char*.
/* * rnusers.c - program to return the number of users on a remote host */ #include <stdio.h> #include <rpc/rpc.h> #include "rusers.h" main(argc, argv) int argc; char **argv; { unsigned long nusers; int stat; if (argc != 2) { fprintf(stderr, "usage: rnusers hostname\n"); exit(1); } if (stat = callrpc(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, 0, xdr_u_long, &nusers) != 0) { clnt_perrno(stat); exit(1); } printf("%d users on %s\n", nusers, argv[1]); exit(0); }
Examples
3-2 Remote Server Procedure
unsigned long * nuser(indata) char *indata; { static unsigned long nusers; /* * Add code here to compute the number of users * and place result in variable nusers. * For this example, nusers is set to 5. */ nusers = 5; return(&nusers); }
3.2.2 Using registerrpc and svc_run
Normally, a server registers all the RPC calls it plans to handle, and
then goes into an infinite loop waiting to service requests. Using
RPCGEN for this also generates a server dispatch function. You can
write a server yourself by using registerrpc. Examples
3-3 is
a program showing how you would use registerrpc in the main
body of a server program that registers a single procedure; the remote
procedure returns a single unsigned long result.
The registerrpc routine establishes the correspondence between a procedure and a given RPC procedure number. The first three parameters (defined in rusers.h), RUSERPROG, RUSERSVERS, and RUSERSPROC_NUM, are the program, version, and procedure numbers of the remote procedure to be registered; nuser is the name of the local procedure that implements the remote procedure; and xdr_void and xdr_u_long are the XDR filters for the remote procedure's arguments and results, respectively. (Multiple arguments or multiple results are passed as structures.)
The underlying transport mechanism for registerrpc is UDP.
Note
The UDP transport mechanism can handle only arguments and results that are less than 8K bytes in length.
After registering the local procedure, the main procedure of the server program calls the RPC dispatcher using the svc_run routine. The svc_run routine calls the remote procedures in response to RPC requests and decodes remote procedure arguments and encodes results. To do this, it uses the XDR filters specified when the remote procedure was registered with registerrpc.
The remote server procedure, nuser, was already shown in
Examples
3-2 and is duplicated in this example. This procedure takes
one argument---a pointer to the input of the remote procedure call
(ignored in the example)---and returns a pointer to the result. In the
current version of C, character pointers are the generic pointers, so
the input argument and the return value can be cast to char*.
Examples
3-3 Using registerrpc in the Main Body of a
Server Program
/* * nusers_server.c - server to return the number of users on a host */ #include <stdio.h> #include <rpc/rpc.h> /* required */ #include "rusers.h" /* for prog, vers definitions */ unsigned long *nuser(); main() { int exit(); registerrpc(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, nuser, xdr_void, xdr_u_long); svc_run(); /* Never returns */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1); } unsigned long * nuser(indata) char *indata; { static unsigned long nusers; /* * Add code here to compute the number of users * and place result in variable nusers. * For this example, nusers is set to 5. */ nusers = 5; return(&nusers); }
3.2.3 Using XDR Routines to Pass Arbitrary Data Types
RPC can handle arbitrary data structures---regardless of system
conventions for byte order and structure layout---by converting them to
their external data representation (XDR) before sending them over the
network. The process of converting from a particular system
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 one that you
supply. XDR has the built-in routines shown on the opposite page.
You cannot use the xdr_string routine with either callrpc or registerrpc, each of which passes only two parameters to an XDR routine. Instead, use xdr_wrapstring, which takes only two parameters and calls xdr_string.
Built-In XDR Integer Routines | ||
xdr_short | xdr_u_short | |
xdr_int | xdr_u_int | |
xdr_long | xdr_u_long | |
xdr_hyper | xdr_u_hyper | |
Built-In XDR Floating-Point Routines | ||
xdr_float | xdr_double | |
Built-In XDR Character Routines | ||
xdr_char | xdr_u_char | |
Built-In XDR Enumeration Routines | ||
xdr_bool | xdr_u_enum | |
Built-In XDR Array Routines | ||
xdr_array | xdr_bytes | |
xdr_vector | xdr_string | |
xdr_wrapstring | xdr_opaque | |
Built-In XDR Pointer Routines | ||
xdr_reference | xdr_pointer |
3.2.4 User-Defined XDR Routines
Suppose that you want to send the following structure:
struct simple { int a; short b; } simple;
To send it, you would use the following callrpc call:
callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_simple, &simple ...);
With this call to callrpc, you could define the routine xdr_simple as in the following example:
#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 (evaluates to TRUE in C) if it completes successfully; otherwise, it returns zero: For a complete description of XDR, see RFC 1014: XDR: External Data Representation Standard and Chapter 4 of this manual.
Note
It is best to use RPCGEN to generate XDR routines. Use the /XDR_FILE option of RPCGEN to generate only the _XDR.C file.
As another example, if you want to send a variable array of integers, you might package them as a structure like this:
struct varintarr { int *data; int arrlnth; } arr;
Then, you would make an RPC call such as this:
callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_varintarr, &arr, .....
You could then define xdr_varintarr as shown:
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 you know the size of the array in advance, you can use xdr_vector, which serializes fixed-length arrays, as shown in the following example:
int intarr[SIZE]; 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. If the examples in Section 3.2.4 had used characters instead of integers, each character would occupy 32 bits. This is why XDR has the built-in routine xdr_bytes, which is like xdr_array except that it packs characters. The xdr_bytes routine has four parameters, similar to the first four of xdr_array. For null-terminated strings, XDR provides the built-in routine xdr_string, which is the same as xdr_bytes without the length parameter.
When serializing, XDR gets the string length from strlen, and on deserializing it creates a null-terminated string. The following example calls the user-defined routine xdr_simple, as well as the built-in functions xdr_string and xdr_reference (which locates pointers):
struct finalexample { char *string; struct simple *simplep; } finalexample; xdr_finalexample(xdrsp, finalp) XDR *xdrsp; struct finalexample *finalp; { if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN)) return (0); if (!xdr_reference(xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple); return (0); return (1); }
Note that xdr_simple could be called here instead of xdr_reference.
3.3 Lowest Layer of RPC
Examples in previous sections show how RPC handles many details
automatically through defaults. The following sections describe how to
change the defaults by using the lowest layer RPC routines.
The lowest layer of RPC allows you to do the following:
The server for the nusers program in Examples
3-4 does the
same work as the previous nusers_server.c program that used
registerrpc (see Examples
3-3). However, it uses the lowest
layer of RPC.
Examples
3-4 Server Program Using Lowest Layer of
RPC
#include <stdio.h> #include <rpc/rpc.h> #include <rpc/pmap_clnt.h> #include "rusers.h" main() { SVCXPRT *transp; unsigned long nuser(); int exit(); transp = svcudp_create(RPC_ANYSOCK); (1) if (transp == NULL){ fprintf(stderr, "can't create an RPC server\n"); exit(1); } pmap_unset(RUSERSPROG, RUSERSVERS); (2) if (!svc_register(transp, RUSERSPROG, RUSERSVERS, (3) nuser, IPPROTO_UDP)) { fprintf(stderr, "can't register RUSER service\n"); exit(1); } svc_run(); /* Never returns */ (4) fprintf(stderr, "should never reach this point\n"); } unsigned long nuser(rqstp, transp) (5) struct svc_req *rqstp; SVCXPRT *transp; { unsigned long nusers; switch (rqstp->rq_proc) { case NULLPROC: if (!svc_sendreply(transp, xdr_void, 0)) fprintf(stderr, "can't reply to RPC call\n"); return; case RUSERSPROC_NUM: /* * Code here to compute the number of users * and assign it to the variable nusers * For this example, nusers is set to 5. */ nusers = 5; if (!svc_sendreply(transp, xdr_u_long, &nusers)) fprintf(stderr, "can't reply to RPC call\n"); return; default: svcerr_noproc(transp); return; } }
To show how a server handles an RPC program that receives data, you could add to the previous example, a procedure called RUSERSPROC_BOOL, which has an argument nusers, and returns TRUE or FALSE depending on whether the number of users logged on is equal to nusers. It would look like this:
case RUSERSPROC_BOOL: { int bool; unsigned nuserquery; if (!svc_getargs(transp, xdr_u_int, &nuserquery) { svcerr_decode(transp); return; } /* * Code to set nusers = number of users */ if (nuserquery == nusers) bool = TRUE; else bool = FALSE; if (!svc_sendreply(transp, xdr_bool, &bool)) fprintf(stderr, "can't reply to RPC call\n"); return; }
Here, the svc_getargs routine takes as arguments a server
handle, the XDR routine, and a pointer to where the input is to be
placed.
3.3.2 The Client Side and the Lowest RPC Layer
When you use callrpc, you cannot control either the RPC
delivery mechanism or the socket that transports the data. The lowest
layer of RPC enables you to modify these parameters, as shown in
Examples
3-5, which calls the nuser service.
Examples
3-5 Using Lowest RPC Layer to Control Data
Transport and Delivery
#include <stdio.h> #include <rpc/rpc.h> #include <sys/time.h> #include <netdb.h> #include "rusers.h" main(argc, argv) int argc; char **argv; { struct hostent *hp; struct timeval pertry_timeout, total_timeout; struct sockaddr_in server_addr; int sock = RPC_ANYSOCK; register CLIENT *client; enum clnt_stat clnt_stat; unsigned long nusers; int exit(); if (argc != 2) { fprintf(stderr, "usage: nusers hostname\n"); exit(-1); } if ((hp = gethostbyname(argv[1])) == NULL) { fprintf(stderr, "can't get addr for %s\n",argv[1]); exit(-1); } pertry_timeout.tv_sec = 3; pertry_timeout.tv_usec = 0; bcopy(hp->h_addr, (caddr_t)&server_addr.sin_addr, hp->h_length); server_addr.sin_family = AF_INET; server_addr.sin_port = 0; if ((client = clntudp_create(&server_addr, RUSERSPROG, (1) RUSERSVERS, pertry_timeout, &sock)) == NULL) { clnt_pcreateerror("clntudp_create"); exit(-1); } total_timeout.tv_sec = 20; total_timeout.tv_usec = 0; clnt_stat = clnt_call(client, RUSERSPROC_NUM, xdr_void, (2) 0, xdr_u_long, &nusers, total_timeout); if (clnt_stat != RPC_SUCCESS) { clnt_perror(client, "rpc"); exit(-1); } printf("%d users on %s\n", nusers, argv[1]); clnt_destroy(client); (3) exit(0); }
To make a stream connection, replace the call to clntudp_create with a call to clnttcp_create:
clnttcp_create(&server_addr, prognum, versnum, &sock, inbufsize, outbufsize);
Here, there is no timeout argument; instead, the "receive" and "send" buffer sizes must be specified. When the program makes a call to clnttcp_create, RPC creates a TCP client handle and establishes a TCP connection. All RPC calls using the client handle use the same TCP connection. The server side of an RPC call using TCP has svcudp_create replaced by svctcp_create:
transp = svctcp_create(RPC_ANYSOCK, 0, 0);
The last two arguments to svctcp_create are "send" and "receive" sizes, respectively. If, as here, 0 is specified for either of these, the system chooses default values.