This chapter addresses the C interface to RPC and describes how to write network applications using RPC. For a complete specification of the routines in the RPC library, see the rpc(3NSL) and related man pages.
The topics covered in this chapter include:
The client and server interfaces described in this chapter are multithread safe, except where noted (such as raw mode). This designation means that applications that contain RPC function calls can be used freely in a multithreaded application.
The simplified interface is the level to use if you do not require the use of any other RPC routines. This level also limits control of the underlying communications mechanisms. You can rapidly develop a program at this level, and the development is directly supported by the rpcgen compiler. For most applications, rpcgen and its facilities are sufficient.
Some RPC services are not available as C functions, but they are available as RPC programs. The simplified interface library routines provide direct access to the RPC facilities for programs that do not require fine levels of control. Routines such as rusers() are in the RPC services library librpcsvc. The following code example is a program that displays the number of users on a remote host. It calls the RPC library routine rusers().
#include <rpc/rpc.h> #include <rpcsvc/rusers.h> #include <stdio.h> /* * a program that calls the * rusers() service */ main(argc, argv) int argc; char **argv; { int num; if (argc != 2) { fprintf(stderr, "usage: %s hostname\n", argv[0]); exit(1); } if ((num = rnusers(argv[1])) < 0) { fprintf(stderr, "error: rusers\n"); exit(1); } fprintf(stderr, "%d users on %s\n", num, argv[1] ); exit(0);}
Compile the program in Example 4–1 by typing:
cc program.c -lrpcsvc -lnsl
Just one function exists on the client side of the simplified interface: rpc_call(). It has nine parameters:
int 0 or error code rpc_call ( char *host /* Name of server host */ rpcprog_t prognum /* Server program number */ rpcvers_t versnum /* Server version number */ rpcproc_t procnum /* Server procedure number */ xdrproc_t inproc /* XDR filter to encode arg */ char *in /* Pointer to argument */ xdr_proc_t outproc /* Filter to decode result */ char *out /* Address to store result */ char *nettype /*For transport selection */ );
The rpc_call() function calls the procedure specified by prognum, versum, and procnum on the host. The argument to be passed to the remote procedure is pointed to by the in parameter, and inproc is the XDR filter to encode this argument. The out parameter is an address where the result from the remote procedure is to be placed. outproc is an XDR filter that decodes the result and places it at this address.
The client blocks on rpc_call() until it receives a reply from the server. If the server accepts, it returns RPC_SUCCESS with the value of zero. The server returns a non-zero value if the call was unsuccessful. You can cast this value to the type clnt_stat, an enumerated type defined in the RPC include files and interpreted by the clnt_sperrno() function. This function returns a pointer to a standard RPC error message corresponding to the error code.
In the example, all “visible” transports listed in /etc/netconfig are tried. Adjusting the number of retries requires use of the lower levels of the RPC library.
Multiple arguments and results are handled by collecting them in structures.
The following code example changes the code in Example 4–1 to use the simplified interface.
#include <stdio.h> #include <utmpx.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 int nusers; enum clnt_stat cs; if (argc != 2) { fprintf(stderr, "usage: rusers hostname\n"); exit(1); } if( cs = rpc_call(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, (char *)0, xdr_u_int, (char *)&nusers, "visible") != RPC_SUCCESS ) { clnt_perrno(cs); exit(1); } fprintf(stderr, "%d users on %s\n", nusers, argv[1] ); exit(0); }
Data types can be represented differently on different machines. Therefore, rpc_call() needs both the type of the RPC argument and a pointer to it. rpc_call() also needs this information for the result. For RUSERSPROC_NUM, the return value is an unsigned int, so the first return parameter of rpc_call() is xdr_u_int, which is for an unsigned int, and the second return parameter is &nusers, which points to unsigned int storage. Because RUSERSPROC_NUM has no argument, the XDR encoding function of rpc_call() is xdr_void() and its argument is NULL.
The server program using the simplified interface is straightforward. The server calls rpc_reg() to register the procedure to be called. It then calls svc_run(), the RPC library's remote procedure dispatcher, to wait for requests to arrive.
rpc_reg() has the following arguments:
rpc_reg ( rpcprog_t prognum /* Server program number */ rpcvers_t versnum /* Server version number */ rpcproc_t procnum /* server procedure number */ char *procname /* Name of remote function */ xdrproc_t inproc /* Filter to encode arg */ xdrproc_t outproc /* Filter to decode result */ char *nettype /* For transport selection */ );
svc_run() invokes service procedures in response to RPC call messages. The dispatcher in rpc_reg() decodes remote procedure arguments and encodes results, using the XDR filters specified when the remote procedure was registered. Some notes about the server program include:
Most RPC applications follow the naming convention of appending a _1 to the function name. The sequence _n is added to the procedure names to indicate the version number n of the service.
The argument and result are passed as addresses. This is true for all functions that are called remotely. Passing NULL as a result of a function means that no reply is sent to the client, because NULL indicates that there is no reply to send.
The result must exist in static data space because its value is accessed after the actual procedure has exited. The RPC library function that builds the RPC reply message accesses the result and sends the value back to the client.
Only a single argument is allowed. If there are multiple elements of data, they should be wrapped inside a structure that can then be passed as a single entity.
The procedure is registered for each transport of the specified
type. If the type parameter is (char *)NULL
, the procedure is
registered for all transports specified in NETPATH.
You can sometimes implement faster or more compact code than can rpcgen. rpcgen handles the generic code-generation cases. The following program is an example of a hand-coded registration routine. It registers a single procedure and enters svc_run() to service requests.
#include <stdio.h> #include <rpc/rpc.h> #include <rpcsvc/rusers.h> void *rusers(); main() { if(rpc_reg(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, rusers, xdr_void, xdr_u_int, "visible") == -1) { fprintf(stderr, "Couldn't Register\n"); exit(1); } svc_run(); /* Never returns */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1); }
rpc_reg() can be called as many times as is needed to register different programs, versions, and procedures.
Data types passed to and received from remote procedures can be any of a set of predefined types, or can be programmer-defined types. RPC handles arbitrary data structures, regardless of the byte orders or structure layout conventions of different machines. RPC always converts these structures to a standard transfer format called external data representation (XDR) before sending them over the transport. The conversion from a machine representation to XDR is called serializing, and the reverse process is called deserializing.
The translator arguments of rpc_call() and rpc_reg() can specify an XDR primitive procedure, like xdr_u_int(), or a programmer-supplied routine that processes a complete argument structure. Argument processing routines must take only two arguments: a pointer to the result and a pointer to the XDR handle.
The XDR Primitive Type Routines are:
The fixed-width integer types found in int_types.h, the routines xdr_char(), xdr_short(), xdr_int(), and xdr_hyper() (and the unsigned versions of each) have equivalent functions with names familiar to ANSI C, as indicated in the following table:
Table 4–1 Primitive Type Equivalences
Function |
Equivalent |
---|---|
xdr_char() |
xdr_int8_t() |
xdr_u_char() |
xdr_u_int8_t() |
xdr_short() |
xdr_int16_t() |
xdr_u_short() |
xdr_u_int16_t() |
xdr_int() |
xdr_int32_t() |
xdr_u_int() |
xdr_u_int32_t() |
xdr_hyper() |
xdr_int64_t() |
xdr_u_hyper() |
xdr_u_int64_t() |
The nonprimitive xdr_string(), which takes more than two parameters, is called from xdr_wrapstring().
The following example of a programmer-supplied routine contains the calling arguments of a procedure.
struct simple { int a; short b; } simple;
The XDR routine xdr_simple() translates the argument structure as shown in the following code example.
#include <rpc/rpc.h> #include "simple.h" 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); }
rpcgen can automatically generate an equivalent routine.
An XDR routine returns nonzero (a C TRUE) if it completes successfully, and zero otherwise. A complete description of XDR is provided in Appendix C, XDR Protocol Specification.
The following list shows prefabricated routines:
For example, to send a variable-sized array of integers, the routine is packaged in a structure containing the array and its length:
struct varintarr { int *data; int arrlnth; } arr;
Translate the array with xdr_varintarr(), as shown in the following code example.
bool_t xdr_varintarr(xdrsp, arrp) XDR *xdrsp; struct varintarr *arrp; { return(xdr_array(xdrsp, (caddr_t)&arrp->data, (u_int *)&arrp->arrlnth, MAXLEN, sizeof(int), xdr_int)); }
The arguments of xdr_array() are the XDR handle, a pointer to the array, a pointer to the size of the array, the maximum array size, the size of each array element, and a pointer to the XDR routine to translate each array element. If the size of the array is known in advance, use xdr_vector(), as shown in the following code example.
int intarr[SIZE]; bool_t xdr_intarr(xdrsp, intarr) XDR *xdrsp; int intarr[]; { return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int)); }
XDR converts quantities to 4-byte multiples when serializing. For arrays of characters, each character occupies 32 bits. xdr_bytes() packs characters. It has four parameters similar to the first four parameters of xdr_array().
Null-terminated strings are translated by xdr_string(), which is like xdr_bytes() with no length parameter. On serializing xdr_string() gets the string length from strlen(), and on deserializing it creates a null-terminated string.
The following example calls the built-in functions xdr_string() and xdr_reference(), which translates pointers to pass a string, and struct simple from previous examples.
struct finalexample { char *string; struct simple *simplep; } finalexample; 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); }
Note that xdr_simple() could have been called here instead of xdr_reference().
Interfaces to standard levels of the RPC package provide increasing control over RPC communications. Programs that use this control are more complex. Effective programming at these lower levels requires more knowledge of computer network fundamentals. The top, intermediate, expert, and bottom levels are part of the standard interfaces.
This section describes how to control RPC details by using lower levels
of the RPC library. For example, you can select the transport protocol, which
can be done at the simplified interface level only through the NETPATH
variable. You should be familiar with
the top-level interface (TLI) in order to use these routines.
The routines shown below cannot be used through the simplified interface because they require a transport handle. For example, there is no way to allocate and free memory while serializing or deserializing with XDR routines at the simplified interface.
At the top level, the application can specify the type of transport to use but not the specific transport. This level differs from the simplified interface in that the application creates its own transport handles in both the client and server.
Assume the header file in the following code example.
/* time_prot.h */ #include <rpc/rpc.h> #include <rpc/types.h> struct timev { int second; int minute; int hour; }; typedef struct timev timev; bool_t xdr_timev(); #define TIME_PROG 0x40000001 #define TIME_VERS 1 #define TIME_GET 1
The following example shows the client side of a trivial date service using top-level service routines. The transport type is specified as an invocation argument of the program.
#include <stdio.h> #include "time_prot.h" #define TOTAL (30) /* * Caller of trivial date service * usage: calltime hostname */ main(argc, argv) int argc; char *argv[]; { struct timeval time_out; CLIENT *client; enum clnt_stat stat; struct timev timev; char *nettype; if (argc != 2 && argc != 3) { fprintf(stderr,”usage:%s host[nettype]\n” ,argv[0]); exit(1); } if (argc == 2) nettype = "netpath"; /* Default */ else nettype = argv[2]; client = clnt_create(argv[1], TIME_PROG, TIME_VERS, nettype); if (client == (CLIENT *) NULL) { clnt_pcreateerror(“Couldn't create client”); exit(1); } time_out.tv_sec = TOTAL; time_out.tv_usec = 0; stat = clnt_call( client, TIME_GET, xdr_void, (caddr_t)NULL, xdr_timev, (caddr_t)&timev, time_out); if (stat != RPC_SUCCESS) { clnt_perror(client, "Call failed"); exit(1); } fprintf(stderr,"%s: %02d:%02d:%02d GMT\n", nettype timev.hour, timev.minute, timev.second); (void) clnt_destroy(client); exit(0); }
If nettype is not specified in the invocation
of the program, the string netpath is substituted.
When RPC libraries routines encounter this string, the value of the NETPATH
environment variable governs transport
selection.
If the client handle cannot be created, display the reason for the failure with clnt_pcreateerror(). You can also get the error status by reading the contents of the global variable rpc_createerr.
After the client handle is created, clnt_call() is used to make the remote call. Its arguments are the remote procedure number, an XDR filter for the input argument, the argument pointer, an XDR filter for the result, the result pointer, and the time-out period of the call. The program has no arguments, so xdr_void() is specified. Clean up by calling clnt_destroy().
To bound the time allowed for client handle creation in the previous example to 30 seconds, replace the call to clnt_create() with a call to clnt_create_timed() as shown in the following code segment:
struct timeval timeout; timeout.tv_sec = 30; /* 30 seconds */ timeout.tv_usec = 0; client = clnt_create_timed(argv[1], TIME_PROG, TIME_VERS, nettype, &timeout);
The following example shows a top-level implementation of a server for the trivial date service.
#include <stdio.h> #include <rpc/rpc.h> #include "time_prot.h" static void time_prog(); main(argc,argv) int argc; char *argv[]; { int transpnum; char *nettype; if (argc > 2) { fprintf(stderr, "usage: %s [nettype]\n", argv[0]); exit(1); } if (argc == 2) nettype = argv[1]; else nettype = "netpath"; /* Default */ transpnum = svc_create(time_prog,TIME_PROG,TIME_VERS,nettype); if (transpnum == 0) { fprintf(stderr,”%s: cannot create %s service.\n”, argv[0], nettype); exit(1); } svc_run(); } /* * The server dispatch function */ static void time_prog(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { struct timev rslt; time_t thetime; switch(rqstp->rq_proc) { case NULLPROC: svc_sendreply(transp, xdr_void, NULL); return; case TIME_GET: break; default: svcerr_noproc(transp); return; } thetime = time((time_t *) 0); rslt.second = thetime % 60; thetime /= 60; rslt.minute = thetime % 60; thetime /= 60; rslt.hour = thetime % 24; if (!svc_sendreply( transp, xdr_timev, &rslt)) { svcerr_systemerr(transp); } }
svc_create() returns the number of transports on which it created server handles. time_prog() is the service function called by svc_run() when a request specifies its program and version numbers. The server returns the results to the client through svc_sendreply().
When you use rpcgen to generate the dispatch function, svc_sendreply() is called after the procedure returns. Therefore, rslt in this example must be declared static in the actual procedure. svc_sendreply() is called from inside the dispatch function, so rslt is not declared static.
In this example, the remote procedure takes no arguments. When arguments must be passed, the calls listed below fetch, deserialize (XDR decode), and free the arguments.
svc_getargs( SVCXPRT_handle, XDR_filter, argument_pointer); svc_freeargs( SVCXPRT_handle, XDR_filter argument_pointer );
At the intermediate level, the application directly chooses the transport to use.
The following example shows the client side of the time service from Top-Level Interface, written at the intermediate level of RPC. In this example, the user must name the transport over which the call is made on the command line.
#include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> /* For netconfig structure */ #include "time_prot.h" #define TOTAL (30) main(argc,argv) int argc; char *argv[]; { CLIENT *client; struct netconfig *nconf; char *netid; /* Declarations from previous example */ if (argc != 3) { fprintf(stderr, "usage: %s host netid\n”, argv[0]); exit(1); } netid = argv[2]; if ((nconf = getnetconfigent( netid)) == (struct netconfig *) NULL) { fprintf(stderr, "Bad netid type: %s\n", netid); exit(1); } client = clnt_tp_create(argv[1], TIME_PROG, TIME_VERS, nconf); if (client == (CLIENT *) NULL) { clnt_pcreateerror("Could not create client"); exit(1); } freenetconfigent(nconf); /* Same as previous example after this point */ }
In this example, the netconfig structure is obtained by a call to getnetconfigent(netid). See the getnetconfig(3NSL) man page and Programming Interfaces Guide for more details. At this level, the program explicitly selects the network.
To bound the time allowed for client handle creation in the previous example to 30 seconds, replace the call to clnt_tp_create() with a call to clnt_tp_create_timed() as shown in the following code segment:
struct timeval timeout; timeout.tv_sec = 30; /* 30 seconds */ timeout.tv_usec = 0; client = clnt_tp_create_timed(argv[1], TIME_PROG, TIME_VERS, nconf, &timeout);
The following example shows the corresponding server. The command line that starts the service must specify the transport over which the service is provided.
/* * This program supplies Greenwich mean * time to the client that invokes it. * The call format is: server netid */ #include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> /* For netconfig structure */ #include "time_prot.h" static void time_prog(); main(argc, argv) int argc; char *argv[]; { SVCXPRT *transp; struct netconfig *nconf; if (argc != 2) { fprintf(stderr, "usage: %s netid\n", argv[0]); exit(1); } if ((nconf = getnetconfigent( argv[1])) == (struct netconfig *) NULL) { fprintf(stderr, "Could not find info on %s\n", argv[1]); exit(1); } transp = svc_tp_create(time_prog, TIME_PROG, TIME_VERS, nconf); if (transp == (SVCXPRT *) NULL) { fprintf(stderr, "%s: cannot create %s service\n", argv[0], argv[1]); exit(1) } freenetconfigent(nconf); svc_run(); } static void time_prog(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { /* Code identical to Top Level version */
At the expert level, network selection is done the same as at the intermediate level. The only difference is in the increased level of control that the application has over the details of the CLIENT and SVCXPRT handles. These examples illustrate this control, which is exercised using the clnt_tli_create() and svc_tli_create() routines. For more information on TLI, see Programming Interfaces Guide.
Example 4–12 shows a version of clntudp_create(), the client creation routine for UDP transport, using clnt_tli_create(). The example shows how to do network selection based on the family of the transport you choose. clnt_tli_create() is used to create a client handle and to:
#include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> #include <netinet/in.h> /* * In earlier implementations of RPC, * only TCP/IP and UDP/IP were supported. * This version of clntudp_create() * is based on TLI/Streams. */ CLIENT * clntudp_create(raddr, prog, vers, wait, sockp) struct sockaddr_in *raddr; /* Remote address */ rpcprog_t prog; /* Program number */ prcvers_t vers; /* Version number */ struct timeval wait; /* Time to wait */ int *sockp; /* fd pointer */ { CLIENT *cl; /* Client handle */ int madefd = FALSE; /* Is fd opened here */ int fd = *sockp; /* TLI fd */ struct t_bind *tbind; /* bind address */ struct netconfig *nconf; /* netconfig structure */ void *handlep; if ((handlep = setnetconfig() ) == (void *) NULL) { /* Error starting network configuration */ rpc_createerr.cf_stat = RPC_UNKNOWNPROTO; return((CLIENT *) NULL); } /* * Try all the transports until it gets one that is * connectionless, family is INET, and preferred name is UDP */ while (nconf = getnetconfig( handlep)) { if ((nconf->nc_semantics == NC_TPI_CLTS) && (strcmp( nconf->nc_protofmly, NC_INET ) == 0) && (strcmp( nconf->nc_proto, NC_UDP ) == 0)) break; } if (nconf == (struct netconfig *) NULL) rpc_createerr.cf_stat = RPC_UNKNOWNPROTO; goto err; } if (fd == RPC_ANYFD) { fd = t_open(nconf->nc_device, O_RDWR, &tinfo); if (fd == -1) { rpc_createerr.cf_stat = RPC_SYSTEMERROR; goto err; } } if (raddr->sin_port == 0) { /* remote addr unknown */ u_short sport; /* * rpcb_getport() is a user-provided routine that calls * rpcb_getaddr and translates the netbuf address to port * number in host byte order. */ sport = rpcb_getport(raddr, prog, vers, nconf); if (sport == 0) { rpc_createerr.cf_stat = RPC_PROGUNAVAIL; goto err; } raddr->sin_port = htons(sport); } /* Transform sockaddr_in to netbuf */ tbind = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR); if (tbind == (struct t_bind *) NULL) rpc_createerr.cf_stat = RPC_SYSTEMERROR; goto err; } if (t_bind->addr.maxlen < sizeof( struct sockaddr_in)) goto err; (void) memcpy( tbind->addr.buf, (char *)raddr, sizeof(struct sockaddr_in)); tbind->addr.len = sizeof(struct sockaddr_in); /* Bind fd */ if (t_bind( fd, NULL, NULL) == -1) { rpc_createerr.ct_stat = RPC_TLIERROR; goto err; } cl = clnt_tli_create(fd, nconf, &(tbind->addr), prog, vers, tinfo.tsdu, tinfo.tsdu); /* Close the netconfig file */ (void) endnetconfig( handlep); (void) t_free((char *) tbind, T_BIND); if (cl) { *sockp = fd; if (madefd == TRUE) { /* fd should be closed while destroying the handle */ (void)clnt_control(cl,CLSET_FD_CLOSE, (char *)NULL); } /* Set the retry time */ (void) clnt_control( l, CLSET_RETRY_TIMEOUT, (char *) &wait); return(cl); } err: if (madefd == TRUE) (void) t_close(fd); (void) endnetconfig(handlep); return((CLIENT *) NULL); }
The network is selected using setnetconfig(), getnetconfig(), and endnetconfig(). endnetconfig() is not called until after the call to clnt_tli_create(), near the end of the example.
clntudp_create() can be passed an open TLI fd. If passed none (fd == RPC_ANYFD), clntudp_create() opens its own using the netconfig structure for UDP to find the name of the device to pass to t_open().
If the remote address is not known (raddr->sin_port == 0), it is obtained from the remote rpcbind.
After the client handle has been created, you can modify it using calls to clnt_control(). The RPC library closes the file descriptor when destroying the handle, as it does with a call to clnt_destroy() when it opens the fd itself. The RPC library then sets the retry timeout period.
Example 4–13 shows the server side of Example 4–12. It is called svcudp_create(). The server side uses svc_tli_create().
svc_tli_create() is used when the application needs a fine degree of control, particularly to:
Set the send and receive buffer sizes. The fd argument can be unbound when passed in. It is then bound to a given address, and the address is stored in a handle. If the bind address is set to NULL and the fd is initially unbound, it is bound to any suitable address.
Use rpcb_set() to register the service with rpcbind.
#include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> #include <netinet/in.h> SVCXPRT * svcudp_create(fd) register int fd; { struct netconfig *nconf; SVCXPRT *svc; int madefd = FALSE; int port; void *handlep; struct t_info tinfo; /* If no transports available */ if ((handlep = setnetconfig() ) == (void *) NULL) { nc_perror("server"); return((SVCXPRT *) NULL); } /* * Try all the transports until it gets one which is * connectionless, family is INET and, name is UDP */ while (nconf = getnetconfig( handlep)) { if ((nconf->nc_semantics == NC_TPI_CLTS) && (strcmp( nconf->nc_protofmly, NC_INET) == 0 )&& (strcmp( nconf->nc_proto, NC_UDP) == 0 )) break; } if (nconf == (struct netconfig *) NULL) { endnetconfig(handlep); return((SVCXPRT *) NULL); } if (fd == RPC_ANYFD) { fd = t_open(nconf->nc_device, O_RDWR, &tinfo); if (fd == -1) { (void) endnetconfig(); return((SVCXPRT *) NULL); } madefd = TRUE; } else t_getinfo(fd, &tinfo); svc = svc_tli_create(fd, nconf, (struct t_bind *) NULL, tinfo.tsdu, tinfo.tsdu); (void) endnetconfig(handlep); if (svc == (SVCXPRT *) NULL) { if (madefd) (void) t_close(fd); return((SVCXPRT *)NULL); } return (svc); }
The network selection here is accomplished similar to clntudp_create(). The file descriptor is not bound explicitly to a transport address because svc_tli_create() does that.
svcudp_create() can use an open fd. It opens one itself using the selected netconfig structure if none is provided.
The bottom-level interface to RPC enables the application to control all options. clnt_tli_create() and the other expert-level RPC interface routines are based on these routines. You rarely use these low-level routines.
Bottom-level routines create internal data structures, buffer management, RPC headers, and so on. Callers of these routines, like the expert-level routine clnt_tli_create(), must initialize the cl_netid and cl_tp fields in the client handle. For a created handle, cl_netid is the network identifier (for example, udp) of the transport and cl_tp is the device name of that transport (for example, /dev/udp). The routines clnt_dg_create() and clnt_vc_create() set the clnt_ops and cl_private fields.
The following code example shows calls to clnt_vc_create() and clnt_dg_create().
/* * variables are: * cl: CLIENT * * tinfo: struct t_info returned from either t_open or t_getinfo * svcaddr: struct netbuf * */ switch(tinfo.servtype) { case T_COTS: case T_COTS_ORD: cl = clnt_vc_create(fd, svcaddr, prog, vers, sendsz, recvsz); break; case T_CLTS: cl = clnt_dg_create(fd, svcaddr, prog, vers, sendsz, recvsz); break; default: goto err; }
These routines require that the file descriptor be open and bound. svcaddr is the address of the server.
The following code example is an example of creating a bottom-level server.
/* * variables are: * xprt: SVCXPRT * */ switch(tinfo.servtype) { case T_COTS_ORD: case T_COTS: xprt = svc_vc_create(fd, sendsz, recvsz); break; case T_CLTS: xprt = svc_dg_create(fd, sendsz, recvsz); break; default: goto err; }
svc_dg_enablecache() initiates service caching for datagram transports. Caching should be used only in cases where a server procedure is a “once only” kind of operation. Executing a cached server procedure multiple times yields different results.
svc_dg_enablecache(xprt, cache_size) SVCXPRT *xprt; unsigned int cache_size;
This function allocates a duplicate request cache for the service endpoint xprt, large enough to hold cache_size entries. A duplicate request cache is needed if the service contains procedures with varying results. After caching is enabled, it cannot be disabled.
The following data structure information is for reference only. The implementation might change.
The first structure is the client RPC handle, defined in <rpc/clnt.h>. Low-level implementations must provide and initialize one handle per connection, as shown in the following code example.
typedef struct { AUTH *cl_auth; /* authenticator */ struct clnt_ops { enum clnt_stat (*cl_call)(); /* call remote procedure */ void (*cl_abort)(); /* abort a call */ void (*cl_geterr)(); /* get specific error code */ bool_t (*cl_freeres)(); /* frees results */ void (*cl_destroy)(); /* destroy this structure */ bool_t (*cl_control)(); /* the ioctl() of rpc */ } *cl_ops; caddrt_t cl_private; /* private stuff */ char *cl_netid; /* network token */ char *cl_tp; /* device name */ } CLIENT;
The first field of the client-side handle is an authentication structure, defined in <rpc/auth.h>. By default, this field is set to AUTH_NONE. A client program must initialize cl_auth appropriately, as shown in the following code example.
typedef struct { struct opaque_auth ah_cred; struct opaque_auth ah_verf; union des_block ah_key; struct auth_ops { void (*ah_nextverf)(); int (*ah_marshal)(); /* nextverf & serialize */ int (*ah_validate)(); /* validate varifier */ int (*ah_refresh)(); /* refresh credentials */ void (*ah_destroy)(); /* destroy this structure */ } *ah_ops; caddr_t ah_private; } AUTH;
In the AUTH structure, ah_cred contains the caller's credentials, and ah_verf contains the data to verify the credentials. See Authentication for details.
The following code example shows the server transport handle.
typedef struct { int xp_fd; #define xp_sock xp_fd u_short xp_port; /* associated port number. Obsoleted */ struct xp_ops { bool_t (*xp_recv)(); /* receive incoming requests */ enum xprt_stat (*xp_stat)(); /* get transport status */ bool_t (*xp_getargs)(); /* get arguments */ bool_t (*xp_reply)(); /* send reply */ bool_t (*xp_freeargs)(); /* free mem alloc for args */ void *xp_destroy)(); /* destroy this struct */ } *xp_ops; int xp_addrlen; /* length of remote addr. Obsolete */ char *xp_tp; /* transport provider device name */ char *xp_netid; /* network token */ struct netbuf xp_ltaddr; /* local transport address */ struct netbuf xp_rtaddr; /* remote transport address */ char xp_raddr[16]; /* remote address. Now obsoleted */ struct opaque_auth xp_verf; /* raw response verifier */ caddr_t xp_p1; /* private: for use by svc ops */ caddr_t xp_p2; /* private: for use by svc ops */ caddr_t xp_p3; /* private: for use by svc lib */ } SVCXPRT;
The following table shows the fields for the server transport handle.
xp_fd |
The file descriptor associated with the handle. Two or more server handles can share the same file descriptor. |
xp_netid |
The network identifier (for example, udp) of the transport on which the handle is created and xp_tp is the device name associated with that transport. |
xp_ltaddr |
The server's own bind address. |
xp_rtaddr |
The address of the remote caller (and so can change from call to call). |
xp_netid xp_tp xp_ltaddr |
Initialized by svc_tli_create() and other expert-level routines. |
The rest of the fields are initialized by the bottom-level server routines svc_dg_create() and svc_vc_create().
For connection-oriented endpoints, the following fields are not valid until a connection has been requested and accepted for the server:
Two pseudo-RPC interface routines bypass all the network software. The routines shown in clnt_raw_create() and svc_raw_create() do not use any real transport.
Do not use raw mode on production systems. Raw mode is intended as a debugging aid only. Raw mode is not MT safe.
The following code example is compiled and linked using the following makefile:
all: raw CFLAGS += -g raw: raw.o cc -g -o raw raw.o -lnsl
/* * A simple program to increment a number by 1 */ #include <stdio.h> #include <rpc/rpc.h> #include <rpc/raw.h> #define prognum 0x40000001 #define versnum 1 #define INCR 1 struct timeval TIMEOUT = {0, 0}; static void server(); main (argc, argv) int argc; char **argv; { CLIENT *cl; SVCXPRT *svc; int num = 0, ans; int flag; if (argc == 2) num = atoi(argv[1]); svc = svc_raw_create(); if (svc == (SVCXPRT *) NULL) { fprintf(stderr, "Could not create server handle\n"); exit(1); } flag = svc_reg( svc, prognum, versnum, server, (struct netconfig *) NULL ); if (flag == 0) { fprintf(stderr, "Error: svc_reg failed.\n"); exit(1); } cl = clnt_raw_create( prognum, versnum ); if (cl == (CLIENT *) NULL) { clnt_pcreateerror("Error: clnt_raw_create"); exit(1); } if (clnt_call(cl, INCR, xdr_int, (caddr_t) &num, xdr_int, (caddr_t) &ans, TIMEOUT) != RPC_SUCCESS) { clnt_perror(cl, "Error: client_call with raw"); exit(1); } printf("Client: number returned %d\n", ans); exit(0); } static void server(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { int num; fprintf(stderr, "Entering server procedure.\n"); switch(rqstp->rq_proc) { case NULLPROC: if (svc_sendreply( transp, xdr_void, (caddr_t) NULL) == FALSE) { fprintf(stderr, "error in null proc\n"); exit(1); } return; case INCR: break; default: svcerr_noproc(transp); return; } if (!svc_getargs( transp, xdr_int, &num)) { svcerr_decode(transp); return; } fprintf(stderr, "Server procedure: about to increment.\n"); num++; if (svc_sendreply(transp, xdr_int, &num) == FALSE) { fprintf(stderr, "error in sending answer\n"); exit (1); } fprintf(stderr, "Leaving server procedure.\n"); }
Note the following points about the example:
The server must be created before the client.
svc_raw_create() has no parameters.
The server is not registered with rpcbind.
The last parameter to svc_reg() is (struct
netconfig *) NULL.
svc_run() is not called.
All the RPC calls occur within the same thread of control.
Example 4–20 copies a file from one host to another. The RPC send() call reads standard input and sends the data to the server receive(), which writes the data to standard output. This example also illustrates an XDR procedure that behaves differently on serialization and on deserialization. A connection-oriented transport is used.
/* * The xdr routine: * on decode, read wire, write to fp * on encode, read fp, write to wire */ #include <stdio.h> #include <rpc/rpc.h> bool_t xdr_rcp(xdrs, fp) XDR *xdrs; FILE *fp; { unsigned long size; char buf[BUFSIZ], *p; if (xdrs->x_op == XDR_FREE) /* nothing to free */ return(TRUE); while (TRUE) { if (xdrs->x_op == XDR_ENCODE) { if ((size = fread( buf, sizeof( char ), BUFSIZ, fp)) == 0 && ferror(fp)) { fprintf(stderr, "can't fread\n"); return(FALSE); } else return(TRUE); } p = buf; if (! xdr_bytes( xdrs, &p, &size, BUFSIZ)) return(0); if (size == 0) return(1); if (xdrs->x_op == XDR_DECODE) { if (fwrite( buf, sizeof(char), size, fp) != size) { fprintf(stderr, "can't fwrite\n"); return(FALSE); } else return(TRUE); } } }
In Example 4–21 and Example 4–22, the serializing and deserializing are done only by the xdr_rcp() routine shown in Example 4–20.
/* The sender routines */ #include <stdio.h> #include <netdb.h> #include <rpc/rpc.h> #include <sys/socket.h> #include <sys/time.h> #include "rcp.h" main(argc, argv) int argc; char **argv; { int xdr_rcp(); if (argc != 2 7) { fprintf(stderr, "usage: %s servername\n", argv[0]); exit(1); } if( callcots( argv[1], RCPPROG, RCPPROC, RCPVERS, xdr_rcp, stdin, xdr_void, 0 ) != 0 ) exit(1); exit(0); } callcots(host, prognum, procnum, versnum, inproc, in, outproc, out) char *host, *in, *out; xdrproc_t inproc, outproc; { enum clnt_stat clnt_stat; register CLIENT *client; struct timeval total_timeout; if ((client = clnt_create( host, prognum, versnum, "circuit_v") == (CLIENT *) NULL)) { clnt_pcreateerror("clnt_create"); return(-1); } total_timeout.tv_sec = 20; total_timeout.tv_usec = 0; clnt_stat = clnt_call(client, procnum, inproc, in, outproc, out, total_timeout); clnt_destroy(client); if (clnt_stat != RPC_SUCCESS) clnt_perror("callcots"); return((int)clnt_stat); }
The following code example defines the receiving routines. Note that in the server, xdr_rcp() did all the work automatically.
/* * The receiving routines */ #include <stdio.h> #include <rpc/rpc.h #include "rcp.h" main() { void rcp_service(); if (svc_create(rpc_service,RCPPROG,RCPVERS,"circuit_v") == 0) { fprintf(stderr, "svc_create: errpr\n"); exit(1); } svc_run(); /* never returns */ fprintf(stderr, "svc_run should never return\n"); } void rcp_service(rqstp, transp) register struct svc_req *rqstp; register SVCXPRT *transp; { switch(rqstp->rq_proc) { case NULLPROC: if (svc_sendreply(transp, xdr_void, (caddr_t) NULL) == FALSE) fprintf(stderr, "err: rcp_service"); return; case RCPPROC: if (!svc_getargs( transp, xdr_rcp, stdout)) { svcerr_decode(transp); return(); } if(!svc_sendreply(transp, xdr_void, (caddr_t) NULL)) { fprintf(stderr, "can't reply\n"); return(); } return(); default: svcerr_noproc(transp); return(); } }
XDR routines normally serialize and deserialize data. XDR routines often
automatically allocate memory and free automatically allocated memory. The
convention is to use a NULL
pointer to an array or structure
to indicate that an XDR function must allocate memory when deserializing.
The next example, xdr_chararr1(), processes a fixed array
of bytes with length SIZE and cannot allocate memory if
needed:
xdr_chararr1(xdrsp, chararr) XDR *xdrsp; char chararr[]; { char *p; int len; p = chararr; len = SIZE; return (xdr_bytes(xdrsp, &p, &len, SIZE)); }
If space has already been allocated in chararr, it can be called from a server as follows.
char chararr[SIZE]; svc_getargs(transp, xdr_chararr1, chararr);
Any structure through which data is passed to XDR or RPC routines must be allocated so that its base address is at an architecture-dependent boundary. An XDR routine that does the allocation must be written so that it can:
Allocate memory when a caller requests
Return the pointer to any memory it allocates
In the following example, the second argument is a NULL pointer, meaning that memory should be allocated to hold the data being deserialized.
xdr_chararr2(xdrsp, chararrp) XDR *xdrsp; char **chararrp; { int len; len = SIZE; return (xdr_bytes(xdrsp, charrarrp, &len, SIZE)); }
The corresponding RPC call is:
char *arrptr; arrptr = NULL; svc_getargs(transp, xdr_chararr2, &arrptr); /* * Use the result here */ svc_freeargs(transp, xdr_chararr2, &arrptr);
After use, free the character array through svc_freeargs(). svc_freeargs() does nothing if passed a NULL pointer as its second argument.
To summarize: