by Govind Seshadri revised/ re-edit Nov 6 / 2000 Peter Komisar
additional references: 'Design
Patterns' Gamma et al.
Good summaries on RMI are hard
to find. To this point we have in this course been using an
abridged version of the RMI
specification. Really, it was as easy to read as anything else.
The jGuru tutorial by Govind
Seshadri has changed that. This one is very good and brings
clarity to topic areas rarely
explained well. The tutorial is also up to date, touching on new
developments intoduced with
JDK 1.2 and 1.3. Seshardri brings to one spot a lot of useful
accessory RMI information. I
summarize much of it below. If you would like to read the whole
tutorial, get missing details
and view it's diagrams, go to
http://developer.java.sun.com/developer/onlineTraining/rmi/index.html
On the second pass I have tried
to simplify the edit further.
PK Nov 6 / 2000
The RMI architecture is based on the principle that the definition
and
implementation
of behavior
are separate concepts. In RMI code that defines a behavior and
code that
implements that behavior can remain separate and run on separate Java
Virtual Machines.
In RMI, a remote service definition is
coded into a java interface. The implementation
of this service is coded into classes
implementing the interface. RMI has one class
providing the behaviour running on the server and a second class, acting
as a proxy for that
remote service running on the client.
A client program makes method calls on the proxy object, RMI sends the
request to the
remote JVM, and forwards it to the implementation. Any return values
provided by the
implementation are sent back to the proxy and then to the client's
program.
RMI Architecture Layers
The RMI implementation is essentially built from three abstraction layers.
The first is the
Stub and Skeleton layer, which
lies just beneath the view of the developer. This layer
intercepts method calls made by the client to the interface reference
variable and redirects
these calls to a remote RMI service.
The next layer is the Remote Reference Layer.
This layer understands how to interpret
and manage references made from clients to the remote service objects.
In JDK 1.1, this
layer connects clients to remote service objects that are running and
exported on a server.
The connection is a one-to-one (unicast) link. In the Java 2 SDK, this
layer was enhanced
to support the activation of dormant remote service objects via Remote
Object Activation.
The transport layer is based on
TCP/IP connections between machines in a network. It
provides basic connectivity, as well as some firewall penetration strategies.
Using a layered architecture allows layers to be enhanced or replaced
without affecting the
rest of the system. For example, the transport layer could be replaced
by a UDP/IP layer
without affecting the upper layers.
Stub and Skeleton Layer
In the stub and skeleton layer, RMI uses the Proxy
design pattern as described in the book,
Design Patterns by Gamma, Helm, Johnson and Vlissides.In
the Proxy pattern, an object
in one context is represented by another (the
proxy) in a separate context. The proxy
knows how to forward method calls between the participating objects.
In RMI's use of the Proxy
pattern, the stub class plays the role of
the proxy, and the remote service implementation
class plays the role of the real subject. Also in RMI, a
helper class a skeleton is generated.
to use. The skeleton understands how to communicate with the stub across
the RMI link. The
skeleton carries on a conversation with the stub; it reads the parameters
for the method call
from the link, makes the call to the remote service implementation
object, accepts the return
value, and then writes the return value back to the stub.
//Proxy -provides a surrogate or placeholder for another object to control access to it -'Design Patterns'
In the Java 2 SDK implementation
of RMI, the new wire protocol has made skeleton classes obsolete.
RMI uses the reflection process
to make the connection to the remote service object. You only have to
worry about skeleton classes
and objects in JDK 1.1 and JDK 1.1-compatible system implementations.
Remote Reference Layer
The Remote Reference Layers defines and supports the invocation semantics
of the RMI
connection. This layer provides a RemoteRef
object that represents the link to the remote
service implementation object. The stub objects use the invoke(
) method in RemoteRef
to forward the method call. The RemoteRef object understands the invocation
semantics
for remote services.
The JDK 1.1 implementation of RMI provides only one way for clients
to connect to remote
service implementations: a unicast, point-to-point connection. Before
a client can use a remote
service, the remote service must be instantiated on the server and
exported to the RMI system.
(If it is the primary service, it must also be named and registered
in the RMI Registry).
The Java
2 SDK implementation of RMI adds a new way for client and server
to connect.
In this version, RMI supports activatable
remote objects. When a method call is made
to the proxy for an activatable object, RMI determines if the remote
service implementation
object is dormant. If it is dormant, RMI will
instantiate the object and restore its state
from a disk file. Once an activatable
object is in memory, it behaves just like JDK 1.1
remote service implementation objects.
Transport Layer
The Transport Layer makes the connection between JVMs. All connections
are stream-based
network connections that use TCP/IP. Even if two JVMs are running on
the same physical
computer, they connect through their host computer's TCP/IP network
protocol stack. (This
is why you must have an operational TCP/IP configuration on your system
even to run locally.)
In the current release of RMI, TCP/IP connections are used as the foundation
for all machine-
to-machine connections. On top of TCP/IP, RMI uses a wire level protocol
called Java
Remote Method Protocol (JRMP).
JRMP
is a proprietary, stream-based protocol that is
only partially specified and is now in two versions. The first version
was released with the JDK
1.1 version of RMI and required the use of Skeleton classes on the
server. The second version
released with Java 2 SDK, is performance optimized and does not require
skeleton classes.
Sun and IBM have jointly worked on the next version of RMI, called RMI-IIOP,
which will
be available with Java 2 SDK Version 1.3. The interesting thing about
RMI-IIOP is that instead
of using JRMP, it will use the Object Management Group (OMG) Internet
Inter-ORB Protocol,
IIOP, to communicate between clients and servers.
Naming Remote Objects
"How does a client find an RMI remote service? " Clients find remote
services by using a naming
or directory service. A naming or directory service is run on a well-known
host and port number.
(Well-known meaning everyone in an organization knowing what it is).
RMI can use many different
directory services, including the Java Naming
and Directory Interface (JNDI).
RMI itself
includes a simple service called the RMI Registry,
rmiregistry.
The RMI Registry runs on each
machine hosting remote service objects and accepts queries for services,
by default on port 1099.
On a host machine, a server program creates a remote service by first
creating a local object that
implements that service. Next, it exports that object to RMI. When
the object is exported, RMI
creates a listening service that waits for clients to connect and request
the service. After exporting,
the server registers the object in the RMI Registry under a public
name.
On the client side, the RMI Registry is accessed through the static
class Naming . It provides the
method lookup( ) that a client
uses to query a registry. The method lookup( ) accepts a URL that
specifies the server host name and the name of the desired service.
The method returns a remote
reference to the service object. The URL takes the form
rmi://<host_name> [:<name_service_port>] / <service_name>
// where the host_name is a name recognized on
the local area network (LAN) or a DNS name on the
// Internet. The name_service_port only needs to be
specified only if the naming service is running on a
// different port to the default 1099.
Using RMI
A working RMI system is composed of several parts.
You take the following steps to build an
RMI system:
1. Write and compile Java
code for interfaces
2. Write and compile
Java code for implementation classes
3. Generate Stub
and Skeleton class files from the implementation classes
4. Write Java code
for a remote service host program
5. Develop Java
code for RMI client program
6. Install and run
RMI system
1.Interfaces
The first step is to write and compile the Java code for the service
interface. The Calculator
interface defines all of the remote features offered by the service:
public interface
Calculator
extends java.rmi.Remote { public long add(long a, long b) throws java.rmi.RemoteException;
public long sub(long a, long b)
public long mul(long a, long b)
public long div(long a, long b)
|
Notice this interface extends Remote, and each method signature declares
that it may
throw a RemoteException object.
Copy this file to your directory and compile it with the Java compiler:
>javac Calculator.java
2.Implementation
Next, you write the implementation for the remote service. This is the
CalculatorImpl class:
public class CalculatorImpl
extends java.rmi.server.UnicastRemoteObject implements Calculator {
// Implementations must have an
public CalculatorImpl( )
public long add(long a, long b)
public long sub(long a, long b)
public long mul(long a, long b)
public long div(long a, long b)
|
Again, copy this code into your directory and compile it.
The implementation class uses UnicastRemoteObject to link into the RMI
system.
In the example the implementation class directly extends UnicastRemoteObject.
This
is not a requirement. A class that does not extend UnicastRemoteObject
may use it's
exportObject( ) method to be linked
into RMI.
When a class extends UnicastRemoteObject, it must provide a constructor
that declares
that it may throw a RemoteException object. When this constructor calls
super( ), it activates
code in UnicastRemoteObject that performs the RMI linking and remote
object initialization.
3.Stubs and Skeletons
You next use the RMI compiler, rmic, to generate the stub and skeleton
files. The compiler
runs on the remote service implementation class file.
>rmic CalculatorImpl
Try this in your directory. After you run rmic you should find the file
Calculator_Stub.class
and, if you are running the Java 2 SDK, Calculator_Skel.class.
Options for the JDK 1.1 version of the
RMI compiler, rmic:
Usage: rmic <options> <class names> where <options> includes:
-keep Do not delete intermediate generated source files
The Java 2 platform version of rmic add three new options:
-v1.1 Create stubs/skeletons for JDK 1.1 stub protocol version
|
4.Host Server
Remote RMI services must be hosted in a server process. The class CalculatorServer
is a very simple server that provides the bare essentials for hosting.
import java.rmi.Naming; public class CalculatorServer {
public CalculatorServer() {
public static void main(String args[]) {
|
5.The Client
import java.rmi.Naming; import java.rmi.RemoteException; import java.net.MalformedURLException; import java.rmi.NotBoundException; public class CalculatorClient {
public static void main(String[] args) {
|
6. Running the RMI System
You are now ready to run the system! You need to start three consoles,
one for the
server, one for the client, and one for the RMIRegistry. Start with
the Registry. You
must be in the directory that contains the classes you have written.
From there, enter
the following:
rmiregistry
If all goes well, the registry will start running and you can switch
to the next console.
In the second console start the server hosting the CalculatorService,
and enter the
following:
>java CalculatorServer
It will start, load the implementation into memory and wait for a client
connection.
In the last console, start the client program.
>java CalculatorClient
If all goes well you will see the output of four numbers 1,9,18, and 3.
That's it; you have created a working RMI system. Even though you ran
the three consoles
on the same computer, RMI uses your network stack and TCP/IP to communicate
between
the three separate JVMs. This is a full-fledged RMI system.
Parameters in RMI
You have seen that RMI supports method calls to remote objects. When
these calls involve
passing parameters or accepting a return value, how does RMI transfer
these between JVMs?
Does RMI support pass-by-value or pass-by-reference? The answer depends
on whether the
parameters are primitive data types, objects, or remote objects.
Parameters in a Single JVM
First, review how parameters are passed in a single JVM. The normal
semantics for Java
technology is pass-by-value. When a parameter is passed to a method,
the JVM makes a
copy of the value, places the copy on the stack*
and then executes the method. When the
code inside a method uses a parameter, it accesses its stack and uses
the copy of the
parameter. Values returned from methods are also copies.
// * in assembly the stack is
a memory area used to sequence instructions in a First-In-Last-Out
// (FILO) configuration
When a primitive data type (boolean, byte, short, int, long, char, float,
or double) is passed as
a parameter to a method, the mechanics of pass-by-value are straightforward.
The mechanics
of passing an object as a parameter are more complex. Recall that an
object resides in heap*
memory and is accessed through one or more reference variables. And,
while the following
code makes it look like an object is passed to the method println(
)
String s = "Test";
System.out.println(s);
in the mechanics it is the reference variable that is passed to the
method. In the example, a
copy of reference variable s is made (increasing the reference count
to the String object by
one) and is placed on the stack. Inside the method, code uses the copy
of the reference to
access the object.
// * from an assembly viewpoint the heap is all addressable memory left after system resource allocation
Now you will see how RMI passes parameters and return values between remote JVMs.
Primitive Parameters
When a primitive data type is passed as
a parameter to a remote method, the RMI
system passes it by value. RMI will make a
copy of a primitive data type and send
it to the remote method. If a method
returns a primitive data type, it is also returned to the
calling JVM by value. Values are passed between JVMs in a machine-independent
format,
allowing JVMs running on different platforms to communicate with each
other reliably.
Object Parameters
When an object is passed to a remote method,
the semantics change from the case of
the single JVM. RMI sends the object itself,
not its reference, between JVMs. It is the
object that is passed by value, not the reference to the object. Similarly,
when a remote
method returns an object, a copy of the whole object is returned to
the calling program.
( Unlike primitive data types,
sending an object to a remote JVM is a nontrivial task. A Java object can
be simple and self-contained,
or it could refer to other Java objects in complex graph-like* structure.
Because different JVMs do not
share heap memory, RMI must send the referenced object and all objects
it
references. (Passing large object
graphs can use a lot of CPU time and network bandwidth.)
// ed. ref.: graph -a generalization of a tree data model where a set of nodes connected by lines or arrows
RMI uses a technology called Object Serialization
to transform an object into a linear format
that can then be sent over the network wire. Object serialization essentially
flattens an object
and any objects it references. Serialized objects can be de-serialized
in the memory of the
remote JVM and made ready for use by a Java program.
Remote Object Parameters
RMI introduces a third type of parameter to consider:remote
objects. As you have
seen, a client program can obtain a reference to a remote object through
the RMI Registry
program. There is another way in which a client can obtain a remote
reference, it can be
returned to the client from a method call. In the following code, the
BankManager service
getAccount( ) method is used to obtain a remote reference to an Account
remote service.
BankManager bm; Account a; try { bm = (BankManager) Naming.lookup("rmi://BankServer/BankManagerService" ); a = bm.getAccount( "jGuru" ); // code that uses the account } catch (RemoteException re) { }
// In the implementation of getAccount(),
the method returns a (local)
public Account
|
When a method returns a local reference to
an exported remote object, RMI does
not return that object. Instead, it substitutes
another object (the remote proxy for that
service) in the return stream.
// Supplemental
material
RMI Client-side Callbacks
In many architectures, a server may need to make a remote call to a
client. Examples include
progress feedback, time tick notifications, warnings of problems, etc.
To accomplish this, a
client must also act as an RMI server. There is nothing really special
about this as RMI works
equally well between all computers. However, it may be impractical
for a client to extend
java.rmi.server.UnicastRemoteObject. In
these cases, a remote object may prepare itself
for remote use by calling the static method
UnicastRemoteObject.exportObject (<remote_object>)
Distributing RMI Classes
To run an RMI application, the supporting class files must be placed
in locations that can
be found by the server and the clients. For the server, the following
classes must be
available to its class loader:
For the client, the following classes must be available to its class
loader:
Once you know which files must be on the different nodes, it is
a simple task to make
sure they are available to each JVM's class loader.
Automatic Distribution of Classes
The RMI designers extended the concept of class loading to include the
loading of classes
from FTP servers and HTTP servers. This is a powerful extension as
it means that classes
can be deployed in one, or only a few places, and all nodes in a RMI
system will be able
to get the proper class files to operate.
RMI supports this remote class loading through the RMIClassLoader.
If a client or server
is running an RMI system and it sees that it must load a class from
a remote location, it calls
on the RMIClassLoader to do this work. The way RMI loads classes is
controlled by a
number of properties.These properties can be set when each JVM is run:
java [ -D<PropertyName>=<PropertyValue> ] + <ClassFile>
The property java.rmi.server.codebase
is used to specify a URL. This URL points to a
file:, ftp:,
or http: location that supplies classes
for objects that are sent from this JVM. If
a program running in a JVM sends an object to another JVM (as the return
value from a
method), that other JVM needs to load the class file for that object.
When
RMI sends the
object via serialization, RMI embeds the URL
specified by this parameter into the
stream, alongside the object.
Note: RMI does not send class
files along with the serialized objects. If the remote JVM needs
to load a class file for an
object, it looks for the embedded URL and contacts the server at that
location for the file.
When the property java.rmi.server.useCodebaseOnly
is
set to true, then the JVM will load
classes from either a location specified by the CLASSPATH environment
variable or the URL
specified in this property. By using different combinations of the
available system properties, a
number of different RMI system configurations can be created.
Closed. All classes used by clients
and the server must be located on the JVM and
referenced by the CLASSPATH environment variable. No dynamic class
loading is
supported.
Server based. A client applet is
loaded from the server's CODEBASE along with all
supporting classes. This is similar to the way applets are loaded from
the same HTTP
server that supports the applet's web page.
Client dynamic. The primary classes
are loaded by referencing the CLASSPATH
environment variable of the JVM for the client. Supporting classes
are loaded by the
java.rmi.server.RMIClassLoader from an
HTTP or FTP server on the network
at a location specified by the server.
Server-dynamic. The primary classes
are loaded by referencing the CLASSPATH
environment variable of the JVM for the server. Supporting classes
are loaded by the
java.rmi.server.RMIClassLoader from an
HTTP or FTP server on the network at a
location specified by the client.
Bootstrap client. In this configuration,
all of the client code is loaded from an HTTP or
FTP server across the network. The only code residing on the client
machine is a small
bootstrap loader.
Bootstrap server. In this configuration,
all of the server code is loaded from an HTTP or
FTP server located on the network. The only code residing on the server
machine is a
small bootstrap loader.
// the original tutorial here
has an excercise to create a bootstrap client configuration
Firewall Issues
Firewalls are inevitably encountered by any networked enterprise application
that has to
operate beyond the sheltering confines of an Intranet. Typically, firewalls
block all network
traffic, with the exception of those intended for certain "well-known"
ports. Since the RMI
transport layer opens dynamic socket connections between the client
and the server to
facilitate communication, the JRMP traffic is typically blocked by
most firewall implementations.
But luckily, the RMI designers had anticipated this problem, and a
solution is provided by the
RMI transport layer itself. To get across firewalls, RMI
makes use of HTTP tunneling
by encapsulating the RMI calls within an HTTP
POST request.
// Seshadri describes here a
couple different firewall configurations vis a vis client and server
It should be noted that notwithstanding the built-in mechanism for overcoming
firewalls,
RMI suffers a significant performance degradation
imposed by HTTP tunneling.
There are other disadvantages to using HTTP tunneling too. For instance,
your RMI
application will no longer be able to multiplex JRMP calls on a single
connection, since it
would now follow a discrete request/response protocol. Additionally,
using the java-rmi.
cgi script exposes a fairly large security
loophole on your server machine, as now,
the script can redirect any incoming request to any port, completely
bypassing your
firewalling mechanism.
Developers should also note that using HTTP
tunneling precludes RMI applications
from using callbacks, which in
itself could be a major design constraint. Consequently,
if a client detects a firewall, it can always disable the default HTTP
tunneling feature by
setting the property:
java.rmi.server.disableHttp=true
Distributed Garbage Collection
One of the joys of programming for the Java platform is not worrying
about memory allocation.
The JVM has an automatic garbage collector that will reclaim the memory
from any object
that has been discarded by the running program. One
of the design objectives for RMI
was seamless integration into the Java programming
language, which includes garbage
collection. Designing an efficient single-machine garbage collector
is hard; designing a
distributed garbage collector is very hard.
The RMI system provides
a reference counting distributed garbage collection algorithm
based on Modula-3's Network Objects.
This system works by having the server keep track
of which clients have requested access to remote objects running on
the server. When a reference
is made, the server marks the object as "dirty" and when a client drops
the reference, it is marked
as being "clean." // the server
tracks when references are requested and when dropped
The interface to the DGC (distributed
garbage collector) is hidden in the stubs and skeletons
layer. However, a remote object can implement the java.rmi.server.Unreferenced
interface
and get a notification via the unreferenced method
when there are no longer any clients holding
a live reference. In addition to the reference counting mechanism,
a live client reference has a
lease with a specified time. If a client does not refresh the connection
to the remote object before
the lease term expires, the reference is considered to be dead and
the remote object may be
garbage collected. The lease time is controlled by the system property
java.rmi.dgc.leaseValue.
The value is in milliseconds and defaults to 10 minutes. Because of
these garbage collection
semantics, a client must be prepared to deal with remote objects that
have "disappeared."
// Seshadri here has an excercise
to experiment with the DGC
Serializing Remote Objects
When designing a system using RMI, there are times when you would like
to have the flexibility
to control where a remote object runs. Today, when a remote object
is brought to life on a
particular JVM, it will remain on that JVM. You cannot "send" the remote
object to another
machine for execution at a new location. RMI makes it difficult to
have the option of running
a service locally or remotely.
The very reason RMI makes it easy to build some distributed application
can make it difficult
to move objects between JVMs. When you declare an object as implementing
the Remote
interface, RMI will prevent it from being
serialized and sent between JVMs as a parameter.
Instead of sending the implementation class for a java.rmi.Remote
interface, RMI substitutes
the stub class. Because this substitution occurs in the RMI internal
code, one cannot intercept
this operation.
There are two different ways to solve this problem. The first involves
manually serializing the
remote object and sending it to the other JVM.To do this, there are
two strategies. The first
strategy is to create an ObjectInputStream
and ObjectOutputStream connection between
the two JVMs. With this, you can explicitly write the remote object
to the stream. The second
way is to serialize the object into a byte array and send the byte
array as the return value to an
RMI method call. Both of these techniques require that you code at
a level below RMI and
this can lead to extra coding and maintenance complications.
In a second strategy, you can use a delegation pattern. In this pattern,
you place the core
functionality into a class that does not implement
java.rmi.Remote and does implement
java.io.Serializable. Then you
build a remote interface that declares remote access to the
functionality. When you create an implementation
of the remote interface, instead of
reimplementing the functionality, you allow
the remote implementation to defer, or
delegate, to an instance of the local version.
Now look at the building blocks of this pattern. Note that this is a
very simple example. A
real-world example would have a significant numberof local fields and
methods.
// Place functionality in a local object public class LocalModel implements java.io.Serializable{ public String getVersionNumber( ){ return "Version 1.0"; } } // Next, declare an java.rmi.Remote interface with the same functionality: interface RemoteModelRef extends
java.rmi.Remote{
public class RemoteModelImpl
extends
public String getVersionNumber( )
interface RemoteModelMgr extends java.rmi.Remote {
public class RemoteModelMgrImpl
|