ONC+ Developer's Guide

rpcgen Programming Techniques

This section suggests some common RPC and rpcgen programming techniques. Each topic is covered in its own subsection.

Network Types/Transport Selection

rpcgen takes optional arguments that allow a programmer to specify desired network types or specific network identifiers. (For details of network selection, see Transport Interfaces Programming Guide).

The -s flag creates a server that responds to requests on the specified type of transport. For example, the invocation

rpcgen -s datagram_n prot.x

writes a server to standard output that responds to any of the connectionless transports specified in the NETPATH environment variable (or in /etc/netconfig, if NETPATH is not defined). A command line can contain multiple -s flags and their network types.

Similarly, the -n flag creates a server that responds only to requests from the transport specified by a single network identifier.


Caution - Caution -

Be careful using servers created by rpcgen with the -n flag. Network identifiers are host specific, so the resulting server may not run as expected on other hosts.


Command Line Define Statements

You can define C-preprocessing symbols and assign values to them from the command line. Command line define statements can, for example, be used to generate conditional debugging code when the DEBUG symbol is defined. For example:

$ rpcgen -DDEBUG proto.x

Server Response to Broadcast Calls

When a procedure has been called through broadcast RPC and cannot provide a useful response, the server should send no reply to the client. This reduces network traffic. To prevent the server from replying, a remote procedure can return NULL as its result. The server code generated by rpcgen detects this and sends no reply.

Example 3-23 is a procedure that replies only if it is an NFS server.


Example 3-23 NFS Server Response to Broadcast Calls

void *
reply_if_nfsserver()
{
	char notnull; /*only here so we can
						 *use its address
						 */

	if( access( "/etc/dfs/sharetab",
						F_OK ) < 0 ) {
		/* prevent RPC from replying */
		return( (void *) NULL );
	}
	/* assign notnull a non-null value
 * so RPC will send a reply
 */
	return( (void *) &notnull );
}

A procedure must return a non-NULL pointer when it wants RPC library routines to send a reply.

In Example 3-23, if the procedure reply_if_nfsserver() is defined to return non void values, the return value (&notnull) should point to a static variable.

Port Monitor Support

Port monitors such as inetd and listen can monitor network addresses for specified RPC services. When a request arrives for a particular service, the port monitor spawns a server process. After the call has been serviced, the server can exit. This technique conserves system resources. The main server function generated by rpcgen allows invocation by inetd. See "Using inetd" for details.

It may be useful for services to wait for a specified interval after satisfying a service request, in case another request follows. If there is no call in the specified time, the server exits, and some port monitors, like inetd, continue to monitor for the server. If a later request for the service occurs, the port monitor gives the request to a waiting server process (if any), rather than spawning a new process.


Note -

When monitoring for a server, some port monitors, like listen(), always spawn a new process in response to a service request. If a server is used with such a monitor, it should exit immediately on completion.


By default, services created using rpcgen wait for 120 seconds after servicing a request before exiting. The programmer can change the interval with the -K flag. In this example,

$ rpcgen -K 20 proto.x

the server waits for 20 seconds before exiting. To create a server that exits immediately, zero value can be used for the interval period:

$ rpcgen -K 0 proto.x.

To create a server that never exits, the value is -K -1.

Time-out Changes

After sending a request to the server, a client program waits for a default period (25 seconds) to receive a reply. This time-out may be changed using the clnt_control() routine. See "Standard Interfaces" for additional uses of the clnt_control() routine. See also the rpc(3N) manpage. When considering time-out periods, be sure to allow the minimum amount of time required for "round-trip" communications over the network. Example 3-24 illustrates the use of clnt_control().


Example 3-24 clnt_control Routine

struct timeval tv;
CLIENT *clnt;
clnt = clnt_create( "somehost", SOMEPROG,
									SOMEVERS, "visible" );

if (clnt == (CLIENT *)NULL)
	exit(1);
tv.tv_sec = 60;	/* change time-out to
							 * 60 seconds
							 */
tv.tv_usec = 0;
clnt_control(clnt, CLSET_TIMEOUT, &tv);

