Remote Method Invocation

Merging distributed processing and object orientation- the two core software technologies of the 1990s- is achieved through remote method invocation (RMI). As its name suggests, object A can invoke a service of object B using ordinary method invocation syntax: B.service(), even if A and B reside in different address spaces or different computers.

Remote method invocation is accomplished using an object request broker architecture (ORB). Using an abstract server base class, a special compiler generates two server proxies: a client-side proxy called a stub, and a server-side proxy is called a skeleton.

The stub is a client-side remote proxy. When the client invokes a stub method, the stub transmits the request, together with its marshaled (i.e., serialized) parameters, across the network to the skeleton. The skeleton is a server-side remote proxy. It unmarshals the parameters, then passes them on to the server. The result returned by the server, if any, is sent back to the client through the reverse process.

When a server comes into existence, it registers its name with a name server called a broker or dispatcher. When the client requests a reference to the server, the stub searches the broker for the address of the skeleton.

ORB architectures become very complicated when the client is a Java object and the server is a C++ or Smalltalk object. In this case a stub and skeleton must also translate to and from a common object model such as COM (used by ActiveX) or IDL (used by CORBA). If client and server are both Java objects, then the stub and skeleton can be generated from a server interface using a tool called the RMI compiler (rmic).

As near as I can remember, the steps are these:

1. Declare the server interface. This must extend the remote interface, but no specifications are inherited:

interface Server extends remote { ... } 2. Define the server as an implementation of this interface: class ServerImpl implements Server { ... } 3. Define the client: class Client { ... } 4. Generate the stub and skeleton from the interface: rmic Server.java 5. The java RMI broker is called Naming. Start it: start Naming 6. Start the server.

7. Start the client.

Example: Remote Observers and Observables

Recall that the Observer-Observable design pattern solves the problem of how a model (the observable) can notify its views (the observers), which dynamically vary in number and type, of its state changes. One of the deficiencies of Java's Observer-Observable implementation is that observer and observable must be in the same address space. What do we do if the observer and observable are in different address spaces?

To solve this problem we introduce the concept of remote observers and remote observables. Remote observers and observables are examples of peers. A remote observer sees the remote observable as a server that provides addObserver() and deleteObserver() services. Assuming RMI is used for communication, the remote observer will invoke the addObserver() and deleteObserver() methods of a remote observable stub, which implements the same interface as the remote observable itself.

A remote observable sees the remote observer as a server that provides an update() service. Assuming RMI is used for communication, the notifyObservers() method will call the update() method for each remote observer stub it contains that implements the same interface as the actual remote observer.

Remote Observer Interface

Here's the definition of RemoteObserver interface. Like the Serializable interface, the Remote interface is empty:

import java.rmi.*;
import java.util.*;
import java.io.*;

public interface RemoteObserver extends Remote
{
   void update(RemoteObservable o, Object args) throws RemoteException;
}

Remote Observable Interface

Here's the RemoteObservable interface. We don't need notifyObservers(), because it won't be called remotely:

public interface RemoteObservable extends Remote
{
   void addObserver(RemoteObserver ro) throws RemoteException;
   void deleteObserver(RemoteObserver ro) throws RemoteException;
}
Proxy Observers

Unfortunately, remote observers cannot be observers because they do not implement the Observer interface. This happens because a remote observer's update function must throw a remote exception when a transmission error occurs. This makes it different from the inherited update specification. We would still like to use the machinery provided by the Observable class. To solve this problem, we introduce the idea of a proxy observer. A proxy observer implements the observer interface, but it is associated with a remote observer stub. The proxy observer's update() function calls the update() function of its associated remote observer stub:

class ProxyObserver implements Observer
{
   public ProxyObserver(RemoteObserver ro)
   {
      myRemote = ro;
   }

   public void update(Observable obs, Object arg)
   {
      try
      {
         if (obs instanceof RemoteObservable)
            myRemote.update((RemoteObservable)obs, arg);
      }
      catch(Exception re)
      {
         System.err.println("Remote Observer error: " + re);
         re.printStackTrace();
      }
   }

   private RemoteObserver myRemote;
}

Remote Observable Implementation

Our implementation of the remote observable interface can extend the Observable class. The addObserver() method creates a proxy observer to be associated with its remote observer argument. The proxy observer is passed to the addObserver() method inherited from the Observer super class. The association is recorded in a local hash table (see the on-line documentation for Hashtable) maintained by the remote observable implementation. deleteObserver() uses this table to locate and delete the corresponding proxy observer.

Here's the code:

