Message passing

The central idea of OOP is autonomous objects interacting through messages. When each object runs its own thread, it becomes autonomous. How to make objects communicate through messages. If two threads reside on the same machine, they communicate through shared objects. To get a feel for message passing, we should force the two objects to reside on different machines. In this case, the objects communicate through messages. Java supports Remote Method Invocation for this.

Since RMI is network based, it is essential to understand a little networking terminology. Networking is based on the notion of hosts, servers, and clients. We can treat each computer as a host. Hosts have names ("ieng9") and network address ("132.239.50.9"). Some hosts provide services. These are called servers. These services are identified by either a name ("ftp", "http", etc.) or the associated port numbers. Clients are machines which use these services. The services mentioned above are "standard" services. If new services are added, how do clients come to know of the services? This is done by having a registry service. With a registry service, servers can register services and clients can lookup services and find the corresponding servers.

In RMI framework, each server is an object. The clients can use these objects as if they are local objects. Since the server and client objects can be geographically distributed, they need to communicate through messages. Java provides a good abstraction for this. (The idea of using the network as a software bus for communication between objects is the underlying idea of CORBA for achieving integration of programs written in various languages.)

Suppose you want to provide a compute service. You are willing to take any program and execute it, and return the result. In the language of Java, you are willing to take any object, execute one of its methods, and return the result. To be more specific, we should specify the following.

  1. The type of services the server provides. This is a specification of the server. For example, the following is a spefification of the compute server.
    import java.rmi.*;
    
    public interface ComputeServer extends Remote {
        Object compute(Task task) throws RemoteException;
    }
    
    Here the class ComputeServer provides a service called compute which returns an object of type Object. To run this method, it needs an object of type Task as input.
  2. The types of objects the server is willing to receive. In the above specification, the input to compute method is an object of type Task. We can specify task as
    public interface Task {
        Object calculate();
    }
    
These are the speficications of the server and client. The client provides an object of type Task. The server runs its run() method and returns the result.

Server

Now we can provide an implementation of the compute server.
import java.rmi.*;
import java.rmi.server.*;

public class CServer
    extends UnicastRemoteObject
    implements ComputeServer {

    public CServer() throws RemoteException { }

    public Object compute(Task task) {
       return task.calculate();
    }

   public static void main(String[] args) {
       System.setSecurityManager(new RMISecurityManager());
       try {
          ComputeServer server = new CServer();
          Naming.rebind("//ieng9:2006/ComputeServer", server);
       } catch (Exception e) {
          e.printStackTrace(System.err);
          System.exit(-1);
      }
       System.out.println("Ready to receive tasks");
       return;
   }
}
Points to note about this are:
  1. Definition of the compute method.
  2. Biding the name of the service ComputeServer at the registry ieng9:2006.
  3. The use of security manager. (This will be discussed later. You can ignore it for now.)

Client

The client is any object looking forward to executing some code. The following client computes the powers of numbers.
import java.rmi.*;
import java.rmi.registry.*;

public class RecPower implements Task, java.io.Serializable {
     private int m, n;

     RecPower(int m, int n) {
         this.m = m;
         this.n = n;
     }

     public Object calculate() {
         int res = recPower(m, n);
         return new Integer(res);
     }

     int recPower(int m, int n) {
        if (0 == n) return m;
        int j = recPower(m, n/2);
        if (0 == n/2) return j*j;
          else return j*j*n;
     }

     public static void main(String[] args) {
         System.setSecurityManager(new RMISecurityManager());
         RecPower rp = new RecPower(11, 10);
         try {
            ComputeServer csi
               = (ComputeServer) Naming.lookup("//ieng9:2006/ComputeServer");
            Integer power = (Integer) csi.compute(rp);
            System.out.println(power);
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
}

Running the code

You have to compile the above classes. But running the code requires a little more work.
  1. Note that the server and the client need to communicate using the network. The code for this can be automatically created as "skeleton" and "stub" classe files. You have to use rmic CServer to create these files.
  2. In a general RMI scenario, there will be lot of services provided by different objects. Each server should register itself at a registry and clients can do service lookup at the registry for the services they need. For this, the registry itself should be started. This is done by the command
    rmiregistry 2006 &
    for the above code. (If you find that this results in an error, you should change the port number in the above code, compile it, and start rmiregistry at the new port number.)

A simpler example

In this example, the server provides just one service: calculating squares. The interface description for the server is given below.
import java.rmi.*;

interface SquareI extends Remote {
  long square(int x) throws RemoteException;
}
The server class implements this method. The main method starts the server.
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.net.*;

public class Square extends UnicastRemoteObject implements SquareI {

  Square() throws RemoteException { }

  public long square(int x) throws RemoteException {
    return x*x;
  }

  public static void main(String[] args) {
    System.setSecurityManager(new RMISecurityManager());
    try {
      Square s = new Square();
      Naming.bind("//ieng9/Square", s);
      System.out.println("Ready to compute squares");
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
}
The client calculates the squares of first ten numbers using the remote service.
import java.rmi.*;
import java.rmi.registry.*;

public class SquareClient {
  public static void main(String[] args) {
    System.setSecurityManager(new RMISecurityManager());
    try {
      SquareI t = (SquareI)Naming.lookup( "//ieng9/Square");
      for(int i = 0; i < 10; i++)
          System.out.println("Square" +"("+i+") = " + t.square(i));
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
}

Behind the scene operations

  1. In RMI, the server and the client do not communicate directly. The communication pattern is shown as below.
             +--------+         +--------+
             |        |         |        |     +--------------+
             |        |         |        |     | RMI REGISTRY |
             |Server  |         | Client |     +--------------+
             |        |         |        |
             +--------+         +--------+
                 |                  |
                 |                  |
                 |                  |
                 |                  |
             +--------+         +--------+
             |        |         |        |
             |        |         |        |
             |Skel    | <-----> | Stub   |
             |        |         |        |
             +--------+         +--------+
    
    The skeletons and stubs are created using rmic.
  2. How does the client obtain the stub? This is obtained through the Naming.lookup() method.
  3. RMI uses serialization mechanism to transfer the stub object from the serve to the client. In this example, the client itself should run its methods on the remote server. So serialization is required for the client also. So the client declares itself as implementing java.io.Serializable. The actual serialization is done by Java itself.
  4. Since both the client and the server need to execute foreign untrusted code, they start a security manager. They can define their own security manager to suit their policies.