Chapter 6. RPC Programming Guide

This chapter is for programmers who want to write network applications using RPC. For most applications, you can use the rpcgen compiler, thus avoiding the need to understand much of the information in this chapter. (Chapter 5, “Programming with rpcgen”, contains the source for a working RPC service, which uses rpcgen to generate XDR routines and client and server stubs.)

Topics in this chapter include:

The Layers of RPC

This section presents detailed information about programming in the three RPC layers (see Chapter 4, “Introduction to RPC Programming”, for background information about the RPC layers).

The Highest Layer of RPC

The highest layer of RPC is transparent to the operating system, machine, and network upon which it is run and consists of RPC library-based services. Suppose you're writing a program that needs to know how many users are logged into a remote machine.

You can do this by calling the RPC library routine rnusers(), as shown in this code fragment:

/*
 * howmany.c
 */

#include <stdio.h>

main(int argc, char **argv)
{
    int num;

    if (argc != 2) {
        fprintf(stderr, "usage: howmany hostname\n");
        exit(1);
    }
    if ((num = rnusers(argv[1])) < 0) {
        fprintf(stderr, "error: howmany\n");
        exit(1);
    }
    printf("%d users on %s\n", num, argv[1]);
    exit(0);
}

RPC library routines in C, such as rnusers(), are included in the DSO librpcsvc.so. (For more information about DSOs, see the IRIX System Programming Guide.) Thus, you can compile the above program with cc:

% cc howmany.c -lrpcsvc -o howmany 


Note: See “Compiling BSD and RPC Programs” in Chapter 1 for other compiling hints.


The Middle Layer of RPC

The middle layer of RPC consists of routines used for most applications. In this layer, the user can make remote procedure calls to routines on other machines without considering details about the socket interface, the UNIX system, or other low-level implementation mechanisms.

The simplest interface, which explicitly makes RPC calls, uses the callrpc() and registerrpc() functions. Another way to determine the number of remote users is shown in this example, which can be compiled in the same way as the previous example:

/*
 * howmany2.c
 */

#include <stdio.h>
#include <rpc/rpc.h>
#include <rpcsvc/rusers.h>

main(int argc, char **argv)
{
    unsigned long nusers;
    int stat;
    if (argc != 2) {
        fprintf(stderr, "usage: howmany2 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); 
}

Each RPC procedure is uniquely defined by a program number, version number, and procedure number (see “Assigning RPC Program Numbers” in Chapter 4 for details). The program number specifies a group of related remote procedures, each of which has a different procedure number. Each program also has a version number, so when a minor change is made to a remote service (such as adding a new procedure), a new program number doesn't have to be assigned.

The simplest way to make a remote procedure call is with the callrpc() routine. callrpc() has eight parameters:

  • The first parameter is the name of the remote server machine.

  • The next three parameters identify the procedure to be called and consist of the program, version, and procedure numbers.

  • The fifth parameter is an XDR filter.

  • The sixth parameter is an argument to be encoded and passed to the remote procedure.

  • The seventh parameter is a filter for decoding the results returned by the remote procedure.

  • The last parameter is a pointer to the place where the procedure's results are to be stored.

Multiple arguments and results are handled by embedding them in structures. If callrpc() completes successfully, it returns zero; otherwise, it returns a nonzero value. The return codes (of type cast into an integer) are found in <rpc/clnt.h>.

Since data types may be represented differently on different machines, callrpc() needs both the type of the RPC argument and a pointer to the argument itself (and similarly for the result). For RUSERSPROC_NUM, the return value is an unsigned long. So, callrpc() has xdr_u_long as its first return parameter, which says that the result is of type unsigned long, and &nusers as its second return parameter, which is a pointer to where the long result will be placed. Since RUSERSPROC_NUM takes no argument, the argument parameter of callrpc() is xdr_void.

After trying several times to deliver a message, if callrpc() gets no answer, it returns with an error code. The delivery mechanism is UDP. Methods for adjusting the number of retries or for using a different protocol require you to use the lowest layer of the RPC library (see “The Lowest Layer of RPC”).

The remote server procedure corresponding to the preceding example might look like this:

void *nuser(indata)
char *indata; 
{
    static int nusers;
    /* Code here to compute the number of users
     * and place result in variable nusers.
     */
    return ((void *)&nusers); 
}

It takes one argument, which is a pointer to the input of the remote procedure call (ignored in the example), and it returns a pointer to the result.

Normally, a server registers all of the RPC calls it plans to handle and then goes into an infinite loop waiting to service requests. In this example, there is only a single procedure to register, so the main body of the server looks like this:

#include <stdio.h>
#include <rpc/rpc.h>
#include <rpcsvc/rusers.h>

void *nuser();

main()
{
    registerrpc(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM,
                nuser, xdr_void, xdr_u_long);
    svc_run();                    /* never returns */
    fprintf(stderr, "Error: svc_run returned!\n");
    exit(1); 
}

The registerrpc() routine establishes what C procedure corresponds to each RPC procedure number. The first three parameters—RUSERSPROG, RUSERSVERS, and RUSERSPROC_NUM—are the program, version, and procedure numbers of the remote procedure to be registered; nuser is the name of the C procedure implementing it; 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.)

Only the UDP transport mechanism can use registerrpc(); thus, registerrpc() is always safe in conjunction with calls generated by callrpc().


Note: The UDP transport mechanism can only deal with arguments and results that are less than 8 kilobytes long.

After registering the local procedure, the server program's main procedure calls
svc_run(), the RPC library's remote procedure dispatcher. It is this function that calls the remote procedures in response to RPC call messages. Note that the dispatcher takes care of decoding remote procedure arguments and encoding results, using the XDR filters specified when the remote procedure was registered.

Passing Arbitrary Data Types

In the previous example, the RPC call passes a single unsigned long. RPC can handle arbitrary data structures, regardless of different machines' byte order or structure-layout conventions, by always converting them to XDR before sending them over the network. (The process of converting from a particular machine representation to XDR format is called serializing, and the reverse is called deserializing.)

The type field parameters passed to callrpc() and registerrpc() can be built-in procedures like xdr_u_long() or user-supplied procedures. XDR has the following built-in type routines that can be used with callrpc() and registerrpc():

xdr_int()        xdr_u_int()        xdr_enum()
xdr_long()       xdr_u_long()       xdr_bool()
xdr_short()      xdr_u_short()      xdr_wrapstring()
xdr_char()       xdr_u_char()

Note that the routine xdr_string() exists but cannot be used with callrpc() and registerrpc(), which pass only two parameters to their XDR routines. xdr_wrapstring() has only two parameters and is thus okay; it calls xdr_string().

This is an example of a user-defined type routine:

struct simple {
    int a;
    short b;
} simple;

If you want to send and receive this structure, call callrpc() like this:

callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_simple,
        &simple, xdr_simple, &simple);