class RemoteObservableImpl extends Observable
implements RemoteObservable, Serializable
{

   public RemoteObservableImpl() throws RemoteException
   {
      UnicastRemoteObject.exportObject(this); // ???
      myProxies = new Hashtable();
   }

   public void addObserver(RemoteObserver ro) throws RemoteException
   {
      ProxyObserver po = new ProxyObserver(ro);
      myProxies.put(ro, po);
      super.addObserver(po);
   }

   public void deleteObserver(RemoteObserver ro) throws RemoteException
   {
      ProxyObserver po = (ProxyObserver) myProxies.remove(ro);
      super.deleteObserver(po);
   }

   private Hashtable myProxies;
}

Application: The Reactor (again)

As a concrete application, let's revisit our nuclear reactor example. This time the reactor will be a remote observable, and the alarm will be a remote observer. Here's the architecture, the dashed line represents a machine boundary:

Reactor Interface

We begin by defining a Reactor interface that extends the RemoteObservable interface:

public interface Reactor extends RemoteObservable
{
   void incTemp() throws RemoteException;
   void decTemp() throws RemoteException;
   double getTemp() throws RemoteException;
   boolean isCritical() throws RemoteException;
}
Reactor Implementation

The reactor implementation extends the remote observable implementation and implements the Reactor interface. The constructor calls a special static function that declares reactor implementation objects to be "exportable."

class ReactorImpl extends RemoteObservableImpl implements Reactor
{

   public ReactorImpl(double t) throws RemoteException
   {
      UnicastRemoteObject.exportObject(this); // ???
      temp = t;
   }

   public void incTemp() throws RemoteException
   {
      temp += delta1;
      setChanged();
      if (isCritical()) notifyObservers();
   }

   public void decTemp() throws RemoteException
   {
      temp -= delta2;
      notifyObservers(new Double(temp));
   }

   public double getTemp() throws RemoteException
   {
      return temp;
   }

   public boolean isCritical()
   {
      return (temp >= 1000);
   }

   private double temp;
   private double delta1 = 10;
   private double delta2 = 1;

   public static void main(String[] args) { ... }

}

The reactor implementation runs as an independent process, and therefore must have its own main function. Main must load a new security manager and it must register itself with the local naming registry: public static void main(String[] args)
{

   // Create and install a security manager
   System.setSecurityManager(new RMISecurityManager());

   try
   {
      ReactorImpl r = new ReactorImpl(995);
      Naming.rebind("Reactor", r); // register with broker
      System.out.println("Reactor bound in registry");

      boolean more = true;

      BufferedReader in
       = new BufferedReader(new InputStreamReader(System.in));

      while(more)
          try
         {
            System.out.println(
               "reactor temperature = " + r.getTemp() + " degrees");
            if (r.isCritical()) System.out.println("reactor is too hot");
            System.out.print("enter q, i, g, or d> ");
            String s = in.readLine();
            if (s.equals("i")) r.incTemp();
            else if (s.equals("d")) r.decTemp();
            else if (s.equals("q")) more = false;
          }
          catch(IOException e) {}

   }
   catch (Exception e)
   {
      System.out.println("Reactor error: " + e.getMessage());
      e.printStackTrace();
   }
}

Alarm Implementation

The client is an alarm, which implements the remote observer and serializable interfaces. It too must make its instances exportable. The constructor consults the broker for a remote reference to the reactor

 
class Alarm implements RemoteObserver, Serializable
{
   public Alarm(String reactorName, int id) throws RemoteException
   {
      UnicastRemoteObject.exportObject(this); // ???
      ringing = false;
      idNum = id;
      String server = "Reactor";

      try
      {
         // Naming.lookup(name) returns a Remote
         myReactor =
            (Reactor)Naming.lookup(server + reactorName);
      }
      catch (Exception e)
      {
         System.err.println("Alarm exception: " + e.getMessage());
         e.printStackTrace();
      }

   } // constructor

   public void subscribe()
   {
      try
      {
         myReactor.addObserver(this);
      }
      catch (Exception e)
      {
         System.err.println("Subscription exception: " + e);
         e.printStackTrace();
      }
   }

   public void update(RemoteObservable o, Object arg)
   throws RemoteException
   {
      ringing = true;
      System.out.println("Warning: reactor is too hot");
   }

   public static void main(String[] args) { ... }

   private boolean ringing;
   private int idNum;
   private Reactor myReactor;
}

Like the reactor implementation, the alarm runs as an independent process, hence must implement a main function. This function creates a new security manager, registers the alarm with the local broker, then subscribes to the reactor:    public static void main(String[] args)
   {
   // Create and install a security manager
      System.setSecurityManager(new RMISecurityManager());

      try
      {
         Alarm alarm1 = new Alarm("Reactor", 1);
         Naming.rebind("Alarm1", alarm1);
         System.out.println("Alarm #1 bound in registry");
         alarm1.subscribe();
      }
      catch (Exception e)
      {
         System.out.println("Alarm error: " + e.getMessage());
         e.printStackTrace();
      }
   }