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:
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:
public interface RemoteObserver
extends Remote
{
void update(RemoteObservable
o, Object args) throws RemoteException;
}
Here's the RemoteObservable interface. We don't need notifyObservers(), because it won't be called remotely:
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:
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;
}
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:
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:
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."
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) { ... }
}
// 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();
}
}
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
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;
}
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();
}
}