Compaq TCP/IP Services for OpenVMS
ONC RPC Programming


Previous Contents Index


Chapter 3
RPC Application Programming Interface

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:

3.2 Middle Layer of RPC

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 Example 3-1, which obtains the number of remote users, shows the usage of callrpc .

The callrpc routine has eight parameters. In Example 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, which is discussed in Section 3.3.

The remote server procedure corresponding to the callrpc usage example might look like the one in Example 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 * .

Example 3-1 Using callrpc

/* 
 * 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); 
} 

Example 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 while waiting to service requests. Using RPCGEN for this also generates a server dispatch function. You can write a server yourself by using registerrpc . Example 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 Example 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 * .

Example 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 in Table 3-1.

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 .

Table 3-1 XDR Routines
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)); 
} 

3.2.5 XDR Serializing Defaults

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 but 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 (the latter 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:

3.3.1 The Server Side and the Lowest RPC Layer

The server for the nusers program in Example 3-4 does the same work as the previous nusers_server.c program that used registerrpc (see Example 3-3). However, it uses the lowest layer of RPC.

Example 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; 
     } 
} 
 

In this example, the following events occur:

  1. The server calls svcudp_create to get a transport handle for receiving and replying to RPC messages. If the argument to svcudp_create is RPC_ANYSOCK , the RPC library creates a socket on which to receive and reply to RPC calls. Otherwise, svcudp_create expects its argument to be a valid socket number. If you specify your own socket, it can be bound or unbound. If it is bound to a port by the user, the port numbers of svcudp_create and clntudp_create (the low-level client routine) must match. The registerrpc routine uses svcudp_create to get a UDP handle. If you need a more reliable protocol, call svctcp_create instead.
  2. The next step is to call pmap_unset so if the nuser server crashed earlier, any previous trace of it is erased before restarting. More precisely, pmap_unset erases the entry for RUSERSPROG from the Portmapper tables.
  3. Use a call to svc_register to associate the program number RUSERSPROG and the version RUSERSVERS with the procedure nuser . Unlike registerrpc , there are no XDR routines in the registration process, and registration is at the program level rather than the procedure level.
    A service can register its port number with the local Portmapper service by specifying a nonzero protocol number in the final argument of svc_register . A client determines the server's port number by consulting the Portmapper on its server system. Specifying a zero port number in clntudp_create or clnttcp_create does this automatically.
  4. Finally, use a call to the svc_run routine to put the program into a wait state until RPC requests arrive.
  5. The server routine nuser must call and dispatch the appropriate XDR routines based on the procedure number. The nuser routine explicitly handles two cases that are taken care of automatically by registerrpc :
    The nuser service routine serializes the results and returns them to the RPC client using svc_sendreply . Its first parameter is the server handle, the second is the XDR routine, and the third is a pointer to the data to be returned. It is not necessary to have nusers declared as static here because the program calls svc_sendreply within that function itself.

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 which returns TRUE or FALSE depending on whether the number of users logged on is equal to nusers . For example:


     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.


Previous Next Contents Index