The following is an example of the lowest layer of Remote Procedure Call (RPC) on the server and client side using the nusers program.
The server for the nusers program in the following example does the same thing as a program using the registerrpc subroutine at the highest level of RPC. However, the following is written using the lowest layer of the RPC package:
#include <stdio.h> #include <rpc/rpc.h> #include <utmp.h> #include <rpcsvc/rusers.h>
main() { SVCXPRT *transp; int nuser();
transp = svcudp_create(RPC_ANYSOCK); if (transp == NULL){ fprintf(stderr, "can't create an RPC server\n"); exit(1); } pmap_unset(RUSERSPROG, RUSERSVERS); if (!svc_register(transp, RUSERSPROG, RUSERSVERS,
nuser, IPPROTO_UDP)) { fprintf(stderr, "can't register RUSER service\n"); exit(1); } svc_run(); /* Never returns */ fprintf(stderr, "should never reach this point\n"); }
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 nusers variable */ if (!svc_sendreply(transp, xdr_u_long, &nusers)) fprintf(stderr, "can't reply to RPC call\n"); return; default: svcerr_noproc(transp); return; } }
First, the server gets a transport handle, which is used for receiving and replying to RPC messages. The registerrpc routine calls the svcudp_create routine to get a User Datagram Protocol (UDP) handle. If a more reliable protocol is required, the svctcp_create routine can be called instead. If the argument to the svcudp_create routine is RPC_ANYSOCK, the RPC library creates a socket on which to receive and reply to remote procedure calls. Otherwise, the svcudp_create routine expects its argument to be a valid socket number. If a programmer specifies a socket, it can be bound or unbound. If it is bound to a port by the programmer, the port numbers of the svcudp_create routine and the clnttcp_create routine (the low-level client routine) must match.
If the programmer specifies the RPC_ANYSOCK argument, the RPC library routines open sockets. The svcudp_create and clntudp_create routines cause the RPC library routines to bind the appropriate socket, if not already bound.
A service may register its port number with the local port mapper service. This is done by specifying a nonzero protocol number in the svc_register routine. A programmer at the client machine can discover the server port number by consulting the port mapper at the server workstation. This is done automatically by specifying a zero port number in the clntudp_create or clnttcp_create routines.
After creating a service transport (SVCXPRT) handle, the next step is to call the pmap_unset routine. If the nusers server crashed earlier, this routine erases any trace of the crash before restarting. Specifically, the pmap_unset routine erases the entry for RUSERSPROG from the port mapper's tables.
Finally, the program number for nusers is associated with the nuser procedure. The final argument to the svc_register routine is normally the protocol being used, in this case IPPROTO_UDP. Registration is performed at the program level, rather than the procedure level.
The nuser user service routine must call and dispatch the appropriate eXternal Data Representation (XDR) routines based on the procedure number. The nuser routine has two requirements, unlike the registerrpc routine which performs them automatically. The first is that the NULLPROC procedure (currently 0) return with no results. This is a simple test for detecting whether a remote program is running. Second, the subroutine checks for invalid procedure numbers. If one is detected, the svcerr_noproc routine is called to handle the error.
The user service routine serializes the results and returns them to the RPC caller through the svc_sendreply routine. The first parameter of this routine is the SVCXPRT handle, the second is the XDR routine that indicates return data type, and the third is a pointer to the data to be returned.
As an example, a RUSERSPROC_BOOL procedure can be added, which has an nusers argument and returns a value of True or False, depending on whether there are nusers logged on. The following example shows this addition:
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 (1); } return; }
The svc_getargs routine takes the following arguments: an SVCXPRT handle, the XDR routine, and a pointer that indicates where to place the input.
A programmer using the callrpc routine has control over neither the RPC delivery mechanism nor the socket used to transport the data. However, the lowest layer of RPC allows the user to adjust these parameters. The following code can be used to request the nusers service:
#include <stdio.h> #include <rpc/rpc.h> #include <utmp.h> #include <rpcsvc/rusers.h> #include <sys/socket.h> #include <sys/time.h> #include <netdb.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;
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, 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, 0, xdr_u_long, &nusers, total_timeout); if (clnt_stat != RPC_SUCCESS) { clnt_perror(client, "rpc"); exit(-1); } clnt_destroy(client); close(sock); exit(0); }
The low-level version of the callrpc routine is the clnt_call macro, which takes a CLIENT pointer rather than a host name. The parameters to the clnt_call macro are a CLIENT pointer, the procedure number, the XDR routine for serializing the argument, a pointer to the argument, the XDR routine for deserializing the return value, a pointer to where the return value is to be placed, and the total time in seconds to wait for a reply. Thus, the number of tries is the time out divided by the clntudp_create time out.
The CLIENT pointer is encoded with the transport mechanism. The callrpc routine uses UDP, thus it calls the clntudp_create routine to get a CLIENT pointer. To get Transmission Control Protocol (TCP), the programmer can call the clnttcp_create routine.
The parameters to the clntudp_create routine are the server address, the program number, the version number, a time-out value (between tries), and a pointer to a socket.
The clnt_destroy call always deallocates the space associated with the client handle. If the RPC library opened the socket associated with the client handle, the clnt_destroy macro closes it. If the socket was opened by the programmer, it stays open. In cases where there are multiple client handles using the same socket, it is possible to destroy one handle without closing the socket that other handles are using.
The stream connection is made when the call to the clntudp_create macro is replaced by a call to the clnttcp_create routine.
clnttcp_create(&server_addr, prognum, versnum, &sock, inputsize, outputsize);
In this example, no time-out argument exists. Instead, the send and receive buffer sizes must be specified. When the clnttcp_create call is made, a TCP connection is established. All remote procedure calls using the client handle use the TCP connection. The server side of a remote procedure call using TCP is similar, except that the svcudp_create routine is replaced by the svctcp_create routine, as follows:
transp = svctcp_create(RPC_ANYSOCK, 0, 0);
The last two arguments to the svctcp_create routine are send and receive sizes, respectively. If 0 is specified for either of these, the system chooses a reasonable default.