Client Authentication

The client create routines do not have any facilities for client authentication. Some clients may have to authenticate themselves to the server.

The following example illustrates one of the least secure authentication methods in common use. See "Authentication" for information on more secure authentication techniques.


Example 3-25 AUTH_SYS Authentication Program

CLIENT *clnt;
clnt = clnt_create( "somehost", SOMEPROG,
									SOMEVERS, "visible" );
if (clnt != (CLIENT *)NULL) {
	/* To set AUTH_SYS style authentication */
		clnt->cl_auth = authsys_createdefault();
}

Authentication information is important to servers that have to achieve some level of security. This extra information is supplied to the server as a second argument.

Example 3-26 is a server that checks client authentication data. It is modified from printmessage_1() in "An rpcgen Tutorial" and only allows superusers to print a message to the console.


Example 3-26 printmsg_1 for Superuser

int *
printmessage_1(msg, req)
	char **msg;
	struct svc_req  *req;
{
	static int result;	/* Must be static */
	FILE *f;
	struct authsys_parms *aup;

	aup = (struct authsys_parms *)req->rq_clntcred;
	if (aup->aup_uid != 0) {
		result = 0;
		return (&result)
	}

/* Same code as before. */
}

Dispatch Tables

It is sometimes useful for programs to have access to dispatch tables used by the RPC package. For example, the server dispatch routine may check authorization and then invoke the service routine; or a client library may deal with the details of storage management and XDR data conversion.

When invoked with the -T option, rpcgen generates RPC dispatch tables for each program defined in the protocol description file, proto.x, in the file proto_tbl.i. The suffix.i stands for "index." rpcgen may be invoked with the -t option to build only the header file. rpcgen cannot be invoked in C-style mode (-N) with either the -T or -t flag.

Each entry in the dispatch table is a struct rpcgen_table, defined in the header file proto.h as follows:

struct rpcgen_table {
   char *(*proc)();
   xdrproc_t xdr_arg;
   unsigned len_arg;
   xdrproc_t xdr_res;
   xdrproc_t len_res
};

where:

proc is a pointer to the service routine

xdr_arg is a pointer to the input (argument) xdr routine

len_arg is the length in bytes of the input argument

xdr_res is a pointer to the output (result) xdr routine

len_res is the length in bytes of the output result

The table, named dirprog_1_table for the dir.x example, is indexed by procedure number. The variable dirprog_1_nproc contains the number of entries in the table.

An example of how to locate a procedure in the dispatch tables is shown by the routine find_proc():


Example 3-27 Using a Dispatch Table

struct rpcgen_table *
find_proc(proc)
   rpcproc_t proc;
{
   if (proc >= dirprog_1_nproc)
       /* error */
   else
      return (&dirprog_1_table[proc]);
}

Each entry in the dispatch table contains a pointer to the corresponding service routine. However, that service routine is usually not defined in the client code. To avoid generating unresolved external references, and to require only one source file for the dispatch table, the rpcgen service routine initializer is RPCGEN_ACTION(proc_ver).

This way, the same dispatch table can be included in both the client and the server. Use the following define statement when compiling the client:

#define RPCGEN_ACTION(routine) 0

And use the following define when writing the server:

#define RPCGEN_ACTION(routine)routine

64-bit Considerations for rpcgen

Note that in Example 3-27 proc is declared as type rpcproc_t. Formerly, RPC programs, versions, procedures, and ports were declared to be of type u_long. On a 32-bit machine, a u_long is a four-byte quantity (as is an int); on a 64-bit system, a u_long is an eight-byte quantity. The data types rpcprog_t, rpcvers_t, rpc_proc_t, and rpcport_t - introduced in Solaris 7 - should be used whenever possible in declaring RPC programs, versions, procedures, and ports in place of both u_long and long. The reason is that these newer types provide backwards compatibility with 32-bit systems; they're guaranteed to be four-byte quantities no matter which system rpcgen is run on. While rpcgenprograms using u_longversions of programs, versions, and procedures will still run, they may have different consequences on 32- and 64-bit machines. For that reason it is a good idea to replace them with the appropriate newer data types. In fact, it is a good idea to avoid using long and u_long whenever possible (see the note below).

Beginning with Solaris 7, source files created byrpcgen, containing XDR routines, use different inline macros depending on whether the code is to run on a 32- or 64-bit machine - specifically, it uses the IXDR_GET_INT32() and IXDR_PUT_INT32() macros instead of IXDR_GETLONG() and IXDR_PUTLONG(). For example, if the rpcgen source file foo.x contains the following code

struct foo {
        char      c;
        int       i1;
        int       i2;
        int       i3;
        long      l;
        short     s;
};
the resulting foo_xdr.c file will ensure that the correct inline macro is used:
#if defined(_LP64) || defined(_KERNEL)
        register int *buf;
#else
        register long *buf;
#endif

. . .

#if defined(_LP64) || defined(_KERNEL)
                        IXDR_PUT_INT32(buf, objp->i1);
                        IXDR_PUT_INT32(buf, objp->i2);
                        IXDR_PUT_INT32(buf, objp->i3);
                        IXDR_PUT_INT32(buf, objp->l);
                        IXDR_PUT_SHORT(buf, objp->s);
#else
                        IXDR_PUT_LONG(buf, objp->i1);
                        IXDR_PUT_LONG(buf, objp->i2);
                        IXDR_PUT_LONG(buf, objp->i3);
                        IXDR_PUT_LONG(buf, objp->l);
                        IXDR_PUT_SHORT(buf, objp->s);
#endif
Note that the code declares buf to be either int or long, depending on whether the machine is 64- or 32-bit.


Note -

Currently, data types transported via RPC are limited in size to four-byte quantities (32 bits). The eight-byte long is provided to allow applications to make maximum use of 64-bit architecture. However, programmers should avoid using longs, and functions that use longs, such as x_putlong(), in favor of ints whenever possible. (As noted above, RPC programs, versions, procedures, and ports have their own dedicated types.) The reason is that xdr_long() will fail if the data value is not between INT32_MIN and INT32_MAX - or the data could be truncated if inline macros such as IXDR_GET_LONG() and IXDR_PUT_LONG() are used. (The same applies for u_longs.) See also the xdr_long(3NSL) man page.


IPv6 Considerations for RPCGEN

Only TI-RPC supports IPv6 transport. If an application is intended to run over IPv6, now or in the future, then the backward compatibility switch must not be used. The selection of IPv4 or IPv6 is determined by the respective order of associated entries in /etc/netconfig.

Debugging Applications

You can simplify the testing and debugging process. First test the client program and the server procedure in a single process by linking them with each other rather than with the client and server skeletons. Comment out calls to the client create RPC library routines (see the rpc_clnt_create(3N) manpage) and the authentication routines. Do not link with libnsl.

Link the procedures from previous example by:

cc rls.c dir_clnt.c dir_proc.c -o rls

With the RPC and XDR functions commented out, the procedure calls execute as ordinary local function calls, and the program is debugged with a local debugger such as dbxtool. When the program works, the client program is linked to the client skeleton produced by rpcgen and the server procedures are linked to the server skeleton produced by rpcgen.

You can also use the Raw RPC mode to test the XDR routines. See "Testing Programs Using Low-level Raw RPC" for details.

There are two kinds of errors that can happen in an RPC call. The first kind of error is caused by a problem with the mechanism of the remote procedure calls. Examples of these are (1) the procedure is not available, (2) the remote server is not responding, and (3) the remote server is unable to decode the arguments. In Example 3-26, an RPC error happens if result is NULL. The reason for the failure can be displayed by using clnt_perror(), or an error string can be returned through clnt_sperror().

The second type of error is caused by the server itself. In Example 3-26, an error can be returned by opendir(). The handling of these errors is application specific and is the responsibility of the programmer.

Note that the mechanism illustrated by the paragraphs above does not function with the -C option because of the _svc suffix added to the server-side routines.