Remote Method Invocation
1. Basic concepts
1.1 Purpose
With sockets, the application code determines the encoding and
interpretation of the bytes transmitted. The bytes sent by the
initiator of the exchange include an indication of the operation to be
performed, as well as an encoding of any arguments that are passed, and
are encoded by the sender and parsed by the recipient. The bytes
sent by the recipient encode a return value or an error message, which
must be parsed by the initiator. With this approach, the
application is responsible for defining a protocol that encodes all
interactions between the client and server. Modifying or
extending the message protocol involves rewriting the encoding and
parsing methods. Furthermore, adding new clients for a given
server or adding access to new servers in a client requires
documentation of the protocol (which, in too many cases, is the source
code) and duplicating the encoding and parsing methods. Such a
scheme is not easily extensible or reusable,
and there is no object-oriented decomposition, encapsulation,
information hiding, or inheritance.
Remote method invocation (RMI) supports sending messages and
arguments to an object on another virtual machine. That is,
instead of packaging the receiver, operation name, and arguments in a
message, the client simply makes a method call. The syntax for a
remote invocation is the same as that for any method call, except that
all remote invocations can throw remote exceptions:
try {
result = remoteObject.method(...args...);
}
catch (RemoteException ex) {
// handle error
}
Any other clients that need the same service use the same call,
which is documented in the remote interface of the server object.
Note also that the client need not know the port or even the host
on which the server object resides. (We will see how clients
obtain references to remote objects below.) Like any method call,
the remote
method can either return a value or signal an exception. Remote
method invocation is the object-oriented version of remote procedure
calls (RPC). Java RMI supports Java-to-Java distributed objects
on any platform for which a Virtual Machine exists,
in contrast to CORBA, which supports any language or system, and DCOM
which works only on MS-Windows. RMI is simpler to use than CORBA
or DCOM.
The caller of the remote method is sometimes termed the "client" and
the receiver the "server". Since an object can be the client for
one remote invocation and the server for another, we will use the term
remote object for an object whose methods can be invoked from a
different virtual machine. Any object can invoke a remote method,
but a remote object must be an instance
of a descendant of RemoteServer
.
1.2 Remote references and stubs
Clearly, an object reference cannot refer to an object on another
virtual machine so the invoker of a remote method does not have an
actual reference to the remote object. When we
define a remote server in Java, the definition must be written as a
remote interface and a separate implementation class. A stub
object on the client’s machine implements the remote interface and
acts as a proxy for the remote object (the term “stub” comes from
CORBA). Clients use the remote interface (not the implementation
class) as the type of remote object references, and the client’s remote
interface reference refers to the stub. When the client sends a
message in the remote interface, the receiver is actually the stub,
which communicates with the remote object via TCP/IP.
When a remote invocation is made, the stub "marshalls"
the parameters (see the next section), then sends a message consisting
of the remote object ID, an operation number that identifies the
method, and the marshalled parameters to the remote object. The
remote object unmarshalls the parameters, invokes the desired method,
marshalls the return value or exception, and sends it to the
stub. (In fact, the remote object delegates marshalling and
unmarshalling to the "dispatcher" described below.) The stub
unmarshalls the return value or exception and
passes it to the caller. The stub class is generated from the
remote interface by a special compiler called rmic
,
which is included in the JDK.
A client can obtain a remote reference as the return value of a remote
method call or by accessing a remote object through a naming service.
When this occurs the stub class is dynamically loaded and linked
if necessary, and an instance is created. The class file for the
remote interface must be available on both the server and client, but
the stub class file is only needed by clients and the class file for
the server implementation class need only be available on the server.
If an applet makes remote invocations, the remote interface and
the stub class must be available via the codebase.
The initial version of RMI in JDK 1.1 used a "skeleton" class which
behaved as the remote object's proxy for the caller (like the
CORBA architecture). An instance performed the marshalling and
method dispatching for the remote object, as the stub does for the
client, and was also generated by rmic
. Beginning
in JDK 1.2, server implementation classes delegate these tasks to a
"dispatcher" implemented using reflection, rather than using a skeleton
class.
The stub and dispatcher (or skeleton in Java 1.1) communicate via the
Java RemoteMethod Protocol (JRMP) embedded within HTTP, which is
built on top of TCP/IP. Sun and IBM have designed an invocation
protocol called RMI-IIOP (IIOP is the Internet Inter-ORB Protocol used
by CORBA), which allows RMI to interoperate with CORBA distributed
systems, possibly written in other langauges. Using RMI-IIOP
requires some changes
to the details of how remote objects and clients are coded (see
RMI-IIOP Programmers Guide). We will not discuss RMI-IIOP.
1.3 Argument passing
On a local virtual machine, argument passing is simple: primitive types
and object references are copied to the method's
stack frame. As we stated above, an object reference cannot
refer to an object on another virtual machine. With RMI,
arguments and return values of remote
methods whose types are remote objects are translated to references to
the corresponding stub objects. That is, all such
arguments refer to the same remote object (directly on its own machine,
or via its stub on other machines). Therefore, the stub class
must
be available in any virtual machine that uses that remote object as the
receiver, parameter, or return vlaue of a remote invocation.
Non-remote arguments and return values of remote method invocations are
serialized and passed by value, whether they are primitive
or class type. (We will see that this differs from CORBA paramter
passing.) That is, the remote object receives copies of the
argument objects, including those that are class instances.
Collection
elements are also copied by the serialization process, i.e. the process
performs a "deep copy". The arguments are serialized in the same
stream, so if two argument objects both have references to a single
object,
there will be one copy of that object accessed via the method's
parameters
within the remote method. However, the arguments for each remote
invocation are serialized separately. Since the remote method
receives
copies, if it invokes methods on its argument objects that change their
state, the original argument objects in the client are
unaffected. That is, a remote method can only communicate with
the caller through its return value (or exceptions), and cannot do so
by modifying arguments. Like any
local variable, the server copies
of the argument objects are garbage collected after the method returns.
Similarly, if the return type of a remote invocation is not a
remote class, the method's return value is serialized and the caller
receives a copy of the object in the remote object. This copying
also means that the RMI implementation does not need to keep the
connection active while the remote method is executing. If a
parameter contains an object
that is not serializable (e.g., as a collection element), an exception
is signaled.
The term marshalling refers to the process of translating
arguments and return values into a form that can be transmitted over a
network. For RMI, this means serializing non-remote objects, or
determining
a remote object identifier from the object or its stub for remote
objects. For RMI, unmarshalling refers to deserializing
non-remote objects and for a remote object identifier, obtaining a
reference to the object
on its machine or to a stub on another machine and loading and
instantiating the stub class if
necessary.
The class MarshalledObject
can be used as
the parameter type of a remote method to allow the method to delay
or avoid unmarshalling that argument. This is useful when a
complex object is passed through a chain of remote invocations, but
is not needed in intermediate steps. The MarshalledObject
constructor takes an object, which it serializes to a byte stream.
The object is then accessed when necessary via get
.
1.4 "At most once" semantics
RMI implements "at most once" semantics (some other distributed systems
provide "at
least once" semantics). If a method returns normally, it is
guaranteed to have executed once. Hoever, if an exception such as
a MarshallException
occurs, the caller doesn't know
whether the exception occurred when transmitting the call or
returning the return value, i.e. the caller cannot determine whether
the remote method executed. In this case, the client may wish to
attempt the invocation again. To facilitate this, the
implementation
of a remote method should be idempotent where possible, which
means that the operation can be executed multiple times with the same
effect as executing once. Even a method such as an account
withdrawal
can be made idempotent by passing an operation ID with the invocation:
the remote object ignores invocations for operation IDs that have
already
been performed.
1.5 rmi
packages
The package java.rmi
defines the marker
interface Remote
, the classes Naming
, RMISecurityManager
, MarshalledObject
, and RemoteException
and several more specific
subclasses of RemoteException
. Remote
is a marker interface that identifies remote interfaces. It
is often necessary to download class files when using RMI, and the
RMI class loader will not load class files from remote sites if no
security manager has been set. RMISecurityManager
provides a basic security manager for this purpose.
MarshalledObject
was discussed previously. Clients only
need to import the package java.rmi
, to access
Remote
, RemoteException
and Naming
.
The package java.rmi.server
defines the classes
RemoteObject
, RemoteStub
, RemoteServer
, UnicastRemoteObject
, and several
interfaces and classes used by server classes and the RMI
infrastructure (e.g., a server class loader, remote and server
references, and a socket factory). RemoteObject
extends Object
and implements Remote
.
RemoteServer
is a subclass of RemoteObject
which is the ancestor of all remote classes, and has two subclasses,
UnicastRemoteObject
and Activatable
(discussed in
the next paragraph). UnicastRemoteObject
is the
ancestor for classes that define a singleton remote object and provides
support for remote invocation and parameter passing using TCP
streams. RemoteStub
is a subclass of
RemoteObject
which is the ancestor of all stub classes,
and provides the framework for invoking remote methods and parameter
marshalling. Server implementation classes that are not activable
extend UnicastRemoteObject
and import this package.
The package java.rmi.activation
defines the class
Activatable
and several classes and exceptions used in RMI
Object Activation. This mechanism supports defining a remote
object that can be activated by the system if it is not
running when a remote client invokes one of its methods. The
package
java.rmi.registry
defines interfaces and classes
for the RMI registry service, which keeps a database of remote objects
so that objects on other hosts can access them by name.
java.rmi.dgc
defines interfaces and classes for distributed
garbage collection.
2. Writing distributed application
2.1 Programming and deployment
As always, the programmers must write code, build the application, and
deploy it. Each of these steps has additional substeps
in the case of a distributed application. The code consists
of one or more remote classes and client classes that access them.
(The separation of application logic into server and client
is usually straightforward, but reorganizing which objects and
operations
are on a server and which are on a client, e.g. to improve performance,
can be tedious.) Each remote object is defined by both a remote
interface and a separate server implementation class.
In addition to compiling the server remote interfaces and
implementation classes and the client classes, you must
generate the stub for each remote interface with the JDK program
rmic
(the RMI stub compiler). The command line argument is
the qualified server implementation class file name, i.e. you must
compile the server class before using rmic
. For a
server implementation class ServerImpl
, rmic
generates a class file called ServerImpl_Stub.class
,
which is in the same package as ServerImpl
.
The stub class implements only
the remote interfaces of the corresponding server class. If you
forget to run rmic
or the necessary classes are not in
the class path, a remote invocation generates
StubNotFoundException
.
To deploy the application, the class files for the remote interfaces
and the stub classes to clients must be distributed to clients. (We
will discuss using the "codebase" facility to avoid manually
distributing
classes below.) To execute the application, you must run three
programs:
- the RMI registry program from the JDK, usually on the server host
- the server class(es) on the server host(s)
- the client class on the client host
As we will discuss below, the RMI registry is a look-up service that
registers remote objects and allows clients to locate them by
name. To run the RMI registry,
execute rmiregistry &
on Unix or start
rmiregistry
on MS-Windows. rmiregistry
uses
port 1099 by default, but you can assign another port on the command
line.
2.2 The interface Remote
The marker interface Remote
identifies an interface
implemented by a remote class and a stub proxy class, but does not
define any methods. A remote interface extends the interface
Remote
, and each method must list RemoteException
in its exception specification (this requirement is not enforced by the
Java compiler, but by rmic
). All parameter and
return types for the methods must be serializable or remote.
Remote parameters must use the remote interface type, not the
server implementation class type (because that remote parameter refers
to a stub, not to an instance of the server implementation class), and
this is true of any remote reference within the object graphs being
transmitted. Remote interfaces must have public, not
package-level access. Clients use the interface as the type of
remote object references (not the class that implements it). The
following is an example remote interface:
package edu.uic.cs.cs441;
import java.rmi.*;
public interface CoolService extends Remote {
boolean isCool(URL) throws RemoteException;
URL[] getCoolSites(String[] keywords) throws RemoteException;
}
2.3 Naming services
A client must obtain its initial remote reference from a naming
service , which provides access to remote objects by name.
Server programs register remote objects and their names with the
naming service and clients access them there by their names. The
class Naming
represents the RMI registry, which provides
a "flat", non-hierarchical name structure. (Naming services that
implement the JNDI interface provide a heirarchical name structure, and
can also be used with RMI.) With the class Naming
,
remote objects are identified by a URL of the form rmi://host:port/objectName
, where host
and port
are the host and
port on which the desired RMI registry is running. The
rmi:
, //host
, and :port/
are
optional: the host defaults to the local host and the default port
number is 1099. To avoid name collisions, it is recommended that
the objectName
for a remote object be the qualified name
(i.e., including the package) of the remote interface, since that name
is certainly know to client code.
All the methods in Naming
are class methods:
public static void bind(String name, Remote object)
throws AlreadyBoundException, MalformedURLException, AccessException, RemoteException
public static void rebind(String name, Remote object)
throws MalformedURLException, AccessException, RemoteException
public static void unbind(String name, Remote object)
throws NotBoundException, MalformedURLException, AccessException, RemoteException
public static Remote lookup(String name)
throws NotBoundException, MalformedURLException, AccessException, RemoteException
public static String[] list(String name)
throws MalformedURLException, AccessException, RemoteException
Server classes use the methods bind
and rebind
(which replaces any existing binding for the given name) to register
remote objects (typically, in the main
method), and use
unbind
to remove a binding from the registry. Since the
default values for the host and port are correct, it
is better practice to pass the name alone (i.e., the qualified remote
interface name) to (re)bind
. The binding methods
can only be called on the same host given in the URL (if one is given),
and throw AccessException
if this is violated (which
is another reason not to give a host in the name URL). However,
this does not mean that a remote object can only be listed with an RMI
registry on the host on which it is executing: a remote reference to
the
object can be passed to the host on which the RMI registry is running
and then bound. It is possible to register the same remote object
with more than one name, or to register the same object with RMI
registries
on multiple hosts, if desired. Clients use the method
lookup
to access remote objects by name, and must specify the
host in the name URL (and the port if it is not the default). The
method list
returns an array of strings giving the URLs
of all the registered
objects.
An alternative to using the class Naming
is to
use the remote interface Registry
. This interface
defines the same methods as Naming
. However because
the host and port are given when accessing the registry initially,
the remote object name only is used with bind
and lookup
, rather than a complete URL. Similarly, Registry.list
returns the remote object names, while Naming.list
returns full URLs. For this reason, the Registry
methods do not throw MalformedURLException
. You
obtain a reference to an implementor of Registry
(i.e., a
stub) with the class method LocateRegistry.getRegistry
,
which takes an optional host name and port (again, the host defaults to
the local host and the default port number is 1099). In fact, the
implementations of the Naming
methods use the host and
port from the argument to call LocateRegistry.getRegistry
to obtain the specified registry, and then call the corresponding
Registry
methods.
An alternative to running the RMI registry program from the
JDK is to use the class method LocateRegistry.createRegistry
, which creates and exports a registery remote object that will exit
when the program terminates. This method allows specifying a
different port number and socket factory for the registry.
Naming services that support the Java Naming and Directory Interface
(JNDI) or the Jini Discovery and Lookup service can also be used for
RMI naming services. These have the advantages over the RMI
registry that they provide a hierarchical name space for locating
remote objects and implement persistent access to services. (When
a RMI registry exits and is restarted, it no longer has references to
the remote objects that had been registered.) Examples of
services that support JNDI include the RMI registry, the CORBA COS
Naming service, the Lightweight Directory Access Protocol (LDAP), and
Novell's Network Directory Service (NDS). We will discuss the
CORBA COS
Naming service
later in the course.
2.4 Defining a server class
The simplest type of remote object is a transient, non-replicated,
"unicast" (point-to-point, as opposed to broadcast or multicast)
server. Such a server class implements one or more remote
interfaces and extends UnicastRemoteObject
. For
example, a server class could implement a remote user interface and a
remote administration interface. The server class can implement
non-remote interfaces and define methods that are not in its remote
interfaces, but they are only available on its virtual machine.
If methods invoked by clients can change the state of the server
object, those methods should be declared as synchronized because
several clients can use the service simultaneously.
UnicastRemoteObject
supports creating a single instance
and registering it with the remote naming service. Stubs that
refer to that object are only valid for the life of the process that
creates the object. The UnicastRemoteObject
constructor
(which is invoked by the super
call in the server
constructor)
"exports" the object, i.e. registers it with the RMI system. This
process creates a ServerSocket
bound to an anonymous
port known to the registry which is used to communicate with client
stubs, and invokes accept
to start a thread that listens for connections. (In fact, the RMI
specification states that a new listening port is created only when
necessary and ports can be shared among remote objects.) The
UnicastRemoteObject
constructor can signal RemoteException
, so the remote server’s constructor must also include that exception
in its exception specification. That is, even if the only
constructor for the server implementation class is a no-argument
constructor that
simply calls super
, it must be explicitly coded so that
the exception specification is given. There are also UnicastRemoteObject
constructors that support specifying a port and server and client
socket factories. A server class can also be defined as a
subclass of RemoteServer
or an implementor of Remote
,
in which case it uses the class method UnicastRemoteObject.exportObject
to export itself. UnicastRemoteObject
’s superclass RemoteServer
defines the methods getLog
and setLog
,
which access a stream for logging RMI calls.
A remote object is automatically "unexported", i.e. de-registered
from the RMI system, when it is garbage collected locally, which can
only occur when all its clients' stubs are garbage collected.
This notification is handled by the RMI Distributed Garbage
Collection subsystem. If the desginer of a remote object wishes
to perform some special processing when the object has no clients, the
server class can implement the interface Unreferenced
,
which defines a single method unreferenced
, a callback
which is invoked by the DCG when the lists of clients is empty.
However, there is no direct callback when a client is added.
The server class main
method installs a security manager,
creates the single instance, and registers that object with the RMI
registry. Any program that accesses class files via an
RMI codebase must set a security manager to check the classes that will
be loaded. The following example illustrates a skeletal server
class:
package edu.uic.cs.cs441;
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.io.IOException;
public class CoolImpl extends UnicastRemoteObject implements CoolService {
private static CoolImpl coolObj;
public static void main(String[] args) {
System.setSecurityManager(new RMISecurityManager());
try {
coolObj = new CoolImpl(...);
Naming.rebind(“edu.uic.cs.cs441.CoolService”, coolObj);
}
catch (IOException ex) {
System.out.println(“Could not register coolService”);
System.exit(1);
}
}
private CoolImpl() throws RemoteException {
// exports the object
super();
// ... build the table of cool sites ...
}
public boolean isCool(URL url) throws RemoteException {
// ... lookup url in the table ...
}
public URL[] getCoolSites(String[] keywords) throws RemoteException {
// ... get the cool sites for the given keywords ...
}
}
The class CoolImpl
may define additional methods that are
not in the CoolService
interface (e.g., for adding and
removing URLs to its list of cool sites), but these are not available
to clients. Even though main
returns after
registering the remote object, the thread that accepts connections
which is started by the super
call in the CoolImpl
constructor continues
execution. Like a server socket, we must use the operating
system’s “kill process” command to stop the server.
2.5 Accessing remote objects in clients
Clients find the remote object from the Java RMI registry via
Naming.lookup
, or can obtain a remote reference as the return
value of a remote invocation. For example:
package edu.uic.cs.cs441;
import java.rmi.Naming;
public class Client {
public static void main(String[] args) {
System.setSecurityManager(new RMISecurityManager());
try {
CoolService cool = (CoolService) Naming.lookup(“rmi://cool.org/edu.uic.cs.cs441.coolService”);
boolean test = cool.isCool(new URL(“www.hotwired.com”));
}
catch(IOException ex) {
System.out.println(“Not so cool!\n” + ex.getMessage());
}
}
}
The method Naming.lookup
returns the type Remote
,
so we must cast the result. The type of the variable cool
must
be the remote interface CoolService
, not the server class
that implements it, because it refers to an instance of the stub class
that acts as a proxy for the remote CoolImpl
object.
Note that the syntax for the remote call is the same as for any
message. Naming.lookup
can
signal both RemoteException
and
MalformedURLException
, which
are subclasses of IOException
.
For applet clients, the security manager only lets an applet
communicate with and load classes from the host directory that
delivered the page, so the remote interface and stub class files must
be there, and the remote object and the RMI registry must be running
there. Note that
Internet Explorer does not (and, apparently, will not) support RMI.
In a sense, remote method invocation is deceptively simple for clients.
In practice, it is a major design decision as to what action the
client should take when a remote invocation fails: Should the operation
be retried? If so, how many times? Should the
client make an attempt to determine whether the method actually
executed? A robust application must handle these issues.
2.6 Code mobility
Having the necessary class files on each machine in an RMI system can
be complex, particularly if clients are widely distributed or are not
known in advance. In addition to distributing stub and parameter
classes to clients, other class loading issues can occur. For
example,
the actual type of an argument may be a subclass of the parameter type,
and that class might not be loaded on the virtual machine to which the
object is being passed. (Recall that serializing an object does
not
include
the object's class.) Similarly, a collection passed as an
argument
might contain objects whose classes are not loaded.
To simplify
these issues, RMI supports loading class files from FTP or HTTP servers
via a codebase. The value of the system property java.rmi.server.codebase
specifies a list of file:
, ftp:
, or http:
URLs that supply classes for invocation of
remote objects on
this virtual machine. Via a codebase, an application can set up a
location or a set of locations from which clients can load the classes
necessary to perform remote method invocations. System properties
are typically set with
the
-D
command line option. Here are two examples for an
HTTP server called codeServer.org
running on the default
HTTP port, 80, the first for a directory of class files, the second for
a jar file:
-Djava.rmi.server.codebase=http://codeServer.org/edu/
-Djava.rmi.server.codebase=http://codeServer.org/coolServices.jar
These properties would typically be set on the command line that starts
the server program. When a client first accesses a remote object,
a remote object
identifier is returned, which includes the server codebase location.
If the
client virtual machine can find the stub class in its classpath, it
loads
the class locally. If not, it uses the RMIClassLoader
to access the class from the HTTP server given by the codebase
property.
Similary, when RMI serializes an object, it annotates the
serialized
object with the codebase URL, which is used during deserialization if
the
object's class is not on the classpath. This process is similar
to
the virtual machine within a browser loading an applet class and the
classes
it requires from a codebase. As stated above, a security manager
must be running to enable downloading code from a codebase. If
the
codebase is not set or set incorrectly, a ClassNotFoundException
wrapped in an UnmarshalException
is signaled when the
virtual
machine attempts to load the class.
To use this facility to distribute classes from the server, you must
be careful when running rmiregistry
. If the RMI
registry program can find your the server classes in its classpath, it
will load them from the classpath when it needs them and ignore its java.rmi.server.codebase
property. This will result in clients not being able to download
those classes because they will not be identified as having come from
that codebase in the stream sent to the client. Therefore, before
running rmiregistry
, set the java.rmi.server.codebase
property to the URL of a FTP or HTTP server that provides the class
files,
and ensure that the application's classes are not on the classpath.
It is also possible that a remote object may need to load classes from
a client, e.g. if the client passes an argument that is a subclass of a
method parameter type. In this case, the client must run a FTP or
HTTP server and specify the URL in its java.rmi.server.codebase
property.