In this case, xdr_simple() is written as:

#include <rpc/rpc.h>
xdr_simple(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 a nonzero value (which means “true” in C) if it completes successfully; zero otherwise.


Note: This section gives only a few examples of implementing XDR. For more information, see Chapter 8, “XDR Programming Notes”.

In addition to the built-in routines, XDR has these prefabricated building blocks:

xdr_array()     xdr_bytes()        xdr_reference()
xdr_vector()    xdr_union()        xdr_pointer()
xdr_string()    xdr_opaque()

To send a variable array of integers, you might package them as a structure:

struct varintarr {
    int *data;
    int arrlength;
} arr; 

Next, you could make an RPC call something like this:

callrpc(hostname, PROGNUM, VERSNUM, PROCNUM, xdr_varintarr,
        &arr, xdr_varintarr, &arr);

In this case, xdr_varintarr() is defined as:

xdr_varintarr(XDR *xdrsp, struct varintarr *arrp)
{
    return (xdr_array(xdrsp, &arrp->data, &arrp->arrlength,
            MAXLEN, sizeof(int), xdr_int)); 
} 

This 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 the size of the array is known in advance, you can use xdr_vector(), which serializes fixed-length arrays:

int intarr[SIZE];
xdr_intarr(XDR *xdrsp, int intarr[])
{
    int i;
    return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int),
                       xdr_int));
}

XDR always converts quantities to four-byte multiples when it is serializing. Thus, if either of the preceding examples involved characters instead of integers, each character would occupy 32 bits, which is the reason for the xdr_bytes() routine. xdr_bytes() is like xdr_array(), except it packs characters; xdr_bytes() has four parameters, similar to the first four parameters of xdr_array().

For null-terminated strings, there is also the xdr_string() routine. xdr_string() is the same as xdr_bytes() without the length parameter. When serializing, the string length is taken from strlen(); when deserializing, a null-terminated string is created.

In this final example of the middle layer, a call is made to the previously written xdr_simple(), as well as to the built-in functions xdr_string() and xdr_reference():

struct finalexample {
    char *string;
    struct simple *simplep;
} finalexample;

xdr_finalexample(XDR *xdrsp, struct finalexample *finalp)
{
    int i;
    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 you could just as easily call xdr_simple() instead of xdr_reference().

The Lowest Layer of RPC

The lowest layer of RPC is used for more-sophisticated applications. In this section, you'll see how to change defaults by using the lowest layer of the RPC library.


Note: This section assumes that you are familiar with socket-related concepts and the socket library (see Chapter 2, “Sockets-based Communication”).

You may need to use the lowest layer of RPC in one of the following instances:

  • To use TCP (the highest layer uses UDP, which restricts RPC calls to 8 kilobytes of data). Using TCP permits calls to send long streams of data (see “TCP”).

  • To allocate and free memory while serializing or deserializing with XDR routines. There is no call at the highest level to let you free memory explicitly. See “Memory Allocation with XDR”.

  • To perform authentication on either the client or server side, by supplying credentials or verifying them. See “Authentication”.

More Information about the Server

There are a number of assumptions built into registerrpc(). One is that you are using the UDP datagram protocol. Another is that you don't want to do anything unusual while deserializing, since deserialization is automatic and occurs before the user's server routine is called.

The server for the following program is written using the lowest layer of RPC, which does not make these assumptions:

#include <stdio.h>
#include <rpc/rpc.h>
#include <utmp.h>
#include <rpcsvc/rusers.h> 

main()
{
    SVCXPRT *transp;
    void 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");
    exit(1);
} 

void nuser(rqstp, transp)
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");
            exit(1);
        }
        return;
    case RUSERSPROC_NUM:
        /* Code here to compute the number of users and
         * assign to the variable nusers
         */
        if (!svc_sendreply(transp, xdr_u_long, &nusers)) {
            fprintf(stderr, "can't reply to RPC call\n");
            exit(1);
        }
        return;
    default:
        svcerr_noproc(transp);
        return;
    } 
}

In this example, the server gets a transport handle, which is used for sending RPC messages. registerrpc() uses svcudp_create() to get a UDP handle. If you require a reliable protocol, call svctcp_create() instead. If the argument to svcudp_create() is RPC_ANYSOCK, the RPC library creates a socket on which to send out 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.

If you specify RPC_ANYSOCK for a socket, the RPC library routines will open sockets. Otherwise, they will expect the caller to do so. The svcudp_create() and clntudp_create() routines will cause RPC library routines to bind their sockets if they are not bound already.

A service may choose to register its port number with the local port mapper service. This is done by specifying a nonzero protocol number in svc_register(). Incidentally, a client can discover the server's port number by consulting the port mapper on the server's machine. This can be done automatically by specifying a zero port number in clntudp_create() or clnttcp_create().

After creating a SVCXPRT, the next step is to call pmap_unset() so that if the nusers server crashed earlier, any previous trace of it is erased before restarting. More precisely, pmap_unset() erases the entry for RUSERS from the port mapper's tables.

Finally, you associate the program number for nusers with the procedure nuser(). The final argument to svc_register() is normally the protocol being used, which in this case is IPPROTO_UDP. Notice that unlike registerrpc(), there are no XDR routines involved in the registration process. In addition, registration is done on the program level rather than the procedure level.

The user routine nuser() must call and dispatch the appropriate XDR routines based on the procedure number. Note that two things are handled by nuser() that registerrpc() handles automatically:

  • A simple test to detect whether a remote program is running: call procedure NULLPROC (currently zero), which returns with no arguments.

  • A check for invalid procedure numbers. If one is detected, svcerr_noproc() is called to handle the error.

The user service routine serializes the results and returns them to the RPC caller via svc_sendreply(). Its first parameter is the SVCXPRT handle, the second parameter is the XDR routine, and the third parameter is a pointer to the data to be returned.

Not illustrated previously is how a server handles an RPC program that passes data. For example, we could add a procedure RUSERSPROC_BOOL, which has an argument nusers, and returns TRUE or FALSE, depending on whether the number of users logged in is equal to nusers. The procedure looks something like this:

case RUSERSPROC_BOOL: {
    int bool;
    unsigned nuserquery;
    
    if (!svc_getargs(transp, xdr_u_int, &nuserquery) {
        svcerr_decode(transp);
        return;
    }
    /* Insert code here 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");
        exit(1);
    }
    return;
}

The relevant routine is svc_getargs(), which takes a SVCXPRT handle, the XDR routine, and a pointer to where the input is to be placed as arguments.

More Information about the Client

When you use callrpc(), you have no control over the RPC delivery mechanism or the socket used to transport the data. To illustrate how the lowest layer of RPC lets you adjust these parameters, consider the following code sample, which calls the nusers service:

/*
 * howmany3.c
 */
#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(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: howmany3 hostname\n");
        exit(1);
    }
    if ((hp = gethostbyname(argv[1])) == NULL) {
        herror(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);
    }

    printf("%d users on %s\n", nusers, argv[1]);

    clnt_destroy(client);
    close(sock);
    exit(0);
}

The low-level version of callrpc() is clnt_call(), which takes a CLIENT pointer rather than a host name. The parameters to clnt_call() 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 will be placed, and the time in seconds to wait for a reply.

The CLIENT pointer is encoded with the transport mechanism. callrpc() uses UDP; thus it calls clntudp_create() to get a CLIENT pointer. To specify TCP/IP, use clnttcp_create().

The parameters to clntudp_create() are the server address, the program number, the version number, a timeout value (how long to wait before trying again), and a pointer to a socket. The final argument to clnt_call() is the total time to wait for a response. Thus, the number of tries is the clnt_call() timeout divided by the clntudp_create() timeout.

Note that the clnt_destroy() call always deallocates the space associated with the CLIENT handle. It closes the socket associated with the CLIENT handle, however, only if the RPC library opened it. If the socket was opened by the user, it stays 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.

To make a stream connection, the call to clntudp_create() is replaced with a call to clnttcp_create()

clnttcp_create(&server_addr, prognum, versnum, &socket,
               inputsize, outputsize);

There is no timeout argument; instead, the receive and send buffer sizes must be specified. When the clnttcp_create() call is made, a TCP connection is established. All RPC calls using that CLIENT handle use this 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 0 is specified for either argument, the system chooses a reasonable default.

Memory Allocation with XDR

In addition to input and output, XDR routines do memory allocation. For this reason, the second parameter of xdr_array() is a pointer to an array, rather than the array itself. If it is NULL, xdr_array() allocates space for the array and returns a pointer to it, putting the size of the array in the third argument. For example, consider the following XDR routine, xdr_chararr1(), which deals with a fixed array of bytes with length SIZE:

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

char chararr[SIZE];
svc_getargs(transp, xdr_chararr1, chararr);

In this case, chararr has already allocated space.

If you want XDR to do the allocation, you have to rewrite the routine; for example:

xdr_chararr2(XDR *xdrsp, char **chararrp)
{
    int len;
    len = SIZE;
    return (xdr_bytes(xdrsp, charrarrp, &len, SIZE));
}

The RPC call might then look like this:

char *arrptr;
arrptr = NULL;
svc_getargs(transp, xdr_chararr2, &arrptr);
/* Use the result here */
svc_freeargs(transp, xdr_chararr2, &arrptr);

Note that after being used, the character array can be freed with svc_freeargs(), which will not attempt to free any memory if the variable indicating memory is NULL. For example, in the routine xdr_finalexample() (described in “Passing Arbitrary Data Types”), if finalp->string is NULL, it is not freed. The same is true for finalp–>simplep.

To summarize, each XDR routine is responsible for serializing, deserializing, and freeing memory. When an XDR routine is called from callrpc(), the serializer is used. When called from svc_getargs(), the deserializer is used. When called from svc_freeargs(), the memory deallocator is used.

When building simple examples like the ones in this section, a user doesn't have to worry about the three modes. Chapter 8, “XDR Programming Notes”, provides examples of more sophisticated XDR routines that determine which of the three modes they are in order to function correctly.

Other RPC Features

This section discusses some other aspects of RPC that can be useful to RPC programmers.

Select on the Server Side

Suppose a process is processing RPC requests while performing some other activity. If the other activity involves periodically updating a data structure, the process can set an alarm signal before calling svc_run(). But if the other activity involves waiting on a file descriptor, the svc_run() call won't work. The following is the code for svc_run():

void svc_run()
{
    fd_set readfds;
    int dtbsz = getdtablesize();
    for (;;) {
        readfds = svc_fdset;
        switch (select(dtbsz, &readfds, NULL,NULL,NULL)) {
        case -1:
            if (errno == EINTR)
                continue;
            perror("select");
            return;
        case 0:
            break;
        default:
            svc_getreqset(&readfds);
        }
    }
}

You can bypass svc_run() and call svc_getreqset() yourself. All you need to know are the file descriptors of the sockets associated with the programs you are waiting on. Thus, you can have your own select() that waits on both the RPC socket and your own descriptors. Note that svc_fdset is a bit mask of all the file descriptors that RPC is using for services. It can change whenever any RPC library routine is called, because descriptors are constantly being opened and closed, such as for TCP connections.

Broadcast RPC

You cannot do broadcast RPC without a port mapper, which converts RPC program numbers into UDP or TCP port numbers; see portmap(1M) or rpcbind(1M) for more information.

The main differences between broadcast RPC and normal RPC calls are:

  • Normal RPC expects one answer, whereas broadcast RPC expects many answers (one or more answers from each responding machine).

  • Broadcast RPC can be supported only by packet-oriented (connectionless) transport protocols, such as UDP/IP.

  • The implementation of broadcast RPC treats all unsuccessful responses as garbage by filtering them out. Thus, if there is a version mismatch between the broadcaster and a remote service, the user of broadcast RPC never knows.

  • All broadcast messages are sent to the port mapper port. Thus, only services that register themselves with their port mapper are accessible via the broadcast RPC mechanism.

  • Broadcast requests are limited in size to the Maximum Transfer Unit (MTU) of the local network. For Ethernet, the MTU is 1500 bytes. For FDDI, the MTU is 4352 bytes.

Broadcast RPC Synopsis

The following is the synopsis of broadcast RPC:

#include <rpc/pmap_clnt.h>
enum clnt_stat  clnt_stat;
    . . .
clnt_stat = clnt_broadcast(prognum, versnum, procnum,
            inproc, in, outproc, out, eachresult)
        u_long    prognum;     /* program number */
        u_long    versnum;     /* version number */
        u_long    procnum;     /* procedure number */
        xdrproc_t inproc;      /* xdr routine for args */
        void      *in;         /* pointer to args */
        xdrproc_t outproc;      /* xdr routine for results */
        void      *out;        /* pointer to results */
        bool_t  (*eachresult)();
                          /* call with each result gotten */
clnt_stat = clnt_broadcast_exp(prognum, versnum, procnum,
                               inproc, in, outproc, out,
                               eachresult,inittime,waittime)
        /* first eight parameters same as above. */
        int    inittime;        /* initial wait period */
        int    waittime;        /* total wait period */ 

The procedure eachresult() is called each time a valid result is obtained. It returns a boolean that indicates whether or not the client wants more responses:

bool_t done;

done = eachresult(resultsp, raddr)
       void *resultsp;
       struct sockaddr_in *raddr;
       /* address of machine that sent response */

If done is TRUE, broadcasting stops and clnt_broadcast() returns successfully. Otherwise, the routine waits for another response. The request is rebroadcast after a few seconds of waiting. If no responses come back, the routine returns with RPC_TIMEDOUT. Use clnt_broadcast_exp() to control the initial and total waiting intervals. To interpret clnt_stat errors, feed the error code to clnt_perrno().

Batching

The RPC architecture is designed so that a client sends a call to a server and waits for a reply that the call succeeded. Clients do not compute while servers are processing a call, which is inefficient if the client does not want or need an acknowledgment for every message sent. RPC batch facilities make it possible for clients to continue computing while waiting for a response.

Batching occurs when RPC messages are placed in a “pipeline” of calls to a server. Batching assumes that:

  • Each RPC call in the pipeline does not require a response from the server, and the server does not send a response message.

  • The pipeline of calls is transported on a reliable byte stream transport such as TCP/IP.

Since the server does not respond to every call, the client can generate new calls in parallel, with the server executing previous calls. In addition, the TCP/IP implementation can buffer many calls and send them to the server in a single write system call.

This overlapped execution greatly decreases the inter-process communication overhead of the client and server processes, and the total elapsed time of a series of calls.

Since the batched calls are buffered, the client should eventually do a nonbatched call to flush the pipeline.

The following is a (contrived) example of batching. Assume that a string–rendering service (such as a window system) has two similar calls: one renders a string and returns void results, while the other renders a string and remains silent. The service (using the TCP/IP transport) could look something like this:

#include <stdio.h>
#include <rpc/rpc.h>
#include <rpcsvc/windows.h>  /* assumes this files exists
                              * and defines all the
                              * necessary constants.
                              */

void windowdispatch();

main()
{
    SVCXPRT *transp;

    transp = svctcp_create(RPC_ANYSOCK, 0, 0);
    if (transp == NULL) {
        fprintf(stderr, "can't create an RPC server\n");
        exit(1);
    }
    pmap_unset(WINDOWPROG, WINDOWVERS);
    if (!svc_register(transp, WINDOWPROG, WINDOWVERS,
                      windowdispatch, IPPROTO_TCP)) {
        fprintf(stderr, "can't register WINDOW service\n");
        exit(1);
    }
    svc_run();            /* never returns */
    fprintf(stderr, "should never reach this point\n");
    exit(1);
}

void windowdispatch(rqstp, transp)
struct svc_req *rqstp;
SVCXPRT *transp;
{
    char *s = NULL;
    switch (rqstp->rq_proc) {
    case NULLPROC:
        if (!svc_sendreply(transp, xdr_void, 0)) {
            fprintf(stderr, "can't reply to RPC call\n");
            exit(1);
        }
        return;
    case RENDERSTRING:
        if (!svc_getargs(transp, xdr_wrapstring, &s)) {
            fprintf(stderr, "can't decode arguments\n");
            /* tell caller it messed up */
            svcerr_decode(transp);
            break;
        } 
        /* Code here to render the string s */
        if (!svc_sendreply(transp, xdr_void, NULL)) {
            fprintf(stderr, "can't reply to RPC call\n");
            exit(1);
        }
        break;
    case RENDERSTRING_BATCHED:
        if (!svc_getargs(transp, xdr_wrapstring, &s)) {
            fprintf(stderr, "can't decode arguments\n");
            /* We are silent in face of protocol errors */
            break;
        }
        /*Code here to render string s, but send no reply!*/
        break;
    default:
        svcerr_noproc(transp);
        return;
    }
    /* Now free string allocated while decoding arguments */
    svc_freeargs(transp, xdr_wrapstring, &s); 
}

Of course, the service could have one procedure that takes the string and a boolean to indicate whether or not the procedure should respond.

In order for a client to take advantage of batching, the client must perform RPC calls on a TCP-based transport, and the actual calls must have these attributes:

  • The result's XDR routine must be zero (NULL).

  • The RPC call's timeout must be zero.

The following is an example of a client that uses batching to render a collection of strings; the batching is flushed when the client gets a null string:

#include <stdio.h>
#include <rpc/rpc.h>
#include <rpcsvc/windows.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>

