Project 3: Remote Procedure Call

CS233/333 - Networks and Distributed Systems
Fall, 1999.

Due Date: Friday, November 19, 11:59 p.m.

1. Overview

One difficulty of using remote procedure call (RPC) with languages like C and C++ is that the programmer is required to precisely define an interface specification (usually in a separate file such as the .x files used by rpcgen). The primary purpose of this file is to identify all of the datatypes and calling conventions for the functions that will be used. It should also be noted that having this file is, in a sense, necessary since there is no easy way for a C program to simply discover the calling conventions of a library once it has been compiled (i.e., calling conventions and type information is not encoded into C libraries or executables).

More modern languages like Java and Python however, have the ability to perform "introspection." That is, a program can inspect the contents of classes and modules at run time and, in some cases, even dynamically generate code to be executed later. As a result, it is possible to support an RPC-like mechanism without requiring a special interface specification or even a stub generator.

In this project, you are going to create a module rpc.py that uses the introspection capabilities of Python to allow remote procedure calls to be made to arbitrary Python modules.

2. The RPC Module

Your rpc module should work for both servers wanting to provide an RPC service and clients that want to connect to those servers. You only need to implement three functions: Here is a simple example showing how your module is support to work:
  1. Start the portmap server on some machine and port. For example:
    % python portmap.py 10000
    Portmapper started on gargoyle.cs.uchicago.edu:10000
    

  2. Write a simple server that provides an RPC service. For example, the following code turns the "string" module into an RPC service:
    # RPC server
    import string
    import rpc
    
    rpc.register_module(string,("gargoyle.cs.uchicago.edu",10000))
    rpc.serve_forever()
    

  3. Now, start your server on some machine and try to connect to it with a client as follows:
    >>> import rpc
    >>> rpcstring = rpc.remote_import("string",("gargoyle.cs.uchicago.edu",10000))
    Remote module 'string' loaded.
    >>> rpcstring.split("Hello world")       # Makes a remote procedure call
    ['Hello', 'world']
    >>> rpcstring.split(3)  
    Traceback (innermost last):
      File "<stdin>", line 1, in ?
    TypeError: argument 1: expected read-only character buffer, int found
    >>> 
    

  4. And that's it. Hopefully, the big picture is clear.
The next few sections describe the pieces you need to implement.

3. The portmapper

The first thing you should implement is the portmapping server. All this server does is keep track of remote services. Both RPC servers and clients will contact the portmapper.

Registering a service

When an RPC server wants to publish the availability of a remote module, it should contact the portmapper and send it the following information: This information should then be saved by the portmapper so that it can later hand it out to clients.

To illustrate, suppose you executed the following code on rustler.cs.chicago.edu:

# RPC server
import string
import rpc

rpc.register_module(string,("gargoyle.cs.uchicago.edu",10000))
This might contact the portmapper and send it the following information:
("rustler.cs.uchicago.edu", 18736, "string", [ 'atof', 
'atoi', 'atol', 'capitalize', 'capwords', 'center', 'count',
'expandtabs', 'find', 'index', 'join', 'joinfields', 'ljust', 'lower',
'lstrip', 'maketrans', 'replace', 'rfind', 'rindex', 'rjust',
'rstrip', 'split', 'splitfields', 'strip', 'swapcase', 'translate',
'upper', 'zfill'])
In this case 18736 is the port number selected by the server for client connections (the value is completely arbitrary). The list of strings starting with 'atof' simply contains all of the function names contained within the string module.

Requesting a service

When a client requests a service using the remote_import() function, it contacts the portmapper and asks it for a particular module name. If the portmapper knows about that module, it should send the above information back to the client (at which point it is up to client to contact the remote service directly). Otherwise, if the portmapper does not know about the module, it should return an error to the client.

Implementation details of the portmapper

Implementation of the portmapper should be relatively straightforward: I would be very surprised if your implementation of the portmapper is more than 50 lines of code. Think simple (it's not much more than a dictionary, a socket connection, a few calls to the pickle module).

4. The RPC server runtime

After you have the portmapper working, create a file rpc.py and implement the register_module() and serve_forever() functions. Follow the basic steps below: Now, a few miscellaneous implementation notes:

5. Client Stubs

Finally, your last step is to figure out the problem of client stubs. First, keep in mind that the real implementation of the remote functions live on the server. A stub is just a little function that lives on the client that takes the arguments passed by the user and packages them up into a network message to be sent to the server. It then needs to be able to receive the return result. In Python, this is going to be relatively easy. A stub might look something similiar to the following:
# atoi stub
def atoi(*args):
    message = ("atoi", args)
    send_message(server, message)
The *args is used to collect any sequence of function arguments into a tuple. Once in this form, it's pretty easy to package. We'll just stuff them all into a network message and let the server figure them out (if the arguments are invalid, it should send an error back).

Now, there are a few somewhat complicated details to work out on the client. However, the general idea of how this is going to work is that when the client contacts the portmapper, it is going to receive a list of function names. Using this list, you are going to dynamically generate a set of stub functions as a big Python string. Then, using some magic, you are going to execute this string to generate stub functions "on the fly"--at which point you will have a working stub module.

Here's how to proceed:

Now, if you make it this far (as I'm sure you will), you will definitely know something about how Python operates.

6. Testing

Testing is pretty simple. I should be able to start your portmapper on some machine. Then using code similiar to that listed at the beginning, I should be able to publish modules on the network and import modules remotely.

7. Extra Credit