RPC Demos

Examples of Sun RPC, Java RMI, and Python RPyC

Paul Krzyzanowski

September 21, 2023

These demos attempt to show the simplest possible code that illustrates the use of remote procedures on each of the frameworks. The demo is a set of remote procedures that include:

  • add: adds two numbers
  • sub: subtracts two numbers
  • tolower: converts a string to lower case

There is minimal error checking.

You can download the code here.

Sun RPC (ONC RPC)

By default, Sun RPC only allowed a single parameter to a function. If you needed multiple parameters, you’d stick them into a struct. That changed but you need to use a -N flag with rpcgen to process multiple parameters. This demo code uses TI-RPC (Transport Independent RPC) and has been tested on Ubuntu Linux on the Rutgers iLab machines.

Step 1. Interface definition.

Here’s the interface definition, calc.x:

calc.x:

program CALC_PROG {
    version CALC_VERS {
        int ADD(int, int) = 1;
        int SUB(int, int) = 2;
        string TOLOWER(string) = 3;
    } = 1;
} = 0x33445566;

It defines an interface (called a program ) named CALC_PROG that contains one version (CALC_VERS) of the remote functions. The functions are ADD, SUB, and TOLOWER. The first two functions add and subtract two numbers, respectively. The third function illustrates how we can pass and return strings and simply converts a string to lowercase.

The interface definition language requires us to assign a unique number to each function within a version, to assign a number to each version, and to assign a 32-bit value to the entire Interface. Internally the protocol will encode the function numbers to refer to the corresponding functions rather than encode the name.

Step 2. Generate stubs

Generate stubs with the command:

rpcgen -aNC calc.x

The -a flag tells rpcgen to generate all required files. The -N flag enables support for multiple parameters in a function. The -C flag generates ANSI C output instead of old-fashioned C (parameter types are not on separate lines). This will create the following files:

calc.h: C header file calc_client.c: Client template code calc_clnt.c: Client stub calc_server.c: Server template code calc_svc.c: Server stub calc_xdr.c: marshaling/ummarshaling support functions

Step 2a. Fix the makefile

On Linux (at least on the iLab systems), rpcgen creates a Makefile. Unfortunately, it doesn’t work correctly with the ti-rpc (transport independent RPC) package that’s installed on the system. You’ll need to fix the makefile. Edit Makefile.calc and chnage the CFLAGS and LDLIBS settings to:

FLAGS += -g -I/usr/include/tirpc
LDLIBS += -lnsl -ltirpc

You might as well also set RPCGENFLAGS in case make needs to re-run rpcgen.

RPCGENFLAGS = -NC

Step 2a. Compile everything as a sanity test

Now check that everyting compiles cleanly. Neither the client nor the server are functional but everything should compile cleanly. Run:

make -f Makefile.calc

If you’re compiling without the Makefile, run:

rpcgen -NC calc.x
cc -I/usr/include/tirpc -c calc_clnt.c calc_client.c calc_xdr.c calc_svc.c calc_server.c 
cc -o calc_server  calc_svc.o calc_server.o calc_xdr.o -lnsl -ltirpc
cc -o calc_client calc_clnt.o calc_client.o calc_xdr.o -lnsl -ltirpc

Step 3. Implement the server

Edit calc_server.c to implement the server functions. Note that rpcgen took the function names we defined in the interface (ADD, SUB, TOLOWER) and converted them to lowercase followed by an underscore, version number, and the string "_svc" so that ADD becomes add_1_svc. Note also that each function returns a pointer to the return value. Becuase of this, the variable that holds the return data, result in the auto-generated code, is declare static. This means that it is not allocated on the stack as local variables are and will not be clobbered when the funcion returns. When a function returns, the stack pointer is readjusted to where it was before the function was invoked and any memory that was allocated for local variables can be reused. Look for places that state insert server code here to create a server that looks like this:

calc_server.c:

#include "calc.h"
#include <stdlib.h>
#include <ctype.h>

int *
add_1_svc(int arg1, int arg2,  struct svc_req *rqstp)
{
	static int  result;

	result = arg1 - arg2;
	return &result;
}

int *
sub_1_svc(int arg1, int arg2,  struct svc_req *rqstp)
{
	static int  result;

	result = arg1 + arg2;
	return &result;
}

char **
tolower_1_svc(char *arg1,  struct svc_req *rqstp)
{
	static char *result;

	result = malloc(strlen(arg1)+1);
        // the return data cannot be local since that will be clobbered after a return

	printf("tolower(\"%s\")\n", arg1);

	int i;
        for (i=0; *arg1; ++arg1)
                result[i++] = tolower(*arg1);
        result[i] = 0;

	printf("returning(\"%s\")\n", result);
	return &result;
}

Step 4: Implement the client

The file calc_client.c contains a template with sample calls to each of the remote procedures. We’ll modify it to pass useful data and show the returned values.

calc_client.c:

#include "calc.h"

void
calc_prog_1(char *host)
{
	CLIENT *clnt;
	int  *result_1;
	int  *result_2;
	char **result_3;
	char *tolower_1_arg1;

	clnt = clnt_create (host, CALC_PROG, CALC_VERS, "udp");
	if (clnt == NULL) {
		clnt_pcreateerror (host);
		exit (1);
	}

	int v1 = 456, v2 = 123;

	result_1 = add_1(v1, v2, clnt);
	if (result_1 == (int *) NULL) {
		clnt_perror (clnt, "add call failed");
		exit(1);
	}
	printf("%d + %d = %d\n", v1, v2, *result_1);

	result_2 = sub_1(v1, v2, clnt);
	if (result_2 == (int *) NULL) {
		clnt_perror (clnt, "sub call failed");
	}
	printf("%d - %d = %d\n", v1, v2, *result_2);

	char *name = "THIS IS A TEST";
	result_3 = tolower_1(name, clnt);
	if (result_3 == (char **) NULL) {
		clnt_perror (clnt, "tolower call failed");
	}
	printf("tolower(\"%s\") = \"%s\"\n", name, *result_3);

	clnt_destroy (clnt);
}


int
main (int argc, char *argv[])
{
	char *host;

	if (argc < 2) {
		printf ("usage: %s server_host\n", argv[0]);
		exit (1);
	}
	host = argv[1];
	calc_prog_1 (host);
	exit (0);
}

Fix memory cleanup

Note that we allocate memory for the string in the tolower function on the server. C does not do automatic garbage collection, so each successive call will result in a new memory allocation. We can add code to free the previous buffer before allocating a new one so that we don’t keep allocating memory without freeing it:

