Remote Procedure Calls
Many modern applications use a distributed computing architecture in which a user must access data or service across a network. Client/server is a computational architecture that involves client processes requesting service from server processes. Client processes usually manage the user-interface portion of the application, validate data entered by the user, and dispatch requests to server processes. The client process is the front-end of the application that the user sees and interacts with. The server process fulfills the client request by performing the task requested and passing back the results. The server process may run on a different machine on the network. In theory, the server could be written for Linux and the client could be written for Win32, however the reality is somewhat more complicated.
Remote Procedure Call (RPC) defines a powerful technology for creating distributed client/server programs.
The idea behind RPC is conceptually very simple: make distributed computing look like a function call, at least to the calling process. This enables you to focus on the details of the application rather than the details of the network communication. The calling process behaves as it usually does when making a function call:
When the calling process calls a function, the action performed by that function will not be the actual code as written, but code that begins network communication. It has to connect to the remote machine, send all the parameters, wait for replies, unpack the results and return. This is the client side stub. The server side stub on the remote machine waits for messages asking for a function to run, reads the parameters, passes them to the local function, then send the results back to the calling process after execution.
- Call a function (and optionally passing it some arguments)
- Wait for the function to finish execution
- Get the return value and resume execution
To summarize, the RPC usage paradigm is:
- Pack the request and the arguments into a network message. This is called marshalling
- Send the message to the remote server
- Block until the results arrive
- Unmarshall the results, place them on the stack for the local process
- Resume execution
The client and server programs will be compiled separately. The communication protocol is achieved by stubs generated with the rpcgen compiler. This is a powerful
compiler does most of the dirty work, so that programmers can focus on the main features of their application, rather than spending time debugging their network interface code.
The output of rpcgen is:
- Receive a function request
- Unmarshall the arguments from the network message
- Invoke the function call locally
- Marshall the return results into a network message
- Send the message back to the client process
- A header file of definitions common to the server and the client
- A set of XDR (eXternal Data Rrepresentation) routines that translate each data type defined in the header file
- A stub program for the server
- A stub program for the client
- Templates for server and client code (optionally)
Here is a simple example that creates a remote Greatest Common Divisor server using a Unix tool called rpcgen (RPC generate).
- Log in to tanner. In your systems directory, create another directory called rpc, then change your current directory to rpc.
- Write the normal, "local functions only" version of your program first. The only real modification here is that in anticipation of using RPC, you must limit your functions to accepting a single argument. This is the sample code for the gcd program
- Create a specification file (it must have the .x extension). This is used to define the RPC functions that will be executed remotely and any special data types. Functions are restricted: they may take at most one in parameter, and return at most one out parameter as the function result. If you want to use multiple parameters, you have to wrap them in a single structure, and similarly with the return values. Multiple RPC functions (numbered from one upward) may be defined at once. Each RPC function is uniquely identified by a program number, version number, and function number. The first implementation of a program will most likely have version number 1 (and incremented when functionality is changed in the remote program). RPC programs should be assigned program numbers according to rules
"Program number assignment".
This is a sample specification file (gcd.x).
- Run rpcgen on your specifcation file to generate the code templates.
rpcgen -a gcd.x
The "-a" option stands for "all", which tells rpcgen to generate all the files, including sample code for client and server side. This will generate the files:
- gcd_xdr.c (do not edit) -- the routines needed to convert local data structures into XDR format
- gcd_clnt.c (do not edit) -- the client stub, written in ANSI C
- gcd_svc.c (do not edit) -- the server stub; it is more complex, but you do not need to understand the details; it registers the service and forks a process to run the service in the background, then the parent exits.
- gcd.h (do not edit) -- the header file that contains all of the XDR types generated from the specification
- gcd_client.c -- client skeleton, needs to be modified
- gcd_server.c -- server skeleton, needs to be modified
- Edit the server skeleton
(follow the suggestions in gcd_server.c) to include the body of the function to be executed remotely. The key here is paying attention to argument and function return type.
It is essential to have result (in this example) declared as static; otherwise, if result were declared local to the remote procedure, references to its address would be invalid after the remote procedure returned.
- Edit the client skeleton
(follow the suggestions in gcd_client.c) to combine the functionality of the original program with the remote access functionality. There are five things to note here:
- First a client handle is created using the RPC library routine clnt_create(). This client handle will be passed to the stub routines that call the remote function.
- The last parameter to clnt_create() is netpath, which checks the NETPATH environment variable to select the transport protocol (UDP or TCP). You could also explicitly state "tcp" or "udp".
- The remote procedure computegcd_1() is called exactly the same way as if it were a local procedure, except for the inserted client handle as the second argument. It also returns a pointer to the result instead of the result itself.
- If the RPC mechanism fails, the remote procedure returns NULL.
- The remote machine on which the RPC server runs is passed as a command line argument.
- Compile the client code
gcc gcd_client.c gcd_clnt.c gcd_xdr.c -lnsl -o xclient
Here the option -lnsl stands for "link the network socket library," necessary to handle the communication over the network.
- Compile the server code
gcc gcd_server.c gcd_svc.c gcd_xdr.c -lnsl -o xserver
Once created, the server should be copied to a remote machine and run. (If the machines are homogeneous, the server can be copied as a binary. Otherwise, the source files will need to be copied to and compiled on the remote machine.)
- Startup the server: on the remote machine, type in
- Test the server with the client: on the local machine, type in
Here remoteHostName is the name of the remote machine running the server, for example tanner.csc.villanova.edu.
- Repeat the steps above (starting with step 2) to add two more functions Add and Subtract to your server, with the same argument type as computeGCD, and invoke them in your client code. Recompile and test your client and server.