RMI only allows you to invoke methods on remote Java objects. At first glance it may appear that this limitation is a weakness of RMI but it actually allows RMI to be far more powerful than other distributed architectures that attempt to operate across different languages. By limiting RMI to just Java objects, the Java Object Model that you're already familiar with can be retained[3]. This includes polymorphism as well as Java's ability to dynamically download code across the network. CORBA, or Common Object Request Broker Architecture, is another popular distributed architectures but it uses a different approach [1]. CORBA allows communication between different programming languages, but in order to allow this a least-common-denominator approach must be used. All remote objects must be based on an Interface Definition Language (IDL) which drastically limits the type of arguments that may be passed in method calls. IDL only allows primitive types or other remote references specified in IDL to be passed across the network. Conventional local objects can not be passed across the network. By using the IDL you can't retain your native object model across the network. This IDL also adds additional complexity to learning how to use CORBA. RMI is quite easy to learn due to it's close alignment with the Java Object Model, but don't let this initial ease of use fool you. It is an extremely powerful system.
RMI was developed as a research project by Sun Microsystems' Sun Laboratories over many years. They looked at various distributed architectures and pooled these experiences together to create a better distributed system. For more information about the history and development of Java RMI see [3] and [4]. One of the key points that they discovered was that most previous distributed systems made the mistake of trying to make the difference between remote objects and local objects transparent to the programmer.
When you invoke a method on a remote object residing on a machine across the network different types of errors occur than when you invoke a method on a local object. RMI forces you to acknowledge this difference between local and remote failure modes by catching additional exceptions. This forces you to design your systems to recover from partial failure, thus allowing you to make more reliable distributed applications.
One of the strongest points of Java is it's platform independent nature. By using a virtual machine, binary code can be generated once and will run anywhere a VM is present. RMI leverages this platform independency to allow complete Java objects to be passed across the network including both their data and functionality. This means that you can send an object across the network and actually have it execute on a remote machine.
By combining RMIs ability to polymorphically pass objects in method invocations, with Java's ability to dynamically download code on demand a very interesting phenomenon happens. In a standard client-server system, if the server is upgraded the client must also be upgraded. With RMI, if you use polymorphism to extend your server side objects, then your clients will automatically download the latest code whenever they need it without requiring a newer version of the client software to be developed and deployed. With polymorphism and dynamic code downloading RMI allows you to use well known object-oriented design patterns, like those detailed in [2] in your distributed designs.
As you can see, RMI unlocks many doors for designing and building distributed systems and allows you to be free to design your distributed applications using all of the power of object orientation. Now that we've gone over what RMI means to developers, lets look into how it works from a developers perspective, and then we'll look at some example RMI programs.
In the RMI world there are two types of objects, local and remote. Remote objects are objects that implement at least one RMI Interface. Local objects are all other objects that do not implement java.rmi.Remote. When you pass local objects in an RMI call they are passed by copy. An identical copy of the complete object graph for that object is made on the remote VM when the call is made. When a remote object is passed in an RMI call it is passed by reference. The entire remote object is not copied into the remote VM, but rather a proxy object is created that points back across the network to the actual object. If you modify the state of a local object passed in an RMI call the original object is not affected. If you modify the state of a remote object in an RMI call the original object is affected.
RMI uses serialization to pass objects across the network [6]. Serialization is a mechanism that allows complete object graphs to be written to a stream. The data on the stream can then be used to re-create the object with both its data and object graph intact. For example you can create a Vector, store a large collection of objects within that Vector, and then write the Vector out using serialization. When you read that Vector back in using serialization, you automatically get all of the contained objects back with their respective data intact. Serialization can also be used quite easily for persistent storage by writing an object to a file. By using the Vector approach, you can store all of your data in one method invocation, and then restore all of your data in one method invocation. While this may not be the most efficient approach as it doesn't allow random-access, it does work quite well for quick and dirty storage.
RMI provides a simple bootstrap server called rmiregistry that allows your clients to get their initial reference to your RMI server object. Once a client has a reference to the remote RMI server any methods from the well known interface may be invoked. The reference that is returned from the rmiregistry is actually a proxy to the real server object. RMI provides a simple compiler called rmic that generates this proxy code for you automatically. After you compile the server that implements your remote interface you simply run rmic on the server object and it generates a client side Stub and a server side Skeleton. The Stub and Skeleton take care of the actual communication between the two VMs.
When designing distributed applications an additional point that should be kept in mind is multi-threading. When you create an RMI server, in theory multiple clients can make simultaneous requests. You must make sure that all of your remote methods are designed in such a way as to be thread safe.
One of the benefits of RMI is that it is designed using multiple layers. As a developer you can choose how deep you wish to understand the system. This paper only discusses the application layer of RMI, which is all you really need to understand to write basic applications using RMI. There are other layers that handle the transport protocol and the way in which remote references are handled. For more information about how these different layers work and how you can go about modifying the way they function take a look at [5].
With this basic understanding of how RMI works we are ready to look at some example programs. We'll start out with a simple object passing client-server application followed by a polymorphic object passing example. We'll then look at a callback example where the server makes RMI calls back to the client program. For our final example we'll look at a sample that implements a well known object-oriented design pattern as documented by [2]. All of the following examples will work with JDK version 1.1 or later.
public class Job implements java.io.Serializable{
String msg;
public Job(){
//Just to distinguish one job from the next
msg = Long.toString(System.currentTimeMillis());
}
public void process(){
//Put complex algorithm here
System.out.println("Job generated on server at: "+msg);
}
}
public interface JobQueue extends java.rmi.Remote{
//Get the next job on the queue
Job getJob() throws java.rmi.RemoteException;
}
import java.rmi.*;
import java.rmi.server.*;
public class JobQueueImpl extends UnicastRemoteObject
implements JobQueue {
public JobQueueImpl() throws RemoteException {
super();
try {
Naming.bind("rmi://localhost/JobQueue",this);
System.out.println("Server Ready");
} catch (Exception e){
System.err.println("Failed to start RMI server: "
+ e.getMessage());
}
}
Naming.bind serves to bind this object to the rmiregistry running on
the host named ``localhost''. By default the registry runs on port 1099
and bind uses this default port number. The argument to bind is in URL
format so an alternate port number can be specified using standard URL
syntax, and if any arguments are omitted default values will be used.
For the future examples the hostname will be left blank so that the
example programs will work without modifications on any hostname. The
rmiregistry associates a textual name with each RMI server and this
server has chosen to be called ``JobQueue.'' Clients will use this
textual name to receive a remote reference to the server from the
rmiregistry. If you attempt to bind twice to the same registry with
the same name an exception will be thrown. The Naming class also
provides a rebind method that will allow you to override a previous
binding.
//JobQueueImpl.java continued...
//The method that will be invoked remotely by clients
public Job getJob(){
return new Job();
}
public static void main(String[] args){
try {
new JobQueueImpl();
} catch (RemoteException e){
System.err.println("Failed to start RMI server: "
+ e.getMessage());
}
}
}
import java.rmi.*;
public class Worker {
public Worker(){
try {
//Get a reference to the remote server on this machine
JobQueue queue = (JobQueue)Naming.lookup(
"rmi:///JobQueue");
//Request a job from the remote server
Job myJob = queue.getJob();
//Start processing the Job.
myJob.process();
} catch(Exception e){
System.err.println("Error connecting to server: "
+e.getMessage());
}
}
public static void main(String[] args){
new Worker();
}
}
To run the above example code, copy the files into a directory and run
the following commands:
Make sure that ``.'' is in your classpath for the rmic command. It uses the Java class loader to load the file you are compiling.
public class Matrix extends Job {
public void process(){
System.out.println("Would be processing a matrix");
}
}
public class Prime extends Job {
public void process(){
System.out.println("Would be computing primes");
}
}
import java.rmi.*;
import java.rmi.server.*;
public class PolyJobQueueImpl extends JobQueueImpl {
int index = 1;
Job[] list = { new Matrix() , new Prime() };
public PolyJobQueueImpl() throws RemoteException {
//JobQueueImpl takes care of the binding for us
super();
}
//Polymorphically extend getJob to return the new Job types
public Job getJob(){
index = (index+1) % list.length; //Walk the index
return list[index];
}
public static void main(String[] args){
try {
new PolyJobQueueImpl();
} catch (RemoteException e){
System.err.println("Failed to start RMI server: "
+ e.getMessage());
}
}
}
To run the above example, simply place the three new files into the
same directory as the first example, then run:
For simplicity sake this example only allows a single client to register but could be easily modified to store a collection of clients. As this is a fairly drastic change in the design of this example, most of the code has changed. The only code that remains the same is the Job class.
Another subtle point that this example shows is how RMI servers don't necessarily have to extend UnicastRemoteObject. They can simply implement an RMI interface and make a call to a static method of UnicastRemoteObject to export themselves. This is useful when your program must extend some other object, like Applet or GenericServlet. When UnicastRemoteObject is not extended equals, hashCode, and toString should be overridden to make sure that clients will function properly. For more information about how to properly extend these methods refer to the [5]. In this example these methods have not been overridden for simplicity sake.
public interface CallbackJobQueue extends java.rmi.Remote{
void register(CallbackWorker aClient) throws java.rmi.RemoteException;
}
import java.rmi.*;
import java.rmi.server.*;
public class CallbackJobQueueImpl extends UnicastRemoteObject
implements CallbackJobQueue {
CallbackWorker client;
public CallbackJobQueueImpl() throws RemoteException {
super();
try {
//Bind to the registry running on this machine
Naming.bind("rmi:///JobQueue",this);
System.out.println("Server Ready");
} catch (Exception e){
System.err.println("Failed to start RMI server: "
+ e.getMessage());
}
//Loop forever and give out jobs
while(true){
if (client != null){
client.processJob(new Job());
}
try {
Thread.sleep(1000); //Sleep for a sec.
} catch(InterruptedException e){}
}
}
public void register(CallbackWorker aClient){
client = aClient;
}
public static void main(String[] args){
try {
new CallbackJobQueueImpl();
} catch (RemoteException e){
System.err.println("Failure with RMI server: "
+ e.getMessage());
}
}
}
public interface CallbackWorker extends java.rmi.Remote{
public void processJob(Job newJob) throws java.rmi.RemoteException;
}
import java.rmi.*;
import java.rmi.server.*;
public class CallbackWorkerImpl implements CallbackWorker{
public CallbackWorkerImpl(){
try {
UnicastRemoteObject.exportObject(this);
CallbackJobQueue server =
(CallbackJobQueue)Naming.lookup(
"rmi:///JobQueue");
//Register with the server, then just wait for jobs
server.register(this);
} catch (Exception e){
System.err.println("Failed to setup for RMI");
}
}
public void processJob(Job newJob){
newJob.process();
}
public static void main(String[] args){
new CallbackWorkerImpl();
}
}
To run the above example, place all of the code into the directory with
the previous examples and run the following commands:
public interface WidgetFactory extends java.rmi.Remote {
public ScrollBar createScrollBar() throws java.rmi.RemoteException;
public Window createWindow() throws java.rmi.RemoteException;
}
public interface ScrollBar extends java.io.Serializable{
public String toString();
}
public interface Window extends java.io.Serializable {
public String toString();
}
import java.rmi.*;
public class Client {
public static void main(String[] args){
try {
WidgetFactory factory = (WidgetFactory)
Naming.lookup("rmi:///WidgetFactory");
System.out.println("Window: "+factory.createWindow());
System.out.println("ScrollBar: "+factory.createScrollBar());
} catch(Exception e){
System.err.println("Failure creating widgets: "
+ e.getMessage());
}
}
}
import java.rmi.*;
import java.rmi.server.*;
public class MotifWidgetFactory extends UnicastRemoteObject
implements WidgetFactory {
public MotifWidgetFactory() throws RemoteException {
super();
try {
Naming.rebind("rmi:///WidgetFactory",this);
System.out.println("Server ready");
} catch (Exception e){
System.err.println("Failure setting up WidgetFactory: "
+ e.getMessage());
}
}
public static void main(String[] args){
try {
new MotifWidgetFactory();
} catch(Exception e){
System.err.println("Failure creating WidgetFactory: "
+ e.getMessage());
}
}
//Remote methods
public Window createWindow(){
return new MotifWindow();
}
public ScrollBar createScrollBar(){
return new MotifScrollBar();
}
}
public class MotifScrollBar implements ScrollBar {
public String toString(){
return "MotifScrollBar";
}
}
public class MotifWindow implements Window {
public String toString(){
return "MotifWindow";
}
}
import java.rmi.*;
import java.rmi.server.*;
public class AWTWidgetFactory extends UnicastRemoteObject
implements WidgetFactory {
public AWTWidgetFactory() throws RemoteException{
super();
try {
Naming.rebind("rmi:///WidgetFactory",this);
System.out.println("Server ready");
} catch(Exception e){
System.err.println("Failure setting up WidgetFactory: "
+ e.getMessage());
}
}
public static void main(String[] args){
try {
new AWTWidgetFactory();
} catch(Exception e){
System.err.println("Failure creating WidgetFactory: "
+ e.getMessage());
}
}
//Remote methods
public Window createWindow(){
return new AWTWindow();
}
public ScrollBar createScrollBar(){
return new AWTScrollBar();
}
}
public class AWTScrollBar implements ScrollBar {
public String toString(){
return "AWTScrollBar";
}
}
public class AWTWindow implements Window {
public String toString(){
return "AWTWindow";
}
}
To run this example place these files into a directory and run the
following commands:
Then terminate the AWTWidgetFactory