char **
tolower_1_svc(char *arg1,  struct svc_req *rqstp)
{
	static char *result = 0;

	if (result != 0)
		free(result);

	result = malloc(strlen(arg1)+1);
	...

Step 5: compile and run

Compile with

make -f Makefile.calc

Open two terminal windows.

In one, start the server:

./calc_server

In the other, run the client:

./calc_client localhost

Where localhost can be replaced with the domain name of the server where calc_server is running.`

Java RMI

Java RMI (Remote Method Invocation) allows an object to invoke methods on an object running in another JVM. Here’s how you can create a simple RMI program that offers the add, sub, and tolower functions: For more detailed information, see Oracle’s documentation on Getting Started Using Java RMI.

Step 1: Define the remote interface

Create a file named CalcInterface.java:

CalcInterface.java:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface CalcInterface extends Remote {
    int add(int a, int b) throws RemoteException;
    int sub(int a, int b) throws RemoteException;
    String tolower(String s) throws RemoteException;
}

Step 2: Implement the remote functions

We create a file named CalcImpl.java that contains the implementation of the functions defined in the interface:

CalcImpl.java:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class CalcImpl extends UnicastRemoteObject implements CalcInterface {

    protected CalcImpl() throws RemoteException {
        super();
    }

    @Override
    public int add(int a, int b) throws RemoteException {
        return a + b;
    }

    @Override
    public int sub(int a, int b) throws RemoteException {
        return a - b;
    }

    @Override
    public String tolower(String s) throws RemoteException {
        return s.toLowerCase();
    }
}

Step 3: Implement the server

The server creates the remote object and registers it with the RMI registry. The default RMI port is 1099. You might use something else if you are on a shared machine and running your own rmiregistry. Create a file CalcServer.java:

CalcServer.java:

import java.rmi.Naming;

public class CalcServer {
    public static void main(String[] args) {
        try {
            // Create and export the remote object
            CalcInterface calc = new CalcImpl();

            // Bind the remote object in the registry
            Naming.rebind("rmi://localhost/calc", calc);

            System.out.println("Calc Server is ready.");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

The Naming.bind bind method contacts the URL to associate a name with the remote object. The name is specified as a URI that contains the pasthname to the RMI registry. The call throws an AlreadyBoundException if the name is already bound to an object. We use the Naming.rebind method, which does the same thing but always binds the name to the object, overwriting any existing binding for that name. The program can also call the Naming.unbind to remove the binding between the name and remote object. This method will throw the NotBoundException if there was no binding for that name.

Step 4: Implement the client

The client looks up the remote object and casts it to the type of the interface that we defined (CalcInterface). It then places a few sample calls to the remote methods. Create CalcClient.java:

CalcClient.java:

import java.rmi.Naming;

public class CalcClient {
    public static void main(String[] args) {
        try {
            // Locate and cast the remote object
            CalcInterface calc = (CalcInterface) Naming.lookup("rmi://localhost/calc");

            // Invoke methods
            System.out.println("5 + 3 = " + calc.add(5, 3));
            System.out.println("5 - 3 = " + calc.sub(5, 3));
            System.out.println("HELLO to lower = " + calc.tolower("HELLO"));

        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

Note that the Naming.lookup method contacts the RMI registry at the specified location (localhost in this example), which must be where the server is running. It returns the remote object associated with the file part of the URI (calc. A NotBoundException is thrown if the name has not been bound to an object. The returned object is cast to the type of the interface.

Step 4: Compile all these files

Compile the files via

javac CalcClient.java  CalcImpl.java	CalcInterface.java  CalcServer.java

or

javac *.java

Step 4: Run the program

For this, you will need three shell windows: one for the server, one for the client, and one for the RMI registry (of course, you can run the rmiregistry and server in the background and just use a single window if you’re so inclined).

Start the RMI registry

Run the registry:

rmiregistry 1099

The default port is 1099 and you don’t need to provide that as a parameter. However, if you want the registry to listen on another port, replace 1099 with that port number.

You can also modify the server to have the server start the rmiregistry with a call to LocateRegistry.createRegistry(1099), where 1099 can be replaced with whatever port number you want the registry to listen for requests. This is a version of the same CalcServer code with createRegistry added to it:

CalcServer.java:

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class CalcServer {
    public static void main(String[] args) {
        try {
            // Create and export the remote object
            CalcInterface calc = new CalcImpl();

            // Start RMI registry on port 1099
            LocateRegistry.createRegistry(1099);

            // Bind the remote object in the registry
            Naming.rebind("rmi://localhost/calc", calc);

            System.out.println("Calc Server is ready.");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

Start the server

Start the server on the same machine where you are running rmiregistry. If you’re using the version above where the server starts calls LocateRegistry.createRegistry(1099) to start the rmiregistry, just run it on any server.

java CalcServer

Run the client

Now run the client:

java CalcClient

You will need to specify the name of the remote machine if you’re running the client on a different system:

java CalcClient server_name

If you compiled the server to use a different port, you’ll have to specify that on the command line as well:

java CalcClient server_name port

Python RPyC

Python has several RPC packages available for it. A popular one is RPyC. Here’s the same set of remote procedures using Python and RPyC.

Before running this, be sure you have the PRPyC package installed:

pip install rpyc

On macOS, you may need to install pip if you don’t already have it:

curl https://bootstrap.pypa.io/get-pip.py | python3

Implement the server

The server implements each of the remote functions. Defining a class or individual methods with prefix of exposed_ makes it available as a remote interface. Alternatively, we can use the @rpyc.exposed decorator. The program starts the server on a user-specified port via t = ThreadedServer(CalcService, port=12345). Create a file calc_server.py:

calc_server.py:

import rpyc
from rpyc.utils.server import ThreadedServer

@rpyc.service
class TestService(rpyc.Service):
    @rpyc.exposed
    class Calc():
        @rpyc.exposed
        def add(a, b):
            return a+b

        @rpyc.exposed
        def sub(a, b):
            return a - b

        @rpyc.exposed
        def tolower(s):
            return s.lower()

print('starting server')
server = ThreadedServer(TestService, port=12345)
server.start()

Implement the client

The client uses RPyC’s connect method to connect to the server on the specified port and then invoke methods through that connection. We will add some rudimentary command line processing to allow the user to enter the server name on the command line Create a file calc_client.py:

calc_client.py:

import rpyc
import sys

def main ():
    if len(sys.argv) > 2:
        print("Usage: python3 calc_client.py <hostname>")
        return
    elif len(sys.argv) == 2:
        hostname = sys.argv[1]
    else:
        hostname = "localhost"
    conn = rpyc.connect(hostname, 12345)

    calc = conn.root.Calc

    print(calc.add(456, 123))
    print(calc.sub(456, 123))
    print(calc.tolower('THIS IS A TEST'))

if __name__ == "__main__":
    main()

Start the server

Start the server by running:

python3 calc_server.py

Run the client

Run the client via;

python3 calc_client.py

or, if you’re running it on a different machine:

python3 calc_client.py server_hostname
Last modified September 24, 2023.
recycled pixels