UNIX Network Programming - Stevens Linux RPC Comer Chapter 19-21 (RPCgen Concept) RFC 1057 – RPC Spec. UNIX Network Programming - Stevens
Sockets API Limitations Must explicitly account for differences in systems Byte order (big-endian / little-endian) Limited to sending characters (without tricking the system) requires source and destination to implicitly understand the data transformation.
Client / Server Between Systems One Solution: eXternal Data Representation (XDR) Developed by Sun Microsystems A standard for representing data over the network. Includes a set of library routines for converting data between local format and XDR format (either direction). So how do we deal with this? We develop a single standard for representing data outside the host system. This reduces the conversion problem to O(N), since each OS only needs to manage a single conversion. Sun was the original developer of this approach, called XDR. It includes a reference for specifying data types, as well as a set of library routines that can be used to convert the data. These routines can be used explicitly, or they can be embedded inside other higher level routines used to support inter-platform communications (like RPC).
XDR Data Types int 32 bits unsigned int 32 bits bool 32 bits enum arbitrary hyper 64 bits unsigned hyper 64 bits float 32 bits double 64 bits opaque arbitrary String arbitrary fixed array arbitrary counted array arbitrary structure arbitrary discriminated union arbitrary. void 0 symbolic constant arbitrary optional data arbitrary The XDR standard identifies a number of different elements within its standard, so that the data transfer can be as flexible as possible. All of these data types are specified in terms of the largest likely definition on host platforms. (Its always easier to increase range than it is to remove it). There are also other sub-types - like short integers, etc.
XDR Data Conversion Objective: Procedure: Gather all parameter data into a buffer in XDR format Procedure: Create a buffer (xdrmem_create) The objective of this conversion process is to make it easier for programmers to arrange data for RPCs in the correct order. The specific syntax that we will be talking about here is from the SUN implementation of RPC. If we want to send a message between machines (or between processes), the first step is to build a buffer that can hold the message. This can include as many parameters as the procedure wants to send. When we create the buffer, our link to the space is through a pointer. Initially the pointer refers to the initial address of the buffer, but as we add items, that pointer gets moved.
XDR Data Conversion Objective: Procedure: Gather all parameter data into a buffer in XDR format Procedure: Create a buffer (xdrmem_create) #include <rpc/xdr.h> #define BUFSIZE 4000 ... XDR * xdrs; char buf[BUFSIZE]; xdrmen_create(xdrs, buf, BUFSIZE, XDR_ENCODE); The objective of this conversion process is to make it easier for programmers to arrange data for RPCs in the correct order. The specific syntax that we will be talking about here is from the SUN implementation of RPC. If we want to send a message between machines (or between processes), the first step is to build a buffer that can hold the message. This can include as many parameters as the procedure wants to send. When we create the buffer, our link to the space is through a pointer. Initially the pointer refers to the initial address of the buffer, but as we add items, that pointer gets moved.
XDR Data Conversion Routines xdrs_short (xdrs, ptrShort); xdrs_u_char (xdrs, ptrUchar); xdrs_u_int (xdrs, ptrUint); xdrs_u_long (xdrs, ptrUlong); xdrs_u_short (xdrs, ptrUshort); xdr_union (xdrs, ptrDiscrim, ptrUnion, choiceFcn, default); xdr_vector (xdrs, ptrArray, size, elemSize, elemProc); xdr_void ( ); xdr_bool (xdrs, ptrBool); xdr_bytes (xdrs,str,strSize,maxsize); xdr_char (xdrs, ptrChar); xdr_double(xdrs, prtDouble); xdr_enum(xdrs, ptrInt); xdr_float(xdrs, ptrFloat); xdr_int (xdrs, ptrInt); xdr_long (xdrs, ptrLong); xdr_opaque (xdrs, ptrChar, count); xdr_pointer (xdrs, ptrObj, pbjSize, xdrObj); There are a number of different conversion routines available to modify data so that it meets XDR specs. The simple objects are simple to follow. The more complex items (like pointer and union) are a little more complex. For a pointer to be relevant across platforms, it must identify what type of object it points to, how big the object is, and the object itself. After all, an address that points to something on a different machine isn’t much use. The union conversion is a little more complex. We need to pass a data space large enough to hold any of the choices, we need to pass some indicator of which choice we have made, and we need to pass a function to define the choices (with default).
XDR Data Conversion Add data items in order to the buffer (after converting to XDR format) int myInt; ... myInt = 260; xdr_int(xdrs, &myInt); The integer conversion xdr_int() will convert myInt to a 4 byte representation, arrange it in the proper byte order, and append it to the buffer (stream). After the conversion, the stream pointer will be updated to point to the new insertion point.
RPC Programming Mechanisms ONC (Open Network Computing) XDR library routines for data conversion XDR library routines for complex data structures RPC run-time library routines Program generator tool One of the most widely accepted RPC models (ONC) or Sun RPC provides a number of tools to help with RPC development
Applications Conceptual Model Main Interface proc1 proc2 proc3 proc4 Conceptually, here’s what that process will look like. We first build the program. We then identify the functions or procedures that can be effectively remoted. For example, routines that rely on data shared with the main program may not be good candidates, since that data would have to be passed across the interface. Once we have identified the procedure(s) that we want to remote, we can now design the interface for that link. proc5 proc6 proc7
RPC Programming Process Dividing the program into local and remote procedures. RPC Proc A Server Stub There are a couple of ways to think of RPC. We’ve discussed the segmentation of the program into calling portion and service (procedure) portion. RPC provides the interface between these two segments. One approach to RPC allows the calling program to explicitly embed the service location within the code. This eliminates the process of accessing a port mapper and determining a location. Client Stub Proc B
RPC Dispatching (Procedure Location) Proc A2 Dispatcher Proc A1 RPC Server Stub Server Stub Client Stub Client Stub Proc B1 Proc B2 The more general case uses the services of a dispatcher (port mapper) to store the specific service access points (endpoints) associated with each registered interface. That way, the calling program only needs to know which machine the procedure is running on (or in some cases, which Domain and LAN the procedure is on).
RPC Interface Specification Proc A Server Comm RPC Client Iface Server Iface In fact, the development of the RPC interface is a little more complicated than previously discussed. Once the interface has been defined, and the stub programs have been generated by the IDL compiler, the calling program and the remote procedure need to develop interfaces to link the RPC generated code to the local process. The communications code is generated by IDL, and the interface code is developed by the programmer. Client comm Proc B
RPC General Build Procedure Define Interface Develop Client Now, let’s go into some detail on the process, from a Windows perspective. At the highest level, the process within Windows is identical to RPC on virtually any other platform. Once you have defined an interface between client and server portions of the program, you can split the program into the appropriate components and share the interface with both halves. Develop Server
Developing the Interface MyApp.x RPCgen MyApp_clnt.c Client Stub In Windows, the interface specification is written in a special language called an “interface definition language”. This is packaged as a .idl file. This file is then compiled with the Microsoft IDL compiler (MIDL) and the result is 3 new files: a header file that is shared with both client and server, and stubs for clients and servers. MyApp.h MyApp_svc.c Server Stub MyApp_xdr.c
RPCgen Input and Output Q.x Interface specification file Output Q.h Declarations header file Q_xdr.cpp XDR procedure calls used to marshal arguments Q_clnt.cpp Client-side communications stub Q_svc.cpp Server-side communications stub The ONC rpcgen program will take as an input a single interface specification file (with an “x” extension) and will generate 4 other files. The other files will build on the base name of the “x” file. The header file will include new declarations needed by the stub programs. The _xdr program will include those procedure calls needed to marshall (or arrange) the data. The client and service programs provide the communications stubs.
RPC Process Flow Client application Client interface Q_clnt.cpp compile Client Q.h rpcgen Q.x Q_xdr.cpp compile In a flow diagram, here is the overall ONC process. Note that Q.h and Q_xdr.cpp are both shared by both server and client programs. Server Q_svc.cpp Remote procedures Server interface
Procedure Registration and Use 6 RPC service RPC client 5 4 2 Port Mapper 3 1. The Port mapper starts up first. 2. When a new RPC service (remote Procedure) becomes available, it registers with the Port mapper. 3. When a client wants to use the procedure, it queries the port mapper for an identifying port for the service. 4. If available, the port mapper returns a protocol and port number. 5. The client now sends a request to the service. 6. The remote procedure is executed and the response is returned to the client. 1
Start Portmap Portmap is included in all Linux distributions as a standard server. Under Red Hat / Fedora Open services applet , select portmap and start From command line (as root) /sbin/service portmap start Other distributions should be similar The first step needed is to register a remote procedure interface with the interface registry table in the host server. This is done by calling the RpcServerRegisterIf function and passing the interface specifier (along with other parameters). This Interface specification is created as part of the MIDL process, and is identified in the header file.
How the Server Prepares for a Connection (Be certain that Portmap is running) Create UDP service Register UDP service with Portmap Create TCP service Register TCP service with Portmap Run service... Uses select( ) to monitor ports. Now we get into the heart of the process. In a general sense, there are 5 functions that need to be performed for a server to prepare for a connection. We will go over each of these steps and look at the functions and data needed to make each work. IMPORTANT!!! Microsoft has extended the concept of RPC to make is significantly more flexible. This also makes it significantly more complicated (to manage that added complexity). We will be looking at a number of different options available with Windows RPC.
Server concurrency mode RPC servers can be created in either single threaded or multi-threaded mode. Servers automatically create and (when done) destroy a thread for each incoming connection. dynamic endpoint binding “well-known” static binding One of the features of Windows RPC is that it allows a RPC to support multiple interfaces. We have talked in terms of TCP and UDP ports, but there are many other choices available within Windows. These can include named pipes, NetBios, IPX, DECnet, and several others. To further customize the process there are several (6) different functions that allow the server to register its endpoints. The approach shown here assumes that the endpoint specifiers are identified in the interface specification.
Register the Server Program svc_register(port#, SERV#, VER#, serv_func, proto#); port#: port on which the service is active SERV#: unique number for the service VER#: version for this particular service serv_func: name by which this function is called proto#: IPPROTO_UDP or IPPROTO_TCP Once the service has been registered with the local server, it can be “advertised”. This is done by exporting the endpoint bindings to a name server (the port mapper). To do this, we first need to acquire a “listing” of the active endpoints. This can then be passed, along with the interface specification and a service name, to the name server. In windows NT, this program is called the Locator. On a Server managed network, it is hosted by the domain server, and is directly accessed by broadcast from any other network members on the same network (subnet). [Doesn’t work as well when there are several separate subnetworks...]
How the Client Establishes a Connection Make a Remote Procedure Call Find the Server Host Computer Find Server Process Port # (through Portmap) Create a (connection) to the Server Process On the client side the process is somewhat simpler. The process of finding the server machine, locating the process, and binding to the appropriate endpoints are all wrapped up into a couple of function calls.
How the Client Establishes a Connection CLIENT = clnt_create(server, SERV#, VER#, proto#); remote procedure call... Clnt_destroy(CLIENT);
Example #1 Temperature Server (Fahrenheit to Centigrade) Source files: Parameters passed as integers TCP / IP port connection Source files: temp.x Tclient.c tempServer.c Now to the example. We will look at a couple of examples. They will all center around the temperature server application, but they will transfer the data in different ways, using different mechanisms. The first one passes integers between the calling program and the remote procedure and uses TCP/IP as the transport. There are 5 source files that the programmer needs to develop. There will be 3 others that are created by the MIDL compiler.
temp.x program TEMPSERV { version TEMPVERS { int TempConv(int) = 1; //procedure number } = 1; //version number } = 77; //program number
TClient.c #include <stdio.h>, <stdlib.h>, <string.h> #include <rpc/rpc.h> #include "temp.h" #define YES 0 #define NO 1 void main (int argc, char *argv[]) { int tempconvert(int temp, char *srvr); int temperature, nuTemp; int loopFlag; char srvr[25]; CLIENT * cl;
TClient.c (cont) cl = clnt_create(srvr, TEMPSERV, TEMPVERS, "tcp"); strcpy (srvr, argv[1]); cl = clnt_create(srvr, TEMPSERV, TEMPVERS, "tcp"); loopFlag = YES; while(loopFlag == YES) { printf("Enter temperature in Faherenheit (-999 to quit)"); scanf ("%d", &temperature); ans = tempconv_1(&temperature, cl);
TClient.c (cont) if (ans != NULL) nuTemp = * ans; if (temperature == -999 || temperature == -9999) { loopFlag = NO; printf ("Goodbye...\n"); continue; } printf("That’s %2d in centigrade\n", nuTemp); } //end of while() clnt_destroy(cl); return 0;
tempServer.c #include <stdlib.h>, <unistd.h>, <stdio.h> #include <rpc/rpc.h>, "temp.h" static int count; static int nuTemp; int *tempconv_1_svc (int *val, struct svc_req * rqst) { int oldTemp; oldTemp = *val; struct svc_req defined in /usr/include/rpc/svc.h Includes prog#, ver #, proc entry, raw credentials, “cooked” creds, associated transport. Not needed in our example, but often needed in real apps to validate request.
tempServer.c if (oldTemp == -9999) { printf("We're shutting down...\n"); exit (0); } printf("We got a temperature of %d, ", oldTemp); count++; nuTemp = (int)((5*(oldTemp -32)) / 9.0); printf("and we returned a value of %d\n", nuTemp); sleep(1); return (&nuTemp);
Files created with rpcgen Input: temp.x Output temp.h temp_xdr.c (NULL file) temp_clnt.c temp_svc.c
temp.h #include <rpc/rpc.h> #ifdef __cplusplus extern "C" { #endif #define TEMPSERV 77 #define TEMPVERS 1 #if defined(__STDC__) || defined(__cplusplus) #define TempConv 1 extern int * tempconv_1(int *, CLIENT *); extern int * tempconv_1_svc(int *, struct svc_req *); extern int tempserv_1_freeresult (SVCXPRT *, xdrproc_t, caddr_t); #ifdef __cplusplus} #endif /* !_TEMP_H_RPCGEN */
temp_xdr.c #include <rpc/rpc.h> #include "temp.h"
temp_clnt.c #include <memory.h> /* for memset */ #include "temp.h“ /* Default timeout can be changed using clnt_control() */ static struct timeval TIMEOUT = { 25, 0 }; int *tempconv_1(int *argp, CLIENT *clnt){ static int clnt_res; memset((char *)&clnt_res, 0, sizeof(clnt_res));
temp_clnt.c (cont) if (clnt_call (clnt, TempConv, (xdrproc_t) xdr_int, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res);}
temp_svc.c #include "temp.h“ #include <stdio.h> #include <stdlib.h> #include <rpc/pmap_clnt.h> #include <string.h> #include <memory.h> #include <sys/socket.h> #include <netinet/in.h> #ifndef SIG_PF #define SIG_PF void(*)(int) #endif
temp_svc.c (cont) static void tempserv_1(struct svc_req *rqstp, register SVCXPRT *transp){ union { int tempconv_1_arg; } argument; char *result; xdrproc_t _xdr_argument, _xdr_result; char *(*local)(char *, struct svc_req *); switch (rqstp->rq_proc) { case NULLPROC: (void) svc_sendreply (transp, (xdrproc_t) xdr_void, (char *)NULL); return; case TempConv: _xdr_argument = (xdrproc_t) xdr_int; _xdr_result = (xdrproc_t) xdr_int; local = (char *(*)(char *, struct svc_req *)) tempconv_1_svc; break; default: svcerr_noproc (transp); return; }
temp_svc.c (cont) memset ((char *)&argument, 0, sizeof (argument)); if (!svc_getargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) { svcerr_decode (transp); return; } result = (*local)((char *)&argument, rqstp); if (result != NULL && !svc_sendreply(transp, (xdrproc_t) _xdr_result, result)) { svcerr_systemerr (transp); if (!svc_freeargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) { fprintf (stderr, "%s", "unable to free arguments"); exit (1); svc_getargs is a macro that decodes the arrguments of an RPC request associated with the RPC service transport handle transp. The parameter _xdr_argument is the address where the arguments will be places. &argument is the SDR routine used to decode the arguments. This reoutine returns one if decoding succeeds and zero otherwise.
temp_svc.c (cont) int main (int argc, char **argv){ register SVCXPRT *transp; pmap_unset (TEMPSERV, TEMPVERS); transp = svcudp_create(RPC_ANYSOCK); if (transp == NULL) { fprintf (stderr, "%s", "can’t create udp service."); exit(1); } if (!svc_register(transp, TEMPSERV, TEMPVERS, tempserv_1, IPPROTO_UDP)) { fprintf (stderr, "%s", "unable to register (TEMPSERV, TEMPVERS, udp)."); exit(1);
temp_svc.c (cont) transp = svctcp_create(RPC_ANYSOCK, 0, 0); if (transp == NULL) { fprintf (stderr, "%s", "cannot create tcp service."); exit(1); } if (!svc_register(transp, TEMPSERV, TEMPVERS, tempserv_1, IPPROTO_TCP)) { fprintf (stderr, "%s", "unable to register (TEMPSERV, TEMPVERS, tcp)."); svc_run (); fprintf (stderr, "%s", "svc_run returned"); exit (1); /* NOTREACHED */}
Sample Client Output D:\data\RPC\onrpc_temp\client\Debug>client localhost Enter the temperature in Faherenheit (-999 to quit)32 That would be 0 in centigrade Enter the temperature in Faherenheit (-999 to quit)100 That would be 37 in centigrade Enter the temperature in Faherenheit (-999 to quit)212 That would be 100 in centigrade Enter the temperature in Faherenheit (-999 to quit)-9999 Goodbye... D:\data\RPC\onrpc_temp\client\Debug>
Sample Server Output D:\data\RPC\examples\onrpc_temp\server\Debug>server We got a temperature of 32, and we returned a value of 0 We got a temperature of 100, and we returned a value of 37 We got a temperature of 212, and we returned a value of 100 We're shutting down... D:\data\RPC\examples\onrpc_temp\server\Debug>
Summary Linux RPC Implementation models SUN ONCRPC functionality RPC specific programming limited to linking original applications code with rpcgen interface code.