Book Home Java Distributed Computing Search this book

3.6. Java RMI

The Java Remote Method Invocation (RMI) package is a Java-centric scheme for distributed objects that is now a part of the core Java API. RMI offers some of the critical elements of a distributed object system for Java, plus some other features that are made possible by the fact that RMI is a Java-only system. RMI has object communication facilities that are analogous to CORBA's IIOP, and its object serialization system provides a way for you to transfer or request an object instance by value from one remote process to another.

3.6.1. Remote Object Interfaces

Since RMI is a Java-only distributed object scheme, all object interfaces are written in Java. Client stubs and server skeletons are generated from this interface, but using a slightly different process than in CORBA. First, the interface for the remote object has to be written as extending the java.rmi.Remote interface. The Remote interface doesn't introduce any methods to the object's interface; it just serves to mark remote objects for the RMI system. Also, all methods in the interface must be declared as throwing the java.rmi.RemoteException . The RemoteException is the base class for many of the exceptions that RMI defines for remote operations, and the RMI engineers decided to expose the exception model in the interfaces of all RMI remote objects. This is one of the drawbacks of RMI: it requires you to alter an existing interface in order to apply it to a distributed environment.

3.6.2. Server Implementations

Once the remote object's Java interface is defined, a server implementation of the interface can be written. In addition to implementing the object's interface, the server also typically extends the java.rmi.server.UnicastRemoteObject class. UnicastRemoteObject is an extension of the RemoteServer class, which acts as a base class for server implementations of objects in RMI. Subclasses of RemoteServer can implement different kinds of object distribution schemes, like replicated objects, multicast objects, or point-to-point communications. The current version of RMI (1.1) only supports remote objects that use point-to-point communication, and UnicastRemoteObject is the only subclass of RemoteServer provided. RMI doesn't require your server classes to derive from a RemoteServer subclass, but doing so lets your server inherit specialized implementations of some methods from Object (hashCode(), equals(), and toString()) so that they do the right thing in a remote object scenario. If you decide that you don't want to subclass from a RemoteServer subclass for some reason, then you have to either provide your own special implementations for these methods or live with the fact that these methods may not behave consistently on your remote objects. For example, if you have two client stubs that refer to the same remote object, you would probably want their hashCode() methods to return the same value, but the standard Object implementation will return independent hash codes for the two stubs. The same inconsistency applies to the standard equals() and toString() methods.

3.6.3. The RMI Registry

In RMI, the registry serves the role of the Object Manager and Naming Service for the distributed object system. The registry runs in its own Java runtime environment on the host that's serving objects. Unlike CORBA, the RMI registry is only required to be running on the server of a remote object. Clients of the object use classes in the RMI package to communicate with the remote registry to look up objects on the server. You start an RMI registry on a host by running the rmiregistry command, which is included in the standard JDK distribution. By default, the registry listens to port 1099 on the local host for connections, but you can specify any port for the registry process by using a command-line option:

objhost% rmiregistry 4001

Once the registry is running on the server, you can register object implementations by name, using the java.rmi.Naming interface. We'll see the details of registering server implementations in the next section. A registered class on a host can then be located by a client by using the lookup() method on the Naming interface. You address remote objects using a URL-like scheme. For example,

MyObject obj1 =
    (MyObject)Naming.lookup("rmi://objhost.myorg.com/Object1");

will look up an object registered on the host objhost.myorg.com under the name Object1. You can have multiple registries running on a server host, and address them independently using their port assignments. For example, if you have two registries running on the objhost server, one on port 1099 and another on port 2099, you can locate objects in either registry using URLs that include the port numbers:

MyObject obj1 = 
    (MyObject)Naming.lookup("rmi://objhost.myorg.com:1099/Object1");
MyObject obj2 =
    (MyObject)Naming.lookup("rmi://objhost.myorg.com:2099/Object2");

3.6.4. Client Stubs and Server Skeletons

Once you've defined your object's interface and derived a server implementation for the object, you can create a client stub and server skeleton for your object. First the interface and the server implementation are compiled into bytecodes using the javac compiler, just like normal classes. Once we have the bytecodes for the interface and the server implementation, we have to generate the linkage from the client through the RMI registry to the object implementation we just generated. This is done using the RMI stub compiler, rmic. Suppose we've defined a remote interface called MyObject, and we've written a server implementation called MyObjectImpl, and compiled both of these into bytecodes. Assuming that we have the compiled classes in our CLASSPATH, we would generate the RMI stub and skeleton for the class with the rmic compiler:

