3.8.2 Program Versions on the Client Side
The network can have different versions of an RPC server. For example,
one server might run RUSERSVERS_ORIG, and another might run
RUSERSVERS_SHORT.
If the version of the server running does not match the version number
in the client creation routines, then clnt_call fails with a
RPC_PROGVERSMISMATCH error. You can determine the version
numbers supported by the server and then create a client handle with an
appropriate version number. To do this, use clnt_create_vers
(refer to Chapter 5 for more information) or the routine shown in
Examples
3-11.
Examples
3-11 Determining Server-Supported Versions
and Creating Associated Client Handles
/* * A sample client to sense server versions */ #include <rpc/rpc.h> #include <stdio.h> #include "rusers.h" main(argc,argv) int argc; char **argv; { struct rpc_err rpcerr; struct timeval to; CLIENT *clnt; enum clnt_stat status; int maxvers, minvers; int exit(); u_short num_s; u_int num_l; char *host; host = argv[1]; clnt = clnt_create(host, RUSERSPROG, RUSERSVERS_SHORT, "udp"); (1) if (clnt == NULL) { clnt_pcreateerror("clnt"); exit(-1); } to.tv_sec = 10; /* set the time outs */ to.tv_usec = 0; status = clnt_call(clnt, RUSERSPROC_NUM, (2) xdr_void, NULL, xdr_u_short, &num_s, to); if (status == RPC_SUCCESS) { /* We found the latest version number */ clnt_destroy(clnt); printf("num = %d\n",num_s); exit(0); } if (status != RPC_PROGVERSMISMATCH) { (3) /* Some other error */ clnt_perror(clnt, "rusers"); exit(-1); } clnt_geterr(clnt, &rpcerr); (4) maxvers = rpcerr.re_vers.high; /*highest version supported */ minvers = rpcerr.re_vers.low; /*lowest version supported */ if (RUSERSVERS_ORIG < minvers || RUSERS_ORIG > maxvers) { /* doesn't meet minimum standards */ clnt_perror(clnt, "version mismatch"); exit(-1); } /* This version not supported */ clnt_destroy(clnt); /* destroy the earlier handle */ (5) clnt = clnt_create(host, RUSERSPROG, RUSERSVERS_ORIG, "udp"); /* try different version */ if (clnt == NULL) { clnt_pcreateerror("clnt"); exit(-1); } status = clnt_call(clnt, RUSERSPROCNUM, (6) xdr_void, NULL, xdr_u_long, &num_l, to); if (status == RPC_SUCCESS) { /* We found the latest version number */ printf("num = %d\n", num_l); } else { clnt_perror(clnt, "rusers"); exit(-1); } }
3.8.3 Using the TCP Transport
Examples 3_12
, 3_13
, and 3-14
work like the
remote file copy command RCP. The initiator of the RPC call,
snd, takes its standard input and sends it to the server
rcv, which prints it on standard output. The RPC call uses
TCP. The example also shows how an XDR procedure behaves differently on
serialization than on deserialization.
Examples
3-12 RPC Example that Uses TCP Protocol---XDR
Routine
/* * 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(xdrs, fp) XDR *xdrs; FILE *fp; { unsigned long size; char buf[BUFSIZ], *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), BUFSIZ, fp)) == 0 && ferror(fp)) { fprintf(stderr, "can't fread\n"); return (1); } } p = buf; if (!xdr_bytes(xdrs, &p, &size, BUFSIZ)) return (0); if (size == 0) return (1); if (xdrs->x_op == XDR_DECODE) { if (fwrite(buf, sizeof(char), size, fp) != size) { fprintf(stderr, "can't fwrite\n"); return (1); } } } }
Examples
3-13 RPC Example that Uses TCP Protocol---
Client
/* * snd.c - the sender routines */ #include <stdio.h> #include <netdb.h> #include <rpc/rpc.h> #include <sys/socket.h> #include "rcp.h" /* for prog, vers definitions */ main(argc, argv) int argc; char **argv; { int xdr_rcp(); int err; int exit(); int callrpctcp(); if (argc < 2) { fprintf(stderr, "usage: %s servername\n", argv[0]); exit(-1); } if ((err = callrpctcp(argv[1], RCPPROG, RCPPROC, RCPVERS, xdr_rcp, stdin, xdr_void, 0) > 0)) { clnt_perrno(err); fprintf(stderr, "can't make RPC call\n"); exit(1); } exit(0); } int callrpctcp(host, prognum, procnum, versnum, inproc, in, outproc, out) char *host, *in, *out; xdrproc_t inproc, outproc; { struct sockaddr_in server_addr; int socket = RPC_ANYSOCK; enum clnt_stat clnt_stat; struct hostent *hp; register CLIENT *client; struct timeval total_timeout; void bcopy(); if ((hp = gethostbyname(host)) == NULL) { fprintf(stderr, "can't get addr for '%s'\n", 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) { clnt_pcreateerror("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); }
Examples
3-14 RPC Example that Uses TCP Protocol---
Server
/* * rcv.c - the receiving routines */ #include <stdio.h> #include <rpc/rpc.h> #include <rpc/pmap_clnt.h> #include "rcp.h" /* for prog, vers definitions */ main() { register SVCXPRT *transp; int rcp_service(), exit(); if ((transp = svctcp_create(RPC_ANYSOCK, BUFSIZ, BUFSIZ)) == 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"); } int rcp_service(rqstp, transp) register struct svc_req *rqstp; register SVCXPRT *transp; { int xdr_rcp(); switch (rqstp->rq_proc) { case NULLPROC: if (svc_sendreply(transp, xdr_void, 0) == 0) fprintf(stderr, "err: rcp_service"); return; case RCPPROC: if (!svc_getargs(transp, xdr_rcp, stdout)) { svcerr_decode(transp); return; } if (!svc_sendreply(transp, xdr_void, 0)) fprintf(stderr, "can't reply\n"); return; default: svcerr_noproc(transp); return; } }
3.8.4 Callback Procedures
It is sometimes useful to have a server become a client, and make an
RPC call back to the process that is its client. An example of this is
remote debugging, where the client is a window-system program and the
server is a debugger running on the remote system. Mostly, the user
clicks a mouse button at the debugging window (converting this to a
debugger command), and then makes an RPC call to the server (where the
debugger is actually running), telling it to execute that command.
However, when the debugger reaches a breakpoint, the roles are
reversed, and the debugger wants to make an RPC call to the window
program, so it can tell the user that a breakpoint has been reached.
Callbacks are also useful when the client cannot block (that is, wait) to hear back from the server (possibly because of excessive processing in serving the request). In such cases, the server could acknowledge the request and use a callback to reply.
To do an RPC callback, you need a program number on which to make the RPC call. The program number is generated dynamically, so it must be in the transient range 0x40000000 to 0c5fffffff. The sample routine gettransient returns a valid program number in the transient range, and registers it with the Portmapper. It only communicates with the Portmapper running on the same system as the gettransient routine itself.
The call to pmap_set is a test-and-set operation, because it indivisibly tests whether a program number has been registered; if not, it is reserved. The following example shows the sample gettransient routine:
#include <stdio.h> #include <rpc/rpc.h> gettransient(proto, vers, portnum) int proto; u_long vers; u_short portnum; { static u_long prognum = 0x40000000; while (!pmap_set(prognum++, vers, proto, portnum)) continue; return (prognum - 1); }
Note that the call to ntohs for portnum is unnecessary because it was already passed in host byte order (as pmap_set expects).
The following list describes how the client/server programs in
Examples
3_15
and Examples
3-16
use the gettransient
routine:
In Examples
3_15
and Examples
3-16
, both the client and the server are on
the same system; otherwise, host name handling would be different.
Examples
3-15 Client Usage of the gettransient
Routine
/* * client */ #include <stdio.h> #include <rpc/rpc.h> #include "example.h" int callback(); main() { int tmp_prog; char hostname[256]; SVCXPRT *xprt; int stat; int callback(), gettransient(); int exit(); gethostname(hostname, sizeof(hostname)); if ((xprt = svcudp_create(RPC_ANYSOCK)) == NULL) { fprintf(stderr, "rpc_server: svcudp_create\n"); exit(1); } if ((tmp_prog = gettransient(IPPROTO_UDP, 1, xprt->xp_port)) == 0) { fprintf(stderr,"Client: failed to get transient number\n"); exit(1); } fprintf(stderr, "Client: got program number %08x\n", tmp_prog); /* protocol is 0 - gettransient does registering */ (void)svc_register(xprt, tmp_prog, 1, callback, 0); stat = callrpc(hostname, EXAMPLEPROG, EXAMPLEVERS, EXAMPLEPROC_CALLBACK,xdr_int,&tmp_prog,xdr_void,0); if (stat != RPC_SUCCESS) { clnt_perrno(stat); exit(1); } svc_run(); fprintf(stderr, "Error: svc_run shouldn't return\n"); } int callback(rqstp, transp) register struct svc_req *rqstp; register SVCXPRT *transp; { int exit(); switch (rqstp->rq_proc) { case 0: if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "err: exampleprog\n"); return (1); } return (0); case 1: fprintf(stderr, "Client: got callback\n"); if (!svc_sendreply(transp, xdr_void, 0)) { fprintf(stderr, "Client: error replyingto exampleprog\n"); return (1); } exit(0); } return (0); }
Examples
3-16 Server Usage of the gettransient
Routine
/* * server */ #include <stdio.h> #include <rpc/rpc.h> #include <sys/signal.h> #include "example.h" char hostname[256]; void docallback(int); int pnum = -1; /* program number for callback routine */ main() { char *getnewprog(); gethostname(hostname, sizeof(hostname)); registerrpc(EXAMPLEPROG, EXAMPLEVERS, EXAMPLEPROC_CALLBACK, getnewprog, xdr_int, xdr_void); signal(SIGALRM, docallback); alarm(10); svc_run(); fprintf(stderr, "Server: error, svc_run shouldn't return\n"); } char * getnewprog(pnump) int *pnump; { pnum = *(int *)pnump; return NULL; } void docallback(int signum) { int ans; if (pnum == -1) { fprintf(stderr, "Server: program number not received yet"); signal(SIGALRM, docallback); alarm(10); return; } ans = callrpc(hostname, pnum, 1, 1, xdr_void, 0, xdr_void, 0); if (ans != RPC_SUCCESS) { fprintf(stderr, "Server: %s\n",clnt_sperrno(ans)); exit(1); } if (ans == RPC_SUCCESS) exit(0); }
This chapter describes the external data representation (XDR) standard, a set of routines that enable C programmers to describe arbitrary data structures in a system-independent way. For a formal specification of the XDR standard, see RFC 1014: XDR: External Data Representation Standard.
XDR is the backbone of ONC RPC, because data for remote procedure calls is transmitted using the XDR standard. ONC RPC uses the XDR routines to transmit data that is read or written from several types of systems. For a complete specification of the XDR routines, see Chapter 8.
This chapter also contains a short tutorial overview of the XDR routines, a guide to accessing currently available XDR streams, and information on defining new streams and data types.
XDR was designed to work across different languages, operating systems, and computer architectures. Most users (particularly RPC users) only need the information on number filters (Section 4.2.1) floating-point filters (Section 4.2.2) and enumeration filters (Section 4.2.3). Programmers who want to implement RPC and XDR on new systems should read the rest of the chapter.
Note
You can use RPCGEN to write XDR routines regardless of whether RPC calls are being made.
C programs that need XDR routines must include the file
<rpc/rpc.h>, which contains all necessary interfaces to
the XDR system. The object library UCX$RPC:UCX$RPCXDR.OLB contains all
the XDR routines, so you can link as you usually would when using a
library. If you wish to use a shareable version of the library,
reference the library SYS$SHARE:UCX$RPCXDR_SHR in your LINK options
file.
4.1 Usefulness of XDR
Consider the following two programs, writer.c and reader.c:
#include <stdio.h> main() /* writer.c */ { long i; for (i = 0; i < 8; i++) { if (fwrite((char *)&i, sizeof(i), 1, stdout) != 1) { fprintf(stderr, "failed!\n"); exit(1); } } exit(0); } #include <stdio.h> main() /* reader.c */ { long i, j; for (j = 0; j < 8; j++) { if (fread((char *)&i, sizeof (i), 1, stdin) != 1) { fprintf(stderr, "failed!\n"); exit(1); } printf("%ld ", i); } printf("\n"); exit(0); }
The two programs appear to be portable because:
Piping the output of the writer.c program to the reader.c program gives identical results on an Alpha computer or on a Sun computer, as shown:
sun% writer | reader 0 1 2 3 4 5 6 7 sun% $ writer | reader 0 1 2 3 4 5 6 7 $
With local area networks and Berkeley UNIX 4.2 BSD came the concept of network pipes, in which a process produces data on one system, and a second process on another system uses this data. You can construct a network pipe with writer.c and reader.c. Here, the first process (on a Sun Microsystem's computer) produces data used by a second process (on a Digital Alpha computer):
sun% writer | rsh alpha reader 0 16777216 33554432 50331648 67108864 83886080 100663296 117440512 sun%
You get identical results by executing writer.c on the Digital Alpha computer and reader.c on the Sun computer. These results occur because the byte ordering of long integers differs between the Alpha computer and the Sun computer, although the word size is the same. Note that 16777216 is equal to 224. When 4 bytes are reversed, the 1 is in the 24th bit.
Whenever data is shared by two or more system types, there is a need for portable data. You can make programs data-portable by replacing the read and write calls with calls to an XDR library routine xdr_long, which is a filter that recognizes the standard representation of a long integer in its external form. Here are the revised versions of writer.c and reader.c:
/* Revised Version of writer.c */ #include <stdio.h> #include <rpc/rpc.h> /* xdr is a sub-library of rpc */ main() /* writer.c */ { XDR xdrs; long i; xdrstdio_create(&xdrs, stdout, XDR_ENCODE); for (i = 0; i < 8; i++) { if (!xdr_long(&xdrs, &i)) { fprintf(stderr, "failed!\n"); exit(1); } } exit(0); } /* Revised Version of reader.c */ #include <stdio.h> #include <rpc/rpc.h> /* XDR is a sub-library of RPC */ main() /* reader.c */ { XDR xdrs; long i, j; xdrstdio_create(&xdrs, stdin, XDR_DECODE); for (j = 0; j < 8; j++) { if (!xdr_long(&xdrs, &i)) { fprintf(stderr, "failed!\n"); exit(1); } printf("%ld ", i); } printf("\n"); exit(0); }
The new programs were executed on an Alpha computer, a Sun computer, and from a Sun computer to an Alpha computer; the results are as follows:
sun% writer | reader 0 1 2 3 4 5 6 7 sun% $ writer | reader 0 1 2 3 4 5 6 7 $ sun% writer | rsh alpha reader 0 1 2 3 4 5 6 7 sun%
Note
Arbitrary data structures create portability problems, particularly with alignment and pointers:
- Alignment on word boundaries may cause the size of a structure to vary on different systems.
- A pointer has no meaning outside the system where it is defined.
The XDR approach to standardizing data representations is canonical, because XDR defines a single byte order (big-endian), a single floating-point representation (IEEE), and so on. A program running on any system can use XDR to create portable data by translating its local representation to the XDR standard. Similarly, any such program can read portable data by translating the XDR standard representation to the local equivalent.
The single standard treats separately those programs that create or send portable data and those that use or receive the data. A new system or language has no effect on existing portable data creators and users. Any new system simply uses the canonical standards of XDR; the local representations of other system are irrelevant. To existing programs on other systems, the local representations of the new system are also irrelevant. There are strong precedents for the canonical approach of XDR. For example, TCP/IP, UDP/IP, XNS, Ethernet, and all protocols below layer 5 of the ISO model, are canonical protocols. The advantage of any canonical approach is simplicity; in the case of XDR, a single set of conversion routines is written once.
The canonical approach does have one disadvantage of little practical
importance. Suppose two little-endian systems transfer integers
according to the XDR standard. The sending system converts the integers
from little-endian byte order to XDR (big-endian) byte order, and the
receiving system does the reverse. Because both systems observe the
same byte order, the conversions were really unnecessary. Fortunately,
the time spent converting to and from a canonical representation is
insignificant, especially in networking applications. Most of the time
required to prepare a data structure for transfer is not spent in
conversion but in traversing the elements of the data structure.
4.1.2 The XDR Library
The XDR library enables you to write and read arbitrary C constructs consistently. This makes it useful even when the data is not shared among systems on a network. The XDR library can do this because it has filter routines for strings (null-terminated arrays of bytes), structures, unions, and arrays. Using more primitive routines, you can write your own specific XDR routines to describe arbitrary data structures, including elements of arrays, arms of unions, or objects pointed at from other structures. The structures themselves may contain arrays of arbitrary elements, or pointers to other structures.
The previous writer.c and reader.c routines manipulate data by using standard I/O routines, so xdrstdio_create was used. The parameters to XDR stream creation routines vary according to their function. For example, xdrstdio_create takes the following parameters:
It is not necessary for RPC users to create XDR streams; the RPC system itself can create these streams and pass them to the users. There is a family of XDR stream creation routines in which each member treats the stream of bits differently.
The xdr_long primitive is characteristic of most XDR library primitives and all client XDR routines for two reasons:
xdr_xxx(xdrs, xp) XDR *xdrs; xxx *xp; { }
In this case, xxx is long, and the corresponding XDR routine is a primitive, xdr_long. The client could also define an arbitrary structure xxx in which case the client would also supply the routine xdr_xxx, describing each field by calling XDR routines of the appropriate type. In all cases, the first parameter, xdrs, is treated as an opaque handle and passed to the primitive routines.
XDR routines are direction-independent; that is, the same routines are called to serialize or deserialize data. This feature is important for portable data. Calling the same routine for either operation practically guarantees that serialized data can also be deserialized. Thus, one routine is used by both producer and consumer of networked data.
You implement direction independence by passing a pointer to an object rather than the object itself (only with deserialization is the object modified). If needed, the user can obtain the direction of the XDR operation. See Section 4.3 for details.
For a more complicated example, assume that a person's gross assets and liabilities are to be exchanged among processes, and each is a separate data type:
struct gnumbers { long g_assets; long g_liabilities; };
The corresponding XDR routine describing this structure would be as follows:
bool_t /* TRUE is success, FALSE is failure */ xdr_gnumbers(xdrs, gp) XDR *xdrs; struct gnumbers *gp; { if (xdr_long(xdrs, &gp->g_assets) && xdr_long(xdrs, &gp->g_liabilities)) return(TRUE); return(FALSE); }
In the preceding example, the parameter, xdrs, is never inspected or modified; it is only passed to subcomponent routines. The program must inspect the return value of each XDR routine call and stop immediately and return FALSE upon subroutine failure.
The preceding example also shows that the type bool_t is declared as an integer whose only value is TRUE (1) or FALSE (0). The following definitions apply:
#define bool_t int #define TRUE 1 #define FALSE 0
With these conventions, you can rewrite xdr_gnumbers as follows:
bool_t xdr_gnumbers(xdrs, gp) XDR *xdrs; struct gnumbers *gp; { return(xdr_long(xdrs, &gp->g_assets) && xdr_long(xdrs, &gp->g_liabilities)); }
Either coding style can be used.
4.2 XDR Library Primitives
The following sections describe the XDR primitives--- basic and
constructed data types---and XDR utilities. The include file
<rpc/xdr.h>, (automatically included by
<rpc/rpc.h>), defines the interface to these primitives
and utilities.
4.2.1 Number and Single-Character Filters
The XDR library provides primitives that translate between numbers and single characters and their corresponding external representations. Primitives include the set of numbers in:
[signed, unsigned] * [char, short, int, long, hyper]
Specifically, the ten primitives are:
bool_t xdr_char(xdrs, cp) XDR *xdrs; char *cp; bool_t xdr_u_char(xdrs, ucp) XDR *xdrs; unsigned char *ucp; bool_t xdr_short(xdrs, sip) XDR *xdrs; short *sip; bool_t xdr_u_short(xdrs, sup) XDR *xdrs; u_short *sup; bool_t xdr_int(xdrs, ip) XDR *xdrs; int *ip; bool_t xdr_u_int(xdrs, up) XDR *xdrs; unsigned *up; bool_t xdr_long(xdrs, lip) XDR *xdrs; long *lip; bool_t xdr_u_long(xdrs, lup) XDR *xdrs; u_long *lup; bool_t xdr_hyper(xdrs, hp) XDR *xdrs; longlong_t *hp; bool_t xdr_u_hyper(xdrs, uhp) XDR *xdrs; u_longlong_t *uhp;