main(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;
    char buf[1000], *s = buf;
    if ((client = clnttcp_create(&server_addr,
         WINDOWPROG, WINDOWVERS, &sock, 0, 0)) == NULL) {
        perror("clnttcp_create");

        exit(1);
    }
    total_timeout.tv_sec = 0;
    total_timeout.tv_usec = 0;
    while (scanf("%s", s) != EOF) {
        clnt_stat = clnt_call(client, RENDERSTRING_BATCHED,
xdr_wrapstring, &s, NULL, NULL, total_timeout);
        if (clnt_stat != RPC_SUCCESS) {
           clnt_perror(client, "batched rpc");
           exit(1);
        }
    }

    /* Now flush the pipeline */

    total_timeout.tv_sec = 20;
    clnt_stat = clnt_call(client, NULLPROC, xdr_void, NULL,
                          xdr_void, NULL, total_timeout);
    if (clnt_stat != RPC_SUCCESS) {
        clnt_perror(client, "rpc");
        exit(1);
    }
    clnt_destroy(client);
    exit(0); 
}

Since the server does not send a message, the clients cannot be notified of any failures that occur. Therefore, clients are on their own when it comes to handling errors.

Authentication

In the examples presented so far, the caller never identifies itself to the server, and the server never requires an ID from the caller. Clearly, some network services, such as a network filesystem, require stronger security.

The Silicon Graphics RPC authentication subsystem provides two protocols: AUTH_NONE and AUTH_UNIX. AUTH_UNIX provides only for identification of the client in the RPC call, while AUTH_NONE, the default, turns the subsystem off. Therefore, for these two protocols, no authentication is actually performed by the RPC code or library code prior to calling the program's implementation function.


Note: For these two protocols, authentication is entirely the responsibility of the server application program. Applications should not rely on the veracity of the identification information in the AUTH_UNIX protocol without first taking steps to authenticate. An example of how to implement an authentication procedure is provided in “Server-side Authentication”.

The authentication subsystem of the RPC package is open-ended. That is, other types of authentication are easy to support. However, this section deals only with the AUTH_UNIX authentication type, which is the only supported type other than AUTH_NONE.

Client-side Authentication

In this example, a caller creates a new RPC client handle:

clnt = clntudp_create(address, prognum, versnum, wait, sockp)

The appropriate transport instance defaults to the associated authentication protocol:

clnt->cl_auth = authnone_create();

The RPC client can choose to use the AUTH_UNIX protocol by setting clnt–>cl_auth after creating the RPC client handle:

clnt->cl_auth = authunix_create_default();

This code causes each RPC call associated with clnt to carry with it this AUTH_UNIX protocol credentials structure:

/*
* AUTH_UNIX protocol credentials
*/
struct authunix_parms {
    u_long aup_time;        /* credentials creation time */
    char   *aup_machname;   /* host name where client is */
    int    aup_uid;         /* client's UNIX effective uid */
    int    aup_gid;         /* client's current group ID */
    u_int  aup_len;         /* element length of aup_gids */
    int    *aup_gids;       /* array of groups user in */
};

These fields are set by authunix_create_default() when you invoke the appropriate system calls.

Since the RPC user created the AUTH_UNIX protocol structure, he or she is responsible for destroying it with:

auth_destroy(clnt->cl_auth);

You should use this call in all cases to conserve memory.

Server-side Authentication

Server-side authentication is necessary in cases where security needs to be enforced. Since by default, no authentication is provided, this section provides an example of how to implement an authentication of the UID that is passed in the AUTH_UNIX protocol.

Consider the fields of a request handle passed to a service dispatch routine:

/*
 * An RPC Service request
 */ 
struct svc_req {
  u_long  rq_prog;        /* service program number */
  u_long  rq_vers;        /* service protocol vers num */
  u_long  rq_proc;        /* desired procedure num */
  struct opaque_auth rq_cred; /* raw credentials from wire */
  caddr_t rq_clntcred;    /* read only credentials */
};

The rq_cred is mostly opaque, except for one field of interest—the style or flavor of authentication credentials:

/*
 * Authentication info.  Mostly opaque to the programmer.
 */ 
struct opaque_auth {
  enum_t  oa_flavor;    /* style of credentials */
  caddr_t oa_base;      /* address of more auth stuff */
  u_int   oa_length;    /* not to exceed MAX_AUTH_BYTES */
}; 

The RPC package guarantees the following to the service dispatch routine:

  • The request's rq_cred is well formed. Thus, the service implementor should inspect the request's rq_cred.oa_flavor to determine which style of authentication protocol the caller used. The service implementor may also want to inspect the other fields of rq_cred if the style is not one supported by the RPC package.

  • The request's rq_clntcred field is either NULL or points to a well–formed structure that corresponds to a supported style of authentication protocol. Only the AUTH_UNIX protocol is currently supported, so it is strongly recommended that the program check that the value of rq_cred.oa_flavor is AUTH_UNIX before rq_clntcred is cast to a pointer to the authunix_parms structure. If the authentication protocol in the client request differs from the protocol used in the server's rq_cred.oa_flavor value, errant behavior and possible security holes could result.


    Note: In both cases, the authentication protocol does not verify the veracity of the request; it checks only that the structure format is adhered to.


The following example implements authentication by first checking that the protocol used in the authentication credential (rq_cred.oa_flavor) is AUTH_UNIX. If it is, and since AUTH_UNIX provides no authentication, further checks are made to verify that the identity provided in the client identity structure, rq_clntcred, is valid. If either of these checks fails, the function svcerr_weakauth() is called to alert the server that security is not being enforced.

#include <stdio.h>
#include <syslog.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <rpc/rpc.h>  /* Required. */
#include <utmp.h>
#include <rpcsvc/rusers.h>

#define MAX_LOG_MESSAGE 160

int nuser_client_ok(struct svc_req *rqstp, SVCXPRT *transp);
void nuser();

main()
{
    SVCXPRT *transp;
    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");
    exit(1);
} 