myhost% rmic MyObject

The rmic compiler bootstraps off of the Java bytecodes for the object interface and implementation to generate a client stub and a server skeleton for the class. A client stub is returned to a client when a remote instance of the class is requested through the Naming interface. The stub has hooks into the object serialization subsystem in RMI for marshaling method parameters.

The server skeleton acts as an interface between the RMI registry and instances of the object implementation residing on a host. When a client request for a method invocation on an object is received, the skeleton is called on to extract the serialized parameters and pass them to the object implementation.

3.6.5. Registering and Using a Remote Object

Now we have a compiled interface and implementation for our remote object, and we've created the client stub and server skeleton using the rmic compiler. The final hurdle is to register an instance of our implementation on a remote server, and then look up the object on a client. Since RMI is a Java-centric API, we can rely on the bytecodes for the interface, the implementation, the rmic-generated stub, and skeleton being loaded automatically over the network into the Java runtimes at the clients. A server process has to register an instance of the implementation with a RMI registry running on the server:

MyObjectImpl obj = new MyObjectImpl();
Naming.rebind("Object1", obj);

Once this is done, a client can get a reference to the remote object by connecting to the remote registry and asking for the object by name:

System.setSecurityManager(new java.rmi.RMISecurityManager());
MyObject objStub = (MyObject)Naming.lookup("rmi://objhost/Object1");

Before loading the remote object stub, we installed a special RMI security manager with the System object. The RMI security manager enforces a security policy for remote stubs to prevent them from doing illicit snooping or sabotage when they're loaded into your local Java environment from a network source. If a client doesn't install an RMI security manager, then stub classes can only be loadable from the local file system.

3.6.6. Serializing Objects

Another Java facility that supports RMI is object serialization. The java.io package includes classes that can convert an object into a stream of bytes and reassemble the bytes back into an identical copy of the original object. Using these classes, an object in one process can be serialized and transmitted over a network connection to another process on a remote host. The object (or at least a copy of it) can then be reassembled on the remote host.

An object that you want to serialize has to implement the java.io.Serializable interface. With this done, an object can be written just as easily to a file, a string buffer, or a network socket. For example, assuming that Foo is a class that implements Serializable, the following code writes Foo on an object output stream, which sends it to the underlying I/O stream:

Foo myFoo = new Foo();
OutputStream out = ... // Create output stream to object destination
ObjectOutputStream oOut = new ObjectOutputStream(out);
oOut.writeObject(myFoo);

The object can be reconstructed just as easily:

InputStream in = ... // Create input stream from source of object
ObjectInputStream oIn = new ObjectInputStream(in);
Foo myFoo = (Foo)oIn.readObject();

We've simplified things a bit by ignoring any exceptions generated by these code snippets. Note that serializing objects and sending them over a network connection is very different from the functionality provided by the ClassLoader, which we saw earlier in this book. The ClassLoader loads class definitions into the Java runtime, so that new instances of the class can be created. The object serialization facility allows an actual object to be serialized in its entirety, transmitted to any destination, and then reconstructed as a precise replica of the original.

When you serialize an object, all of the objects that it references as data members will also be serialized, and all of their object references will be serialized, and so on. If you attempt to serialize an object that doesn't implement the Serializable interface, or an object that refers to non-serializable objects, then a NotSerializableException will be thrown. Method arguments that aren't objects are serialized automatically using their standard byte stream formats.

In RMI, the serialization facility is used to marshal and unmarshal method arguments that are objects, but that are not remote objects. Any object argument to a method on a remote object in RMI must implement the Serializable interface, since the argument will be serialized and transmitted to the remote host during the remote method invocation.

3.6.7. An RMI Solver

Now let's go back to our Solver example and distribute it using RMI. First, we would have to rewrite the Solver interface so that it implements the java.rmi.Remote interface. The methods on the interface also have to be specified as throwing RemoteExceptions. This modified version of the Solver interface, the RMISolver, is shown in Example 3-10.

