# Digital TCP/IP Services for OpenVMS
ONC RPC Programming

Previous | Contents

This example generates four output files using the default file names: PROTO.H, PROTO_CLNT.C, PROTO_SVC.C, and PROTO_XDR.C. INETd starts the server and the server exits after 20 seconds of inactivity. #3

    RPCGEN /HEADER_FILE /TABLE PROTO.X

> This example sends the header file (with support for dispatch tables) to the default output file PROTO.H.

#4

    RPCGEN /TRANSPORT=TCP PROTO.X

> This example sends the server skeleton file for the transport TCP to the default output file PROTO_SVC.C.

#5

    RPCGEN /HEADER_FILE /TABLE /OUTPUT=PROTO_TABLE.H PROTO.X

> This example sends the header file (with support for dispatch tables) to the output file PROTO_TABLE.H.

# 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:

- registerrpc, which obtains a unique systemwide procedure-identification number
- callrpc, which executes a remote procedure call
- svc_run, which calls a remote procedure in response to an RPC request

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:

- Timeout specifications
- Choice of transport
- Operating system process control
- Processing flexibility after occurrence of error
- Multiple kinds of call authentication

The lowest layer is suitable for programming tasks that require greater efficiency or flexibility. The lowest layer routines include client creation routines such as:

- clnt_create, which creates a client handle
- clnt_call, which calls the server
- svcudp_create, which creates a UDP server handle
- svc_register, which registers the server

# 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

### Examples

3-1, which obtains the number of remote users, shows the usage of callrpc.

The callrpc routine has eight parameters. In

### Examples

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, discussed in Section 3.3.

The remote server procedure corresponding to the callrpc usage example might look like the one in

### Examples

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*.

### Examples

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

### Examples

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

### Examples

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

### Examples

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*.

### Examples

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 on the opposite page.

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 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 (which 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:

- Use TCP as the underlying transport instead of UDP. Using TCP allows you to exceed the 8K-byte data limitation imposed by UDP.
- Allocate and free memory explicitly while serializing or deserializing with XDR routines.
- Use authentication on either the client or server side, through credential verification.

## 3.3.1 The Server Side and the Lowest RPC Layer

The server for the nusers program in

### Examples

3-4 does the same work as the previous nusers_server.c program that used registerrpc (see

### Examples

3-3). However, it uses the lowest layer of RPC.

1. (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. (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. (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 non-zero 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. (4) Finally, use a call to the svc_run routine to put the program into a wait state until RPC requests arrive.
5. (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 procedure NULLPROC (currently zero) returns with no results. This can be used as a simple test for detecting if a remote program is running.
   - There is a check for invalid procedure numbers; if the program detects one, it calls svcerr_noproc to handle the error.
   The nuser service routine serializes the results and returns them to the RPC client via 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.

   ### Examples

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

   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 returns TRUE or FALSE depending on whether the number of users logged on is equal to nusers. It would look like this:

            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.

   ## 3.3.2 The Client Side and the Lowest RPC Layer

   When you use callrpc, you cannot control either the RPC delivery mechanism or the socket that transports the data. The lowest layer of RPC enables you to modify these parameters, as shown in

   ### Examples

   3-5, which calls the nuser service.
   1. (1) This example calls the clntudp_create routine to get a client handle for the UDP transport. To get a TCP client handle you would use clnttcp_create. The parameters to clntudp_create are the server address, the program number, the version number, a timeout value, and a pointer to a socket. If the client does not hear from the server within the time specified in pertry_timeout, the request may be sent again to the server. When the sin_port is 0, RPC queries the remote Portmapper to find out the address of the remote service.
   2. (2) The lowest level version of callrpc is clnt_call, which takes a client handle rather than a host name. The parameters to clnt_call are a client handle, the procedure number, the XDR routine for serializing the argument, a pointer to the argument, the XDR routine for deserializing the results, a pointer to where the results will be placed, and the time in seconds to wait for a reply. The number of times that clnt_call attempts to contact the server is equal to the total_timeout value divided by the pertry_timeout value specified in the clntudp_create call.
   3. (3) The clnt_destroy call always deallocates the space associated with the CLIENT handle. It closes the socket associated with the CLIENT handle only if the RPC library opened it. If the socket was opened by the user, it remains open. This makes it possible, in cases where there are multiple client handles using the same socket, to destroy one handle without closing the socket that other handles are using.

   ### Examples

   3-5 Using Lowest RPC Layer to Control Data Transport and Delivery

       #include <stdio.h>
       #include <rpc/rpc.h>
       #include <sys/time.h>
       #include <netdb.h>
       #include "rusers.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;
            int exit();

            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, (1)
              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, (2)
              0, xdr_u_long, &nusers, total_timeout);

            if (clnt_stat != RPC_SUCCESS) {
                 clnt_perror(client, "rpc");
                 exit(-1);
            }

            printf("%d users on %s\n", nusers, argv[1]);
            clnt_destroy(client); (3)
            exit(0);
       }

   To make a stream connection, replace the call to clntudp_create with a call to clnttcp_create:

            clnttcp_create(&server_addr, prognum, versnum, &sock,
              inbufsize, outbufsize);

   Here, there is no timeout argument; instead, the "receive" and "send" buffer sizes must be specified. When the program makes a call to clnttcp_create, RPC creates a TCP client handle and establishes a TCP connection. All RPC calls using the client handle use the same TCP connection. The server side of an RPC call using TCP has svcudp_create replaced by svctcp_create:

                      transp = svctcp_create(RPC_ANYSOCK, 0, 0);

   The last two arguments to svctcp_create are "send" and "receive" sizes, respectively. If, as here, 0 is specified for either of these, the system chooses default values.

   Previous | Next | Contents