This section suggests some common RPC and rpcgen programming techniques.
Table 3–4 RPC Programming Techniques
Technique |
Description |
---|---|
Network type |
rpcgen can produce server code for specific transport types. |
You can define C-preprocessing symbols on rpcgen command lines. |
|
Servers need not send error replies to broadcast calls. |
|
Debug as normal function calls, then change to a distributed application. |
|
Port monitor support |
Port monitors can “listen” on behalf of RPC servers. |
Programs can access server dispatch tables. |
|
You can change client default time-out periods. |
|
Clients can authenticate themselves to servers; the appropriate servers can examine client authentication information. |
rpcgen takes optional arguments that enable a programmer to specify desired network types or specific network identifiers. For details of network selection, see Programming Interfaces Guide.
The -s flag creates a server that responds to requests
on the specified type of transport. For example, the command 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.
Be careful using servers created by rpcgen with the -n flag. Network identifiers are host specific, so the resulting server might not run as expected on other hosts.
You can define C-preprocessing symbols and assign values to them from the command line. Command-line define statements can be used to generate conditional debugging code when the DEBUG symbol is defined. For example:
$ rpcgen -DDEBUG proto.x
When a procedure has been called through broadcast RPC and cannot provide a useful response, the server should send no reply to the client, thus reducing 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 return and sends no reply.
The following code example is a procedure that replies only if it reaches an NFS server.
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 * then RPC sends a reply */ return( (void *) ¬null ); }
A procedure must return a non-NULL pointer when it is appropriate for RPC library routines to send a reply.
In the example, if the procedure reply_if_nfsserver() is defined to return nonvoid values, the return value ¬null should point to a static variable.
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.
Services might wait for a specified interval after completing a service request, in case another request follows. If no call arrives 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.
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, the server should exit immediately on completion.
By default, services created using rpcgen wait for 120 seconds after servicing a request before exiting. You can change the interval with the -K flag. In the following example, the server waits for 20 seconds before exiting. To create a server that exits immediately, you can use zero value for the interval period.
rpcgen -K 20 proto.x
rpcgen -K 0 proto.x
To create a server that never exits, the value is -K -1.
For more information on port monitors, see Appendix F, Writing a Port Monitor With the Service Access Facility (SAF).
After sending a request to the server, a client program waits for a default period (25 seconds) to receive a reply. You can change this timeout by using the clnt_control() routine. See Standard Interfaces for additional uses of the clnt_control() routine. See also the rpc(3NSL) man page. When considering time-out periods, be sure to allow the minimum amount of time required for “round-trip” communications over the network. The following code example illustrates the use of clnt_control().
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);
The client create routines do not have any facilities for client authentication. Some clients might 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.
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.
The following example is for a server that checks client authentication data. It is modified from printmessage_1() in rpcgen Tutorial. The code allows only superusers to print a message to the console.
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. */ }
Sometimes programs should have access to the dispatch tables used by the RPC package. For example, the server dispatch routine might check authorization and then invoke the service routine. Or, a client library might handle 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.” You can invoke rpcgen with the -t option to build only the header file. You cannot invoke rpcgen 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.
The find_proc() routine shows an example of how to locate a procedure in the dispatch tables.
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).
Using this technique, 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
Use the following define when writing the server.
#define RPCGEN_ACTION(routine)routine
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 4–byte quantity (as is an int); on a 64–bit system, a u_long is an 8-byte quantity. The data types rpcprog_t, rpcvers_t, rpc_proc_t, and rpcport_t, introduced in the Solaris 7 environment, should be used whenever possible in declaring RPC programs, versions, procedures, and ports in place of both u_long and long. These newer types provide backwards compatibility with 32–bit systems. They are guaranteed to be 4–byte quantities no matter which system rpcgen is run on. While rpcgen programs using u_long versions of programs, versions, and procedures can still run, they have different consequences on 32– and 64–bit machines. For that reason, replace them with the appropriate newer data types. In fact, avoid using long and u_long whenever possible.
Beginning with the Solaris 7 environment, source files created with rpcgen containing XDR routines use different inline macros depending on whether the code is to run on a 32–bit or 64–bit machine. Specifically, the source files will use 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, the resulting foo_xdr.c file ensures that the correct inline macro is used.
struct foo { char c; int i1; int i2; int i3; long l; short s; };
#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
The code declares buf to be either int or long, depending on whether the machine is 64–bit or 32–bit.
Currently, data types transported by using RPC are limited in size to 4-byte quantities (32 bits). The 8-byte long is provided to enable 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 previously, RPC programs, versions, procedures, and ports have their own dedicated types. xdr_long() fails if the data value is not between INT32_MIN and INT32_MAX. Also, the data could be truncated if inline macros such as IXDR_GET_LONG() and IXDR_PUT_LONG() are used. The same concerns apply to u_long variables. See also the xdr_long(3NSL) man page.
Only TI-RPC supports IPv6 transport. If an application is intended to run over IPv6, now or in the future, you must not use the backward compatibility switch. The selection of IPv4 or IPv6 is determined by the respective order of associated entries in /etc/netconfig.
To 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(3NSL) man page) and the authentication routines. Do not link with libnsl.
Link the procedures from the previous example by using the command 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. For details, see Testing Programs Using Low-Level Raw RPC for details.
Two kinds of errors 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 this problem are:
The procedure is not available
The remote server is not responding
The remote server is unable to decode the arguments.
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 you will be unable to link the client and server programs to each other if you are using the -C option, because of the -_svc suffix added to the server-side routines.