void
nuser(rqstp, transp)
struct svc_req *rqstp;
SVCXPRT *transp;
{
    uid_t uid;
    unsigned long nusers;
    struct authunix_parms *unix_cred;

    /* we don't care about authentication for the null procedure */
    if (rqstp->rq_proc == NULLPROC) {
        if (!svc_sendreply(transp, xdr_void, 0)) {
            fprintf(stderr, "can't reply to RPC call\n");
            exit(1);
        }
        return;
    }
    /* now get the uid */
    switch (rqstp->rq_cred.oa_flavor) {
    case AUTH_UNIX:
        /* perform authentication checks on client credentials */
        if (! nuser_client_ok(rqstp, transp)) {
            svcerr_weakauth(transp);
            return;
        }
        /* passed test */
        unix_cred = (struct authunix_parms *)rqstp->rq_clntcred;
        uid = unix_cred->aup_uid;
        break;
    case AUTH_NULL:
    default:
        svcerr_weakauth(transp);
        return;
    }
    switch (rqstp->rq_proc) {
    case RUSERSPROC_NUM:
        /* make sure the caller is allowed to call this procedure.  */
        if (uid == 16) {
            svcerr_systemerr(transp);
            return;
        }
        /* code here to compute the number of users and put
         * in variable nusers
         */
        if (!svc_sendreply(transp, xdr_u_long, &nusers)) {
            fprintf(stderr, "can't reply to RPC call\n");
            exit(1);
        }
        return;
    default:
        svcerr_noproc(transp);
        return;
    }
} /* nuser */

static int logging_successful_requests = 1;

/* This routine attempts to verify that the client user is
 * authorized access on the server host.  A value of 0 is
 * returned to indicate that the client user is not authorized.
 * Otherwize the value returned is 1. */
int
nuser_client_ok(struct svc_req *rqstp, SVCXPRT *transp)
{
    uid_t uid;
    char *user = NULL;
    struct authunix_parms *unix_cred;
    struct hostent *host_entry = NULL;
    struct passwd *passwd_entry;
    char log_message[MAX_LOG_MESSAGE];

    static u_long peer_addr = 0;
    static char *client_host = NULL;
    static u_long client_host_addr = 0;

    if (transp->xp_raddr.sin_port >= IPPORT_RESERVED) {
        sprintf(log_message, "Rejected request, "
                 "non-priviledged port %d", transp->xp_raddr.sin_port);
        syslog(LOG_NOTICE | LOG_AUTH, log_message);
        return 0;
    }
    /* Determine the client host name and address. */
    if (peer_addr != transp->xp_raddr.sin_addr.s_addr) {
        host_entry = gethostbyaddr(&transp->xp_raddr.sin_addr,
                                    sizeof(struct in_addr),
                                    AF_INET);
        if (host_entry == NULL) {
            sprintf(log_message, "Rejected request, "
                     "unknown client host at address 0x%08x",
                     transp->xp_raddr.sin_addr);
            syslog(LOG_NOTICE | LOG_AUTH, log_message);
            return 0;
        }
        peer_addr = transp->xp_raddr.sin_addr.s_addr;
        if (client_host != NULL) {
            free(client_host);
        }
        client_host = strdup(host_entry->h_name);
        client_host_addr = *(u_long *) host_entry->h_addr;
    }

    /* Determine the user name. */
    unix_cred = (struct authunix_parms *)rqstp->rq_clntcred;
    uid = unix_cred->aup_uid;
    passwd_entry = getpwuid(uid);
    if (passwd_entry == NULL) {
        sprintf(log_message, "Rejected request, "
                 "unknown uid %d from host %s",
                 uid, client_host);
        syslog(LOG_NOTICE | LOG_AUTH, log_message);
        return 0;
    }

    user = strdup(passwd_entry->pw_name);
    if (passwd_entry->pw_passwd != NULL &&
        *passwd_entry->pw_passwd != '\0' &&
        ruserok(client_host, uid == 0, user, user) < 0) {
        sprintf(log_message, "Rejected request by %s at %s",
                 user, client_host);
        syslog(LOG_NOTICE | LOG_AUTH, log_message);
        free(user);
        return 0;
    }

    if (logging_successful_requests) {
        if (user != NULL) {
            sprintf(log_message, "Granted request by %s at %s",
                     user, client_host);
        } else {
            sprintf(log_message, "Granted request by uid %d at %s",
                     uid, client_host);
        }
        syslog(LOG_INFO | LOG_AUTH, log_message);
    }
    if (user != NULL) {
        free(user);
    }
    return 0;
} /* nuser_client_ok */

Several points should be noted:

  • It is customary not to check the authentication parameters associated with the NULLPROC (procedure number zero).

  • If the authentication parameter's type is not suitable for your service, you should call svcerr_weakauth().

  • The service protocol itself should return status for access denied; in the case of our example, the protocol does not have such a status, so we call the service primitive svcerr_systemerr() instead.

The last point underscores the relationship between the RPC authentication package and the services—RPC with the AUTH_UNIX protocol is concerned only with identification and not with individual services' authentication or access control. The services themselves must implement these policies, and they must reflect these policies as return statuses in their protocols.

It is important to recognize that the AUTH_ UNIX credential is passed in the clear across the network and can be easily modified or counterfeited. There are no checks performed on the AUTH_ UNIX credential except to make sure it is correctly formatted.

Services that provide functions that require root permissions, and accept requests bearing AUTH_UNIX credentials, should take steps to perform authentication of the information in the AUTH_UNIX credential before relying on that information for access control decisions.

  • Limit the service to a reserved port. This requires that the originator have sufficient privilege to create a port with an address less than 1024.

  • Verify that the source IP address of the RPC request is from the machine named in the RPC AUTH_UNIX credential. The name corresponding to the source IP address should be in the list of addresses for the name.

  • Ensure that the user name/UID is known to the local system. This requires that the originator be in the network name service.

  • Consider using ruserok() (see ruserok(3N)) or an equivalent functionality to limit access to a list of known hosts.

Further authentication such as encryption, digital signatures, Kerberos, and time stamps can be incorporated into the body of the RPC request. Consult references on network security such as Applied Cryptography, Second Edition, for more examples.

Using inetd

An RPC server can be started from inetd. Call the service creation routine as follows (since inetd passes a socket as file descriptor 0):

transp = svcudp_create(0);    /* For UDP */ 
transp = svctcp_create(0,0,0);/* For listener TCP sockets */ 
transp = svcfd_create(0,0,0); /* For connected TCP sockets */ 

In addition, you should call svc_register() as:

svc_register(transp, PROGNUM, VERSNUM, service, 0);

The final flag is 0, since the program will already be registered by inetd.

Remember that if you want to exit from the server process and return control to inetd, you must explicitly exit, since svc_run() never returns.

Entries in /usr/etc/inetd.conf for RPC services should be in one of these two formats:

p_name/version dgram  rpc/udp wait user server args
p_name/version stream rpc/tcp wait user server args

In these entries, p_name is the symbolic name of the program as it appears in rpc(4); server is the program implementing the server; and version is the version number of the service. By convention, the first argument must be the program's name. For more information about inetd, see inetd(1M).

If the same program handles multiple versions, the version number can be a range. For example:

rstatd/1-2 dgram rpc/udp wait root /usr/etc/rpc.rstatd rstatd

For server programs that handle multiple services or protocols, inetd allocates socket descriptors to protocols based on lexicographic order of service and protocol names.

More Examples

The examples in this section illustrate a program version number, TCP use, and a callback procedure.

Program Version Number

By convention, the first version number of program PROG is PROGVERS_ORIG, and the most recent version is PROGVERS. Suppose there is a new version of the user program that returns an unsigned short rather than a long. If we name this version RUSERSVERS_SHORT, a server that wants to support both versions does a double register:

if (!svc_register(transp, RUSERSPROG, RUSERSVERS_ORIG,
                  nuser, IPPROTO_TCP)) {
    fprintf(stderr, "can't register RUSER service\n");
    exit(1);
}
if (!svc_register(transp, RUSERSPROG, RUSERSVERS_SHORT,
                  nuser, IPPROTO_TCP)) {
    fprintf(stderr, "can't register RUSER service\n");
    exit(1);
}

Both versions can be handled by the same C procedure:

nuser(struct svc_req *rqstp, SVCXPRT *transp)
{
    unsigned long nusers;
    unsigned short nusers2;
    switch (rqstp->rq_proc) {
    case NULLPROC:
        if (!svc_sendreply(transp, xdr_void, 0)) {
            fprintf(stderr, "can't reply to RPC call\n");
            exit(1);
        }
        return;

    case RUSERSPROC_NUM:
        /* Code here to compute the number of users and
         * assign it to the variable nusers
         */
        nusers2 = nusers;
        switch (rqstp->rq_vers) {
        case RUSERSVERS_ORIG:
           if (!svc_sendreply(transp, xdr_u_long,
                              &nusers)) {
             fprintf(stderr, "can't reply to RPC call\n");
             }
            break;
        case RUSERSVERS_SHORT:
           if (!svc_sendreply(transp,xdr_u_short,&nusers2)) {
                fprintf(stderr,"can't reply to RPC call\n");
            }
            break;
        }
    default:
        svcerr_noproc(transp);
        return;
    }
}

TCP

This example is essentially rcp. The initiator of the RPC snd() call sends its standard input to the server rcv(), which prints it on standard output. The RPC call uses TCP. This example also illustrates an XDR procedure that behaves differently on serialization than on deserialization:

/*
 * The xdr routine:
 * on decode, read from wire, write onto fp
 * on encode, read from fp, write onto wire
 */
#include <stdio.h>
#include <rpc/rpc.h>

xdr_rcp(XDR *xdrs, FILE *fp)
{
    unsigned long size;
    char buf[MAXCHUNK], *p;
    
    if (xdrs->x_op == XDR_FREE)  /* nothing to free */
        return 1;
    while (1) {
        if (xdrs->x_op == XDR_ENCODE) {
            if ((size = fread (buf, sizeof(char),
                 MAXCHUNK, fp)) == 0 && ferror(fp)) {
                fprintf(stderr, "can't fread\n");
                exit(1);
            }
        }
        p = buf;
        if (!xdr_bytes(xdrs, &p, &size, MAXCHUNK))
            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");
            exit(1);
           }
        }
    } 
}

/* The sender routines */
#include <stdio.h>
#include <netdb.h>
#include <rpc/rpc.h>
#include <sys/socket.h>
#include <sys/time.h>
main(int argc, char **argv)
{
    int xdr_rcp();
    int err;

    if (argc < 2) {
        fprintf(stderr, "usage: %s server-name\n", argv[0]);
        exit(-1);
    }
    if ((err = callrpctcp(argv[1], RCPPROG, RCPPROC_FP,
         RCPVERS, xdr_rcp, stdin, xdr_void, 0)) != 0) {
        clnt_perrno(err);
        fprintf(stderr, " can't make RPC call\n");
        exit(1);
    }
    exit(0); 
}

callrpctcp(host, prognum, procnum, versnum, inproc, in,
           outproc, out)
