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