Example 3-10. Interface for a RMI Solver

package dcj.examples.rmi;

import java.rmi.*;
import java.io.OutputStream;

public interface RMISolver extends java.rmi.Remote
{
  public boolean solve() throws RemoteException;
  public boolean solve(RMIProblemSet s,
                       int numIters) throws RemoteException;

  public RMIProblemSet getProblem() throws RemoteException;
  public boolean setProblem(RMIProblemSet s) throws RemoteException;
  public int getIterations() throws RemoteException;
  public boolean setIterations(int numIter) throws RemoteException;
}

There are two methods in this interface, the solve() method with arguments, and the setProblem() method, where we have problem set arguments that we want to pass into the remote method invocation. We could achieve this by creating a version of the ProblemSet class that implements the Serializable interface. If we did that, the problem set would be sent to the remote host by value--the remote object would be operating on a copy of the problem set. But in both of these cases we want to pass the problem set by reference; we want the remote Solver to operate on the same problem set object that we have on the client, so that when the solution is stored in the problem set, we will see it automatically on the client. We can do this in RMI by making a remote version of the ProblemSet class. With an RMI-enabled ProblemSet interface, we can use an instance of an implementation of the interface as an argument to remote methods, and the remote object will receive a stub to the local ProblemSet. The RMI version of the ProblemSet interface, the RMIProblemSet, is shown in Example 3-11.

Example 3-11. Interface for an RMI ProblemSet

package dcj.examples.rmi;

import java.rmi.*;

public interface RMIProblemSet extends Remote {
  public double getValue() throws RemoteException;
  public double getSolution() throws RemoteException;
  public void setValue(double v) throws RemoteException;
  public void setSolution(double s) throws RemoteException;
}

Now we'll need to write server-side implementations of these interfaces. Our server-side implementation of the RMISolver derives from java.rmi.Uni-castRemoteObject, and is shown in Example 3-12. The implementation of the RMIProblemSet interface is shown in Example 3-13. It also extends the UnicastRemoteObject class.

Example 3-12. Implementation of the RMISolver

package dcj.examples.rmi;

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.io.*;

public class RMISolverImpl
    extends UnicastRemoteObject
    implements RMISolver {

  // Protected implementation variables
  protected int numIterations = 1; // not used for this Solver...
  protected RMIProblemSet currProblem = null;

  // Constructors
  public RMISolverImpl() throws RemoteException { super(); }
  public RMISolverImpl(int numIter) throws RemoteException {
    super();
    numIterations = numIter;
  }

  // Public methods
  public boolean solve() throws RemoteException {
    System.out.println("Solving current problem...");
    return solve(currProblem, numIterations);
  }

  public boolean solve(RMIProblemSet s, int numIters) 
      throws RemoteException {
    boolean success = true;

    if (s == null) {
      System.out.println("No problem to solve.");
      return false;
    }

    System.out.println("Problem value = " + s.getValue());

    // Solve problem here...
    try {
      s.setSolution(Math.sqrt(s.getValue()));
    }
    catch (ArithmeticException e) {
      System.out.println("Badly-formed problem.");
      success = false;
    }

    System.out.println("Problem solution = " + s.getSolution());

    return success;
  }

  public RMIProblemSet getProblem() throws RemoteException {
    return currProblem;
  }
  
  public boolean setProblem(RMIProblemSet s) throws RemoteException {
    currProblem = s;
    return true;
  }

  public int getIterations() throws RemoteException {
    return numIterations;
  }
  
  public boolean setIterations(int numIter) throws RemoteException {
    numIterations = numIter;
    return true;
  }

  public static void main(String argv[]) {
    try {
      // Register an instance of RMISolverImpl with the
      // RMI Naming service
      String name = "TheSolver";
      System.out.println("Registering RMISolverImpl as \"" + name + "\"");
      RMISolverImpl solver = new RMISolverImpl();
      Naming.rebind(name, solver);
      System.out.println("Remote Solver ready...");
    }
    catch (Exception e) {
      System.out.println("Caught exception while registering: " + e);
    }
  }
}

Example 3-13. Implementation of the RMIProblemSet

package dcj.examples.rmi;

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class RMIProblemSetImpl
    extends java.rmi.server.UnicastRemoteObject
    implements RMIProblemSet {

  protected double value;
  protected double solution;
  
  public RMIProblemSetImpl() throws RemoteException {
    value = 0.0;
    solution = 0.0;
  }
  
  public double getValue() throws RemoteException {
    return value;
  }
  
  public double getSolution() throws RemoteException {
    return solution;
  }
  
  public void setValue(double v) throws RemoteException {
    value = v;
  }
  
  public void setSolution(double s) throws RemoteException {
    solution = s;
  }
}

These implementations of our Solver and ProblemSet interfaces are very similar to those that we created for the earlier CORBA examples. As in our earlier examples, the Solver simply performs a square root on the ProblemSet floating-point value. The RMISolverImpl has a main() method that registers a RMISolverImpl object with the local RMI registry.

Now we compile our interfaces and our server implementations into bytecodes, then generate their client stubs and server skeletons using the rmic compiler:

myhost% rmic dcj.examples.rmi.RMIProblemSetImpl
myhost% rmic dcj.examples.rmi.RMISolverImpl

The last required item is a client to use our remote object. The RMISolverClient in Example 3-14 is a simple client for the remote solver. The client has a single main() method where it gets a stub for the remote solver and asks it to solve a problem. The first line of the main() method installs the RMISecurityManager. Next, the client looks up the solver registered on the remote server through the Naming.lookup() method. Once it has the RMISolver stub, it creates a RMIProblemSetImpl object (our RMI-enabled ProblemSet implementation), and passes it into the solver's solve() method. The remote solver receives a stub to the RMIProblemSetImpl object on the client host, and solves the problem it represents. The methods that the remote RMISolver calls on the RMIProblemSet stub are invoked remotely on the RMIProblemSetImpl object on the client. Once the solve() method returns, our client can get the problem solution from the RMIProblemSetImpl object that it passed into the remote method call.

Example 3-14. An RMISolver Client

package dcj.examples.rmi;

import java.rmi.*;
import java.rmi.server.*;

public class RMISolverClient {
  public static void main(String argv[]) {
    // Install a security manager that can handle remote stubs
    System.setSecurityManager(new RMISecurityManager());

    // Get a remote reference to the RMISolver class
    String name = "rmi://objhost.myorg.com/TheSolver";
    System.out.println("Looking up " + name + "...");
    RMISolver solver = null;
    try {
      solver = (RMISolver)Naming.lookup(name);
    }
    catch (Exception e) {
      System.out.println("Caught an exception looking up Solver.");
      System.exit(1);
    }

    // Make a problem set for the solver
    RMIProblemSetImpl s = null;
    
    try {
      s = new RMIProblemSetImpl();
      s.setValue(Double.valueOf(argv[0]).doubleValue());
    }
    catch (Exception e) {
      System.out.println("Caught exception initializing problem.");
      e.printStackTrace();
    }

    // Ask solver to solve
    try {
      if (solver.solve(s, 1)) {
        System.out.println("Solver returned solution: " +
                           s.getSolution());
      }
      else {
        System.out.println(
          "Solver was unable to solve problem with value = " +
          s.getValue());
      }
    }
    catch (RemoteException e) {
      System.out.println("Caught remote exception.");
      System.exit(1);
    }
  }
}

Finally, we're ready to try our distributed object system. First, we start a registry on the host that is serving objects through the Naming service:

objhost% rmiregistry &

Now we can register a RMISolverImpl object by running the main() method on the RMISolverImpl class:

objhost% java dcj.examples.rmi.RMISolverImpl
Registering RMISolverImpl as "TheSolver"
Remote Solver ready...

Back on our client host, we can run the client class:

client% java dcj.examples.rmi.RMISolverClient 47.0
Looking up "rmi://objhost.myorg.com/TheSolver"...
Solver returned solution: 6.855654600401044

Our remote solver has solved our problem for us.

It's important to note here that the ProblemSet we're sending to the remote Solver object through a remote method call isn't being served in the same way as the Solver object. The Solver server doesn't need to lookup the ProblemSet object through the RMI registry. A stub interface to the client-side RMIProblemSetImpl object is automatically generated on the server side by the underlying RMI system.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.