char *host;
int prognum;
int procnum;
int versnum;
xdrproc_t inproc;
char *in;
xdrproc_t outproc;
char *out;
{
    struct sockaddr_in server_addr;
    int socket = RPC_ANYSOCK;
    enum clnt_stat clnt_stat;
    struct hostent *hp;
    register CLIENT *client;
    struct timeval total_timeout;
    if ((hp = gethostbyname(host)) == NULL) {
        herror(host);
        return (-1);
    }
    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 = clnttcp_create(&server_addr, prognum,
         versnum, &socket, BUFSIZ, BUFSIZ)) == NULL) {
        perror("rpctcp_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);
    return (int)clnt_stat; 
}

/* The receiving routines */
#include <stdio.h>
#include <rpc/rpc.h>

main()
{
    register SVCXPRT *transp;
    int rcp_service(), xdr_rcp();
    if ((transp = svctcp_create(RPC_ANYSOCK, 1024, 1024))
        == NULL) {
        fprintf(stderr, "svctcp_create: error\n");
        exit(1);
    }
    pmap_unset(RCPPROG, RCPVERS);
    if (!svc_register(transp, RCPPROG, RCPVERS, rcp_service,
                      IPPROTO_TCP)) {
        fprintf(stderr, "svc_register: error\n");
        exit(1);
    }
    svc_run();              /* never returns */
    fprintf(stderr, "svc_run should never return\n");
    exit(1);
}

rcp_service(rqstp, transp)
register struct svc_req *rqstp;
register SVCXPRT *transp; 
{
    switch (rqstp->rq_proc) {
    case NULLPROC:
        if (svc_sendreply(transp, xdr_void, 0) == 0) {
            fprintf(stderr, "err: rcp_service\n");
            return(1);
        }
        return;
    case RCPPROC_FP:
        if (!svc_getargs(transp, xdr_rcp, stdout)) {
            svcerr_decode(transp);
            return(1);
        }
        if (!svc_sendreply(transp, xdr_void, 0)) {
            fprintf(stderr, "can't reply\n");
            return(1);
        }
        return(0);
    default: 
        svcerr_noproc(transp);
        return(1);
    }
}

Callback Procedures

In some cases (for example, in remote debugging), it's useful for a server to become a client and make an RPC callback to its client process.

For example, if the client is a window system program and the server is a debugger running on a remote machine, when the user clicks a mouse button in the debugging window, the click is converted to a debugger command, and an RPC call is made to the server (where the debugger is actually running) telling it to execute that command.

When the debugger hits a breakpoint, however, the roles are reversed. The debugger wants to make an RPC call to the window program to notify the user of the breakpoint.

To do an RPC callback, you need a program number on which to make the RPC call (see “Assigning RPC Program Numbers” in Chapter 4 for more information). Because the callback will be a dynamically generated program number, it should be in the transient range, 0x40000000—0x5fffffff.

The gettransient() routine returns a valid program number in the transient range and registers it with the port mapper (see “The Port Mapper Programs” in Chapter 4 for more information). The program talks only to the port mapper running on the same machine as the gettransient() routine itself.

The call to pmap_set() is a test-and-set operation. pmap_set() indivisibly tests whether a program number has already been registered; if the number has not been registered, pmap_set() reserves it. Upon return, the sockp argument will contain a socket that can be used as the argument to a svcudp_create() or svctcp_create() call.

#include <stdio.h>
#include <rpc/rpc.h>
#include <sys/socket.h>
#include <netinet/in.h>

gettransient(proto, vers, sockp)
int proto, vers, *sockp;
{
    static int prognum = 0x40000000;
    int s, len, socktype;
    struct sockaddr_in addr;
    switch(proto) {
        case IPPROTO_UDP:
            socktype = SOCK_DGRAM;
            break;
        case IPPROTO_TCP:
            socktype = SOCK_STREAM;
            break;
        default:
            fprintf(stderr, "unknown protocol type\n");
            return(0);
    }
    if (*sockp == RPC_ANYSOCK) {
        if ((s = socket(AF_INET, socktype, 0)) < 0) {
            perror("socket");
            return(0);
        }
        *sockp = s;
    } else
        s = *sockp;
    addr.sin_addr.s_addr = 0;
    addr.sin_family = AF_INET;
    addr.sin_port = 0;
    len = sizeof(addr);
         /* may be already bound, so don't check for error*/
    bind(s, &addr, len);
    if (getsockname(s, &addr, &len)< 0) {
        perror("getsockname");
        return(0);
    } 
    while (!pmap_set(prognum++, vers, proto,
                     ntohs(addr.sin_port)))
        continue;
    return (prognum-1);
}


Note: The call to ntohs() is necessary to ensure that the port number in addr.sin_port, which is in network byte order, is passed in host byte order (as pmap_set() expects). See byteorder(3N) for more information about network address conversion from network to host byte order.

The following programs illustrate how to use the gettransient() routine. The client makes an RPC call to the server, passing it a transient program number. Next, the client waits to receive a callback from the server at that program number. The server registers the program EXAMPLEPROG so it can receive the RPC call informing it of the callback program number. Then, at some random time (on receiving a SIGALRM signal in this example), it sends a callback RPC call, using the program number it received earlier.

/* client */
#include <stdio.h>
#include <rpc/rpc.h>

void callback();
char hostname[256];

main()
{
    int x, ans, s;
    SVCXPRT *xprt;
    
    gethostname(hostname, sizeof(hostname));
    s = RPC_ANYSOCK;
    x = gettransient(IPPROTO_UDP, 1, &s);
    fprintf(stderr, "client gets prognum %d\n", x);
    if ((xprt = svcudp_create(s)) == NULL) {
        fprintf(stderr, "rpc_server: svcudp_create\n");
        exit(1);
    }
    /* protocol is 0 - gettransient does registering */
    (void) svc_register(xprt, x, 1, callback, 0);
    ans = callrpc(hostname, EXAMPLEPROG, EXAMPLEVERS,
          EXAMPLEPROC_CALLBACK, xdr_int, &x, xdr_void, 0);
    if ((enum clnt_stat) ans != RPC_SUCCESS) {
        fprintf(stderr, "call:");
        clnt_perrno(ans);
        fprintf(stderr, "\n");
    }
    svc_run();
    fprintf(stderr,
            "Error: svc_run shouldn't have returned\n");
    exit(1);
} 

void callback (rqstp, transp)
    register struct svc_req *rqstp;
    register SVCXPRT *transp; 
{
    switch (rqstp->rq_proc) {
    case 0:
        if (!svc_sendreply(transp, xdr_void, 0)) {
            fprintf(stderr, "err: exampleprog\n");
            return(1);
        }
        return(0);
    case 1:
        if (!svc_getargs(transp, xdr_void, 0)) {
            svcerr_decode(transp);
            return(1);
        }
        fprintf(stderr, "client got callback\n");
        if (!svc_sendreply(transp, xdr_void, 0)) {
            fprintf(stderr, "err: exampleprog");
            return(1);
        }
    }
}



/* server */
#include <stdio.h>
#include <rpc/rpc.h>
#include <sys/signal.h>

char *getnewprog();
char hostname[256];
void docallback();
int pnum;           /*program number for callback routine */ 

main()
{
    gethostname(hostname, sizeof(hostname));
    registerrpc(EXAMPLEPROG, EXAMPLEVERS,
                EXAMPLEPROC_CALLBACK, getnewprog, xdr_int,
                xdr_void);
    fprintf(stderr, "server going into svc_run\n");
    signal(SIGALRM, docallback);
    alarm(10);
    svc_run();
    fprintf(stderr,
            "Error: svc_run shouldn't have returned\n");
    exit(1);
} 

char *getnewprog(pnump)
char *pnump;
{
    pnum = *(int *)pnump;
    return NULL;
} 

void docallback()
{
    int ans;
    ans = callrpc(hostname, pnum, 1, 1, xdr_void, 0,
                  xdr_void, 0);
    if (ans != 0) {
        fprintf(stderr, "server: %s", clnt_sperrno(ans));
    }
}