Java's RMI: Introduction
Last class, we talked about ONC RPC and XDR. RPC and XDR provided a facility for remote procedure calls across platforms. But, it did not provide support for the object extraction -- an RPC program is really nothing more than a collection of RPC procedures. The RPC mechanism does not provide any way to represent an instantiation of a program with private state or any other way of adaquately modeling objects.
This class, we're going to talk about Java's RMI mechanism, which provides a beautiful and highly transparent model for Remote Method Invocation (RMI). Although Java's RMI supports the object abstraction, it in some sense has it easy. Unlike RPC, it does provide true support for interoperability. Instead, it provides the Java Native Interface (JNI) to allow other languages to adapt to it. CORBA, which we'll discuss next class, is a remote object solution that was designed specifically with interoperability in mind.
The Big Picture
The figure below illustrates the model used by Java's RMI facility.
Remote Objects and Remote Object References
Java remote objects are exactly the same objects as Java's local objects. Any object which implements a Remote interface can be used as a remote object (programmers might also want their remote object classes to extend UnicastRemote, because this fixes some methods of the Object class, which are otherwise non-functional due to exception issues that can't be made transparent).
For those less familiar with Java, remember that an interface serves roughly the same function as a C/C++ header file with function prototypes or a class definition -- except that Java enforces it rigorously.
Java identifies objects using references. References are nothing more than names for objects. Typical references, such as those contained within Java's primitive reference variables are local references. That is to say that these references are capable of naming object only within a single JVM. Internally, Java also implements remote object references. That is to say that Java can name on object within one JVM from another JVM.
Java application programmers never actually object known as a stub within the client's JVM. The application programmer interacts with this stub as a proxy for the remote object. The stub, in cooperation with the Java runtime, interacts with the remote object. In other words, for practical purposes, the programmer just pretends that the stub object is the remote object -- and it all works out.
Java application programmers never actually posess a reference to a remote object. Instead, Java represents objects to remote callers by placing a proxy object, known as a stub, locally within the caller's JVM. It is the job of this stub to handle the marshalling of the method invocation into a message, the delivery of the message to the communication module, and the reverse of this process, all the way to the client object, upon the methods return. Since the stub object and the remote object implement the same interface, the client can interact with the stub exactly as it would the remote object, even though they are technically different classes.
There is at least one instance of the stub class for each remote object in use within the JVM. If there are several remote objects, even if they are of the same type, there are several different instances of the stub class, one for each. Each instance of the stub class contains the remote object reference for the object that it represents.
In order to ensure that only one stub exist for each remote object, Java's RMI maintains a mapping between the remote object reference and the local reference to the stub.
In one of Java's more magic features, if the class for the stub is not already available on the client, it can be downloaded from the server via HTTP. Java achieves this by sending the URL of the stub's .class file along with the reference to the remote object.
In the original version of RMI, there was a server-side compliment to the stub, known as the skeleton. The skeleton, like the stub, was responsible for marshalling. Java 2 eliminated the need for the server-side skeletons. It did this by factoring this functionality a common component, which I call the proxy dispatcher. This was possible, because no part of the process is necessarily unique to the particular target object. The unmarshalling of the method call is a mechanical step which yields a local object reference, a method to invoke, and the parameters to this method. Once this is known, the process of invoking the method using the local reference is the same for all objects. And, the last step, the marshalling of the return value, is just as mechanical as the initial unmarshalling.
The Stub Compiler: rmic
Just as RPC provides a tool, rpcgen, to generate the stubs, so does RMI. RMI's tool is called "rmic". The application programmer feeds "rmic" the .class file, and "rmic" generates both the stub and skeleton classes. As discussed above, the skeleton classes are only needed for Java 1.
Pass by Value vs. Pass by Reference
In Java, parameters are passed into methods and returned from methods by reference. This is problematic for an RMI facility, because not all objects can be remote objects - not all JVMs are willing to expose any of their objects, never mind all of them. As a result, the RMI facility needs to determine which object can be passed by reference, and which can't. And, it needs to have some mechanism for handling those that can't be passed by reference.
To address the first concern, Java has a very simple rule. Any object that is to be remotely accessible must be an instance of a class that implements the Remote interface. Objects that implement the Remote interface are passed by reference into methods and when they are returned from methods. Other objects are passed by value, in other words by creating local copies.
Java passes object by value using a process known as serialization. Basically, this means that Java flattens out the object, copies it, and sends this copy to the other side. At the other side, the object is recreated from the serialized copy, and a reference to this recreated object is used. Java needs to have an object's .class file to reconstitute it from the serialized copy.
In order to recreate an object from a serialized copy, the object's .class file is needed. To facilitate this, Java sends the URL for the .class file along with the serialized copy. If the recipient doesn't already have the .class file, it can download it via HTTP using the provided URL.
The result of this process is that there are two copies - one on each side. The client's JVM has one copy and the server's JVM has another. Each acts on its own copy. The object has been passed by value.
When Java passes an object by reference, it does this by passing a remote reference to the object, along with the URL of the stub class. This enables the recipient to recreate the stub object just as it did objects passed by value. As before, if the recipient doesn't already have a copy of the defining .class file, it can download it using HTTP via the provided URL.
The process of recreating a remote object or stub on the local system is called localization. Pass by value localizes the remote object, pass by reference localizes a stub for the remote object.
Failure and Exceptions
Unlike local method calls, remote method calls can fail. As an example, the network could be down or partitioned. Java's native RMI handles this by requiring that all methods of remote objects throw RemoteException. This, in turn, requires that each use of a remote method catch the RemoteException.
Finding Remote Objects
Most remote objects are "found" when references to them are returned by methods invoked on other remote objects. But, for obvious reasons, this mechanism does not explain how all remote objects are found -- we need to find the first object somehow.
Java does this using a server program known as the RMIregistry. Servers that create remote objects designed to be the first point of contact by a client can register these remote objects using bind() or rebind(), which take a common, URL-style name and the local reference to the object.
Once that happens, a client can connect to the RMIregistry on that server and ask for an object by name. In return, the client will get a reference to the remote object. A client can also invoke the list() method on the RMIregistry, which will return an array containing the names of all of the registered objects. The registry isn't global, instead there is one per server. Clients need to connect to a particular server's registry, which can tell them only about the objects registered on the same server.
"Hello World!" RMI
Here's a really quick "Hello World" RMI. The client takes a String, representing a person's first name and sends it as a parameter to the (remote) instance of the Hello object. The Hello object then returns a string such as, "Hello World! Hello Greg". The client then prints this String.
I wrote Hello.java (the remote object) and HelloClient.java. These were compiled, as usual, using "javac". I then used the rmi compiler, "rmic" to create the stub class from the "Hello.class file": "rmic Hello".
For the curious, I used the DJ Java to decompile the resulting "Hello_Stub.class" file, producing "Hello_Stub.java".
If you want to test this, first compile everything and run "rmic" on the "Hello.class" file. Then, start the registry on the server:rmiregistry &
Then, you'll need to start the server. The simplest possible way is like this:But, if you do want the client to be able to automatically download the _Stub file, you'll need to tell the server where it lives, by starting the server as follows:java Hello &java -Djava.rmi.server.codebase=http://somemachine/somedirectory/ Hello &
In order for the client to have proper permission to access the network, you may need a security policy file, such as the one I used, "client.policy". If this is the case, start the client as follows:java -Djava.security.policy=client.policy HelloClient Greg
Otherwise, you can just start it the simple, intuitive way:java HelloClient Greg
In either case, we're directing java to instantiate the class "HelloClient" and to run main with one parameter, "Greg".
The sample files are below: