[Before reading the following notes you should read Tanenbaum's textbook from page 68 to page 98 included.]
Much of what we do when we communicate using sockets is standardized. How we establish connections between clients and servers, how data is packed in messages and extracted from them, how we organize the services on the server side as traditional functions. So it is not surprising that people have come up with methods for mechanising these activities. Systems where these mechanisms are available are called Remote Procedure Call (RPC) systems and they involve [among other things]:
These systems are usually compatible with a series of transport mechanisms and protocols. A number of different systems for RPC are currently available. The one we will use is Open Networking Computing (ONC) since it is available for free on most Unix systems. Other systems, such as RPC for the Distributed Computing Environment (DCE) are more comprehensive and a natural evolution of the ONC functionality. You may want to check the manual pages for the unix commands rpcinfo and portmap, and see the file /etc/rpc.
The normal way to use an RPC system is by defining an interface between the client and the server and by compiling it with a protocol compiler (the protocol compiler we use is called rpcgen). This produces a number of files that are linked with the client and server code written by the programmer to produce the needed executable images. This way of doing things is extremely convenient and requires very little knowledge of protocols on the part of the programmer. Alternatively the protocol compiler is not used and the programmer uses directly the Application Programming Interface (API) to the RPC. We will follow the first approach.
To make our discussion concrete, here is a simple example of client server interaction using ONC RPC.
This example is clearly unrealistic: nobody would create a server that implements functions to add/subtract two integers and return the result. The purpose of the example is to show how RPC works, with as few distractions as possible. Here are the files specified by the programmer:
/* * mc.x: remote calculator access protocol */ /* structure definitions*/ struct mypair { int arg1; int arg2; }; /* program definition, no union or typdef definitions needed */ program MCPROG { /* could manage multiple servers */ version MCVERS { int ADD(mypair) = 1; int SUBTRACT(mypair) = 2; } = 1; } = 0x20000002; /* program number ranges established by ONC */Here we see the definitions of the functions to be called remotely, and of the constants and types needed in those definitions. All is written in a language very similar to C called RPCL, that represents the commands provided by the server and their possible parameters.
CLIENT *cl; /* a client handle */ if (!(cl = clnt_create(argv[1], MCPROG, MCVERS, "tcp"))) { /* * CLIENT handle couldn't be created, server not there. */ clnt_pcreateerror(argv[1]); exit(1); }where MCPROG and MCVERS are defined, as you saw, in mc.x and are the name and version of the remote program, while argv[1] is the name of the server host (soemthing like snowhite.cis.temple.edu). The client handle returned by clnt_create will be used as last parameter in RPC calls.
The name of the port used by the server is not given. This is so because of the presence of a daemon called portmapper. The server registers with portmapper the port it uses and the client implicitly asks the portmapper for the name of the port [portmapper is itself a service responding on port 111]. The programmer can use directly the API for the portmapper, see for example the functions pmap_getport, pmap_set, pmap_unset in the man pages.
Here is how the client program calls the remote functions:
v = (*add_1(&p,cl)); v = (*subtract_1(&p,cl));where cl is the client handler, v is an integer, and p is a "pair structure" with two integers. Notice the name we have used, "add_1", in lowercase it is the name "ADD" we introduced in mc.x, with appended the version number.
This code is written with knowledge of the information specified in the interface but without need of socket and network commands or of RPC library functions. It is not written as a "main program", it consists just of the functions that will be called remotely and of auxiliary data structures and definitions. As you can see, it is trivial code:
#include <stdio.h> #include <rpc/rpc.h> #include "mc.h" int v; int *add_1(mypair *p) { v = p->arg1+p->arg2; return &v; } int *subtract_1(mypair *p) { v = p->arg1-p->arg2; return &v; }
rpcgen mc.x cc -c -o mc.o -g -DDEBUG mc.c cc -g -DDEBUG -c mc_clnt.c cc -g -DDEBUG -c mc_xdr.c cc -g -DDEBUG -o mc mc.o mc_clnt.o mc_xdr.o cc -c -o mc_svc_proc.o -g -DDEBUG mc_svc_proc.c cc -g -DDEBUG -c mc_svc.c cc -g -DDEBUG -o mc_svc mc_svc_proc.o mc_svc.o mc_xdr.oThen the server will be launched as just
mc_svc &(we use '&' so that the server runs in the background). A client will be launched as
mc serverhostnameFor example if the server is on yoda.cis.temple.edu, we will call
mc yoda.cis.temple.eduIt would be possible to use the inetd demon to run servers as needed without having to launch them ourselves as we did above. The use of inetd would require root privilege so as to modify the files /etc/setvices and /etc/inetd.conf. [inetd is a super-server. It is launched when unix is started and monitors the ports specified in /etc/services for the services specified in /etc/inetd.conf. Then when these ports are accessed, it launches the corresponding servers, if they are not already active. The servers so launched can be given a deadline so that if they are not active for more than the specified deadline, they terminate. Of course the aim is to minimize the number of idle active servers. This idea of a server whose business is to monitor the existence of regular servers and minimize the number of executing servers and effort required to manage servers, is a powerful one. It is carried out at a greater extent and a higher level in the ORBs of CORBA, which we will discuss later in the course.]
+------------+ ^ | older fp | | High Memory +------------+ | | locals and | | temporaries| | of caller | +------------+ | | arg 2 | | Stack growth +------------+ V | arg 1 | +------------+ | return | +------------+ | old fp | <-- frame pointer (fp) +------------+ | locals and | | temporaries| | of callee | +------------+ <-- stack pointer (sp)The second argument does not affect the caller (since it knows that there are two arguments) nor the callee (since the positions of return and arg1 relative to the frame pointer are not affected by arg2).
From rdb.x the protocol compiler rpcgen generates:
rpcgen rdb.x cc -c -o rdb.o -g -DDEBUG rdb.c cc -g -DDEBUG -c rdb_clnt.c cc -g -DDEBUG -c rdb_xdr.c cc -g -DDEBUG -o rdb rdb.o rdb_clnt.o rdb_xdr.o cc -c -o rdb_svc_proc.o -g -DDEBUG rdb_svc_proc.c cc -g -DDEBUG -c rdb_svc.c cc -g -DDEBUG -o rdb_svc rdb_svc_proc.o rdb_svc.o rdb_xdr.oThen the server will be launched as just
rdb_svcand any client will be launched as
rdb serverhostname dbkey dbvalue
If you are using a protocol compiler for the RPC, you have no need to know anything about XDR and its API. If you are not using a protocol compiler then you need to know the XDR API. Information about it can be found, say, with the command
man xdrHere is an example of use of the XDR API to write and read from a file in XDR format. It is the program portable.c from Bloomer's book.
#include <rpc/xdr.h> #include <stdio.h> short sarray[] = {1, 2, 3, 4}; main() { FILE *fp; XDR xdrs; int i; /* * Encode the 4 shorts. */ fp = fopen("data", "w"); xdrstdio_create(&xdrs, fp, XDR_ENCODE); for (i = 0; i < 4; i++) if (xdr_short(&xdrs, &(sarray[i])) == FALSE) fprintf(stderr, "error writing to stream\n"); xdr_destroy(&xdrs); fclose(fp); /* * Decode the 4 shorts. */ fp = fopen("data", "r"); xdrstdio_create(&xdrs, fp, XDR_DECODE); for (i = 0; i < 4; i++) if (xdr_short(&xdrs, &(sarray[i])) == FALSE) fprintf(stderr, "error reading stream\n"); else printf("%d\n", sarray[i]); xdr_destroy(&xdrs); fclose(fp); }
Note that the use of RPC has simplified considerably the task of exchanging information across computers. The situation is considerably easier than in the case that we use sockets directly. The transport mechanism, whether tcp, or udp, or tci etc. is hidden. Of course basic problems, such as how to insure reliability of communication, or recover from crashes, or how best to solve concurrency and performance problems remain.
ingargiola@cis.temple.edu