Java 1.1 Unleashed
- 17 -
The RMI Package
by Eddie Burris
IN THIS CHAPTER
- What Is RMI?
- Where Does RMI Fit?
- A Simple Example
- Remote Objects and the Remote Interface
- The Remote Object Registry
- Dynamic Class Loading
- RMI to and from Applets
- Distributed Object-Oriented Programming
- Security
- RMI Tools
The Java programming language is designed to be a programming language for the
Internet. Any language with such aspirations must include native support for communication
between programs on different machines. To be more precise, the language must include
native support for communication between process address spaces or, when both programs
are written in Java, between virtual machines.
The first version of the Java programming language provided support for interprocess
communication in the java.net package. The main feature of this package
is support for interprocess communication through sockets. The implementation of
sockets in java.net was a welcome relief to many programmers struggling
with add-on network APIs (Application Programming Interfaces). Although using the
Java implementation of sockets is easier than using other implementations, programmers
are still required to do a fair amount of work at the application level just to send
simple data types between applications.
This chapter discusses RMI (Remote Method Invocation), a new alternative for interprocess
communication between Java virtual machines. The chapter is organized around the
capabilities RMI provides. Each new concept and capability is explained and examples
are given to demonstrate exactly how the capabilities are used in practice.
What Is RMI?
RMI is a term used to describe both the act of invoking a method defined in a
different address space and the changes made to the Java language to support remote
method calls. The changes made to the Java language to support RMI include new tools,
APIs, and runtime support. RMI is part of the core Java programming language that
all licensees are required to support.
As its name implies, RMI enables remote method invocations. RMI enables the method
of an object in one virtual machine to call the method of an object in another virtual
machine with the same syntax and ease as a local method invocation. RMI supports
not only the transfer of control between virtual machines but also the passing of
objects either by reference or by copy.
RMI supports interprocess communication at a higher level of abstraction. This
capability alone makes RMI an attractive alternative to sockets for interprocess
communication. However, RMI provides much more than support for passing data types
between processes. RMI also provides support for new capabilities such as these:
- Dynamic class loading
- Callbacks to applets
- Distributed object model
Don't worry if you don't completely understand the significance of these capabilities.
They are explained in detail throughout this chapter.
Where Does RMI Fit?
We already mentioned one alternative for interprocess communication--sockets.
If you are writing a distributed application that requires some form of interprocess
communication, there are at least two other options you may want to consider: RPC
(Remote Procedure Call) and CORBA (Common Object Request Broker Architecture). The
following sections compare RMI to these alternatives to help you decide which is
best for your specific situation.
Sockets
Sockets provide a low-level, general-purpose solution to interprocess communication.
Because sockets have near planet-wide support, you should have no trouble using sockets
for communication in a heterogeneous environment. Sockets support both TCP and UDP
transport protocols and are the most efficient method for moving data between process
address spaces. However, sockets are not the most efficient solution in terms of
programmer time. Sockets require the programmer to invent application-level protocols.
For example, to send a sound file, you must decide on a protocol for transferring
the binary data. One such method may be to first send the type of file followed by
the number of bytes in the file; the program on the receiving end can read just that
number of bytes and interpret them as the type of file that was sent.
With RMI, sending a complex data type such as a sound file to a remote process
is as easy as passing the data type to an internal method. Because there is some
overhead for each RMI method call, sending data using RMI is not as fast as sending
the data through a socket. The upside of using RMI is that RMI programs are much
easier to write and maintain without the extra code to support an application-level
protocol.
Remote Procedure Call
The abstraction provided by RPC (Remote Procedure Call) is similar to the abstraction
provided by RMI. RPC makes calling an external procedure that resides in a different
address space as simple as calling a local procedure. Arguments and return values
are packaged and sent between the local and external procedures to keep the semantics
of a remote procedure call the same as those of a local call.
RPC was designed for a heterogeneous environment. In a heterogeneous environment,
fewer assumptions can be made about the machine at the other end of the network.
For example, because integer byte ordering can be different between machines (big-endian
versus little-endian), parameters in RPC must be packaged in an architecture-neutral
format before they can be passed to a remote procedure.
Because RMI supports calls only between Java virtual machines, certain assumptions
can be made about the communicating processes. These assumptions reduce the overhead
associated with a method invocation and make RMI a more efficient method than RPC
for external calls.
With RPC, the programmer is limited to passing parameters between external routines.
RMI supports passing objects. Remote objects passed between virtual machines support
polymorphism and casting between the implementation types supported by the remote
object.
Dynamic class loading is another, more powerful capability provided by RMI that
is not provided by RPC. With RPC, you are limited to passing only the argument types
expected by the destination process. More specifically, you can pass only the data
types known to or compiled into the client. When you invoke a remote method with
RMI, class definitions can be downloaded from the server at run time. This capability
enables the client to execute code and make calls to methods created long after the
client has been compiled. (The mechanism used is the same mechanism that supports
the downloading of applets to Web browsers.)
CORBA
CORBA (Common Object Request Broker Architecture) defines a specification for
a standard heterogeneous object-oriented distributed computing environment. Part
of the specification describes an industry standard IDL (Interface Definition Language)
that all CORBA implementations must support. A CORBA implementation maps the capabilities
defined by the IDL to the capabilities of the specific language environment. The
object model of CORBA doesn't provide all the services of any one native language;
instead, it defines a subset of services that is reasonable for all languages to
implement.
RMI doesn't have an IDL or separate object model for distributed computing. The
object model of RMI is the object model of Java. Consequently, RMI supports a feature-rich
distributed computing object model. One such feature supported by RMI not supported
by CORBA is garbage collection. Programmers writing to the CORBA specification must
keep track of objects that are created so that they can be specifically removed when
the objects are no longer needed. RMI objects are automatically garbage collected
when there are no more references to them.
Another capability exposed by the Java object model but not available in CORBA
is the ability to pass an object as its actual type rather than as its declared type.
With CORBA, when an object reference is passed from a server to a client, the object
must be of a type the client recognizes. If the actual object is a more derived type,
the object is converted to the declared type before being sent to the client. With
the RMI object model, an object can be passed around and referenced as any of the
types implemented by the remote object. The instanceof operator can be used
to determine the actual type of the object. If the object is actually of a more derived
type, it can be cast back to the more derived type. Any class definitions required
for the cast are downloaded automatically by the runtime system. This capability
allows you to manipulate objects at a more abstract level.
One capability in the CORBA specification not supported by RMI is automatic object
activation. Before you can invoke a method on a remote object with RMI, the object
must be active (that is, instantiated) and exported.
Communication Comparison Wrap-Up
When you compare RMI, sockets, RPC, and CORBA, the one you decide is "better"
depends on your specific requirements. If you are programming a very performance-sensitive
application, sockets may be your only alternative. If you are working in a heterogeneous
environment and have to communicate between applications written in different object-oriented
programming languages, CORBA may be the best solution.
RMI is the clear winner for Java-to-Java interprocess communication. If you like
the features of RMI but have to communicate with nonJava programs, you may want to
consider adding a Java front-end to the nonJava destinations. With this Java front-end,
or wrapper, in place, you can communicate across the network with RMI and with your
nonJava applications by using native function calls.
A Simple Example
One of the best ways to learn about a new programming feature is to see an example.
Listings 17.1 through 17.3 show all the code necessary to create a simple but complete
client/server application that uses RMI to communicate. The client retrieves a Date
object created on the server. The client makes two separate calls to the server to
retrieve two Date objects. The difference between the two Date
objects is computed to estimate the amount of time a remote method invocation takes.
The source code for all the examples used in this chapter is available on the
companion CD-ROM. You can find the source code for this simple example in the calendar
directory.
NOTE: With some distributed applications,
it is not always clear which is the client and which is the server. This is especially
true with RMI systems because RMI supports a peer-to-peer communication model. For
the rest of this chapter, the term client is used to refer to the process invoking
a method on a remote object. The term server is used to refer to the process that
created the remote object.
Listing 17.1. iCalendar.java: The interface declaration
for a remote object.
import java.rmi.*;
public interface iCalendar extends Remote {
java.util.Date getDate () throws RemoteException;
}
Listing 17.2. CalendarImpl.java: The remote object and
server definition.
import java.util.Date;
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
public class CalendarImpl
extends UnicastRemoteObject
implements iCalendar {
public CalendarImpl() throws RemoteException {}
public Date getDate () throws RemoteException {
return new Date();
}
public static void main(String args[]) {
CalendarImpl cal;
try {
LocateRegistry.createRegistry(1099);
cal = new CalendarImpl();
Naming.bind("rmi:///CalendarImpl", cal);
System.out.println("Ready for RMI's");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Listing 17.3. CalendarUser.java: The client definition.
import java.util.Date;
import java.rmi.*;
public class CalendarUser {
public CalendarUser() {}
public static void main(String args[]) {
long t1=0,t2=0;
Date date;
iCalendar remoteCal;
try {
remoteCal = (iCalendar)
Naming.lookup("rmi://ctr.cstp.umkc.edu/CalendarImpl");
t1 = remoteCal.getDate().getTime();
t2 = remoteCal.getDate().getTime();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("This RMI call took " + (t2-t1) +
" milliseconds");
}
}
Figure 17.1 shows the order in which the components should be created and on which
machine they should be placed. As with most client/server applications, both the
client and server portions can run on the same machine. This example shows the client
and server portions running on different machines to make clear on which machine
each component belongs and on which machine each activity takes place. For example,
notice that the rmic command is used on the server to create the stub file
CalendarImpl_Stub.class and the skeleton file CalendarImpl_Skel.class.
Also notice that the stub file is copied to the client before run time. The rmic
command and the purpose of the stub and skeleton files are explained in a moment.
Figure 17.1.
Compilation steps for the Calendar example.
Before looking at the details of this example, here is a high-level description
of the application: the CalendarImpl class creates and exports a remote
object. The CalendarUser class looks up the remote object in a registry
and calls the getDate() method defined for the remote object. CalendarImpl
defines the implementation for the remote object. A Date object is created
on the server and a copy is sent back to the client. The getDate() method
is called twice in succession; the values of the calls are subtracted (with the difference
being the amount of time between the calls) to estimate the amount of time a remote
method invocation takes.
Listing 17.1 shows the source code for iCalendar.java, the remote interface
declaration for the remote object. A remote interface declares the remote methods
for a remote object. A remote object is an object with remote methods that can be
called from another Java virtual machine.
Listing 17.2 shows the source code for CalendarImpl.java, the server
portion of the system. The first thing to notice about this code is the new import
libraries. The API support for RMI is contained in the four packages java.rmi,
java.rmi.server, java.rmi.registry, and java.rmi.dgc.
CalendarImpl.java is defined as extending UnicastRemoteObject
and implementing the remote interface iCalendar. UnicastRemoteObject
is covered more thoroughly later in this chapter. For now, it's enough to know that
by extending UnicastRemoteObject, ClientImpl becomes "exported"
and is ready to be used outside the virtual machine in which it was created.
CalendarImpl.java defines getDate(), the implementation for
the remote method declared in the iCalendar interface. The main()
method creates a registry on port 1099 (the default port for registry services),
creates an instance of the remote object CalendarImpl, and binds that instance
to the name CalendarImpl in the registry just created. Placing an object
in a registry makes it available to clients on other virtual machines. Once an object
is placed in a registry, any client that has access to the machine with the registry
can get a reference to the remote object by specifying the machine name, port number
(if it is different than the default port number), and the name given to the object
when it was exported to the registry.
TIP: A remote object can be bound to any
name in the registry. To prevent name collisions, it's a good idea to use the full
package qualified name of an object you export to the registry.
CalendarUser.java (in Listing 17.3) defines the client side of our client/server
application. Notice that CalendarUser.java is compiled with the remote interface
iCalendar.java (see Figure 17.1). On the client side, the type of a remote
object reference must be one of the remote interface types implemented by the remote
object. In this simple example, the remote interface class is copied to the client
at compile time. In another example later in this chapter, you see how you can use
dynamic class loading to transparently copy the remote interface classes at run time.
The first executable statement in CalendarUser.java is a call to the
static method Naming.lookup(String url). This method retrieves a reference
to a remote object specified with URL-like syntax. In this case, the remote object
resides on the machine ctr.cstp.umkc.edu and was bound to the name CalendarImpl.
Because no port number is specified, the default port number of 1099 is
used. The next two statements in the application are remote method invocations. The
objects returned are copies of the Date object created on the remote system.
The final statement prints the difference in time between the two Date objects
returned from the server.
So far, this example has concentrated almost exclusively on the client and server
programmer interfaces that support remote method invocations. We haven't said much
about how RMI works. To effectively use RMI, you have to look below the surface and
learn something about how it is implemented.
One of the advantages of using RMI is the abstraction it provides from the details
of interprocess communication. Part of this abstraction is provided by special classes
called stubs and skeletons. A stub is a client-side proxy that implements the remote
methods of a remote object. A skeleton is a server-side proxy that accepts a method
invocation from a client and dispatches the invocation to the target method on the
server. Figure 17.2 shows the relationship between these special classes and the
client and server portions of an RMI application. When a client invokes a remote
method, there is the illusion of directly invoking the method on the remote object.
In reality, the remote method invocation starts as a local method invocation on a
stub. The stub packages any parameters and sends the request to the skeleton for
the remote object. The skeleton unpacks the parameters that were sent and dispatches
the invocation to the target method. The stub and skeleton are also responsible for
marshaling any return values.
TIP: A stub is best described as a client-side
proxy, but the stub for a remote object must also be available to the server process.
The stub for a remote object is instantiated when the remote object is exported.
When a client makes a request for a reference to the remote object, it is the stub
for the remote object that is sent to the client.
Figure 17.2.
Communication layers
NOTE: Looking at abstraction in network
programming is a lot like peeling an onion. Each time you peel away a layer of abstraction,
you find another layer of abstraction. Certainly, stubs and skeletons don't communicate
directly. Beneath the stubs and skeletons is the remote reference layer. The remote
reference layer implements the semantics of the server and the specific invocation.
Beneath the remote reference layer is the transport layer. The transport layer is
responsible for setting up and managing the connection. Currently, only one type
of remote reference layer and transport layer is supported.
You must be aware of stubs and skeletons because, when you create a remote object,
you also have to create the stub and skeleton for that remote object. Stubs and skeletons
are created by the rmic compiler. Figure 17.1 shows the stub and skeleton
being created for this simple example. Also notice that the stub is copied to the
client machine. Later in this chapter, you see how you can use dynamic loading to
automatically deliver stubs to clients at run time.
This simple example introduces many of the concepts that are explained in more
detail in the following sections.
Remote Objects and the Remote Interface
A remote object was defined as an object with methods that can be called from
another Java virtual machine; a remote interface was defined as an interface that
declares the remote methods for a remote object. This section gives a more precise
definition of both these terms and how they work together to define the semantics
of a remote object.
A remote interface is a Java interface that extends (directly or indirectly) the
interface java.rmi.Remote. java.rmi.Remote declares no methods:
public interface Remote {}
The java.rmi.Remote interface is used exclusively to identify remote
objects. Remote objects must directly or indirectly implement this interface.
A remote object can implement any number of remote interfaces and can extend other
remote implementation classes. Java's cast and instanceof operators
can be used to cast between and test for any of the remote interface types supported
by the implementation of the remote object.
Remote objects outside the virtual machine on which they were created must be
declared as their remote interface type rather than as their implementation type.
The client's interface to a remote object is through the stub generated for the remote
object. The stub implements only those methods declared in a remote interface. Only
the methods declared in a remote interface can be invoked from another Java virtual
machine.
A method defined in a remote interface must have java.rmi.RemoteException
declared in its throws clause. A remote method invocation across a network
is fraught with perils. This exception, which extends java.io.IOException,
provides a mechanism to gracefully handle unlikely but possible failure scenarios.
The remote objects in all the examples we have looked at so far extend java.rmi.server.UnicastRemoteObject.
However, remote objects aren't required to extend this class. As stated earlier,
a remote object is a class that implements a remote interface. You should, however,
be aware of the semantics inherited by extending UnicastRemoteObject.
UnicastRemoteObject is currently the only type of server object supported
by RMI. UnicastRemoteObject provides support for point-to-point active object
references. The most important features inherited from UnicastRemoteObject
are these:
- Automatic export
- java.lang.Object behavior
Before a remote object can be passed as a parameter or returned as a result, it
must be exported. A remote object that extends UnicastRemoteObject is exported
in the constructor of the UnicastRemoteObject class. Remote objects that
don't extend UnicastRemoteObject must be exported explicitly with the static
method java.rmi.server.UnicastRemoteObject .exportObject().
The other feature inherited by extending UnicastRemoteObject is correct
java.lang.Object behavior. Remote objects that extend UnicastRemoteObject
inherit more appropriate behavior for the equals(), hashCode(),
and toString() methods.
To understand why the default java.lang.Object behavior for equals(),
hashCode(), and toString() doesn't work for remote objects, consider
the following scenario. A server creates and exports a remote object that doesn't
extend any other objects and doesn't redefine the equals() method inherited
from java.lang.Object. The server receives back from a client a reference
to the remote object it exported. The server now has two references to the same remote
object: one to the implementation class and one to the stub for the remote object.
Because both references are to the same object, you would expect them to compare
equally, but they don't. The equals() method for the implementation class
is inherited from java.lang.Object and knows nothing about remote objects.
UnicastRemoteObject redefines equals(), hashCode(), and
toString() to work correctly for remote objects.
The Remote Object Registry
When a client/server system using RMI first starts, there must be some way for
a client to get its first reference to a remote object. Once a client has a remote
object reference, other remote references can be passed to the client in the form
of parameters or return values. A remote object registry is the mechanism a client
can use to get its first remote object reference.
A remote object registry provides a simple name service that binds a character
string name to a remote object reference. A remote object registry has both a client
and a server interface. A server can bind, unbind, or rebind a name to a remote reference;
a client can look up the remote reference for a certain name. A remote object registry,
like other TCP/IP-based tools, listens for requests on a certain port. You can have
multiple registries running on the same machine as long as they are listening on
different ports. The default port number for the registry is 1099.
NOTE: The remote object registry is not
completely open. To bind, unbind, or rebind an object in the registry, you must be
running on the same computer as the registry. The simple registry mechanisms provided
with the JDK are not intended to be fully featured object request brokers. Instead,
they are intended to provide a simple bootstrap mechanism for clients that don't
require a lot of security.
There are two ways to start a registry. A simple tool (rmiregistry) is
provided with the JDK that will start a registry on the specified port. (This tool
is described in more detail in "RMI Tools," later in this chapter.) You
can also start a registry from within your Java application. The static method java.rmi.registry.LocateRegistry.createRegistry(port)
will create a registry on the specified port number. A registry created within one
Java application can be used by other servers on the same machine to export remote
references.
There are two classes for working with remote object registries:
- java.rmi.Naming
- java.rmi.registry.LocateRegistry
The java.rmi.Naming class provides a group of static methods for binding
and looking up objects using familiar URL-like syntax. Each method takes a String
URL in this form:
rmi://machine:port/name
For example, the following URL identifies the object named StudentDB
in the registry listening on port 1098 running on the machine named ctr.cstp.umkc.edu:
rmi://ctr.cstp.umkc.edu:1098/StudentDB
The java.rmi.registry.LocateRegistry class provides a group of static
methods for retrieving the registry on a particular host and for creating a registry
on the local host. Note that an application is allowed to create only one registry.
Although other registries can be running on the host, each process is allowed to
create only one registry.
Dynamic Class Loading
The Internet is becoming one of the most efficient software distribution channels
ever to be used. On the very day programs are released and made available on the
Internet, hundreds of users download and use them immediately. The dynamic class
loading capabilities of RMI take software distribution over the Internet one step
further by providing support for automatically downloading applications and application
components at run time.
To fully understand the dynamic class loading capabilities in RMI, you should
know what a class loader is and how it is used during the execution of a Java program.
A Java application or applet is defined by one or more classes. A class can be
a system class or one written by the developer of the application or applet. At run
time, the classes are loaded into the system for execution. As part of the runtime
behavior of a Java virtual machine, class loaders are used to load the classes that
make up an application. The class loaders give the Java programming language a measure
of security and flexibility.
You may already be familiar with two existing types of class loaders: the AppletClassLoader
and the default class loader.
The AppletClassLoader is used to download an applet class and all the
classes used directly by the applet. The AppletClassLoader follows a well-defined
search path as it searches for a class. The local class file is searched first. If
the class loader doesn't find a match, it looks for the class at the code base for
the applet.
The default class loader is used when a Java application is started from the command
line with the java interpreter. The default class loader looks for classes
in the locations designated by the environment variable CLASSPATH.
The RMI runtime system introduces a new type of class loader called the RMIClassLoader.
The RMIClassLoader supports two types of dynamic class loading. First, the
RMIClassLoader can be used to download stubs, skeletons, and the extended
classes of parameters and return values. These are the classes that a client may
require to make a remote method invocation but that aren't specifically used within
the client application.
Second, the RMIClassLoader can be used to download arbitrary classes
and all the classes used within the class. With this capability, a small bootstrap
client application can be delivered to the end user. During execution, this bootstrap
client connects to the server and downloads all the classes that make up the application.
The RMIClassLoader--like the AppletClassLoader and the default
class loader--follows a certain search order when looking for classes at run time.
The RMIClassLoader looks for classes in the following order:
- 1. The local CLASSPATH environment variable.
2. The URL encoded with a local or remote object being passed as a parameter
or return value. When local and remote objects are passed as parameters or return
values, the URL of the object's class loader is encoded with the object. If the class
of the object was originally loaded by the default class loader, the value of the
property java.rmi.server.codebase, if defined, is associated with the object.
If the class was originally loaded by any other class loader, the URL of that class
loader is used instead.
3. For stubs and skeletons of objects created in the local machine, the
URL specified by the java.rmi.server.codebase property is used.
When the locations of class files are specified by a URL, the URL points to a
directory on the Internet using standard Internet Web protocols like HTTP and FTP.
For example, the following URL points to a directory on a Web server that contains
the class files for some objects:
http://ctr.cstp.umkc.edu/java/classes/
Let's look at how this capability can be used in practice. In the simple example
presented earlier in this chapter, the stub of the remote object had to be copied
to the client by hand before the client could be started. Now we'll show two ways
to copy the stub for the remote object to the client at run time using the RMIClassLoader.
The first method relies on the URL's being coded into the stub for the remote
object being retrieved from the server. To encode the URL into the stream for the
remote object, you must add the following four lines as the first executable statements
in the code for the server CalendarImpl.java:
System.setSecurityManager(new RMISecurityManager());
System.getProperties().put(
"java.rmi.server.codebase",
"http://ctr.cstp.umkc.edu/java/classes/");
The stub for the remote object (CalendarImpl_Stub.class) must be placed
in the directory on the server defined by the URL http://ctr.cstp.umkc.edu/java/classes/.
Before the RMIClassLoader can load a class from a remote source, a security
manager must be in place. (The security manager for the RMI class loader is discussed
in more detail later in this chapter.) Because the client will now be loading the
stub for the remote object over the network, you must add the following line as the
first executable statement in the client CalendarUser.java:
System.setSecurityManager(new RMISecurityManager());
Both the classes CalendarUser and iCalendar must be copied to--or
created on--the client. This time, when CalendarUser starts, the RMIClassLoader
loads the stub from the URL specified in the byte stream for the remote object reference
passed to the client. The complete source code for this example is included on the
companion CD-ROM in the directory dynload1.
The second method for copying the stub for the remote object to the client relies
on the property java.rmi.server.codebase being set on the client machine
at the time a reference to the remote object is requested. This method requires no
changes to the server application CalendarImpl in Listing 17.2. Instead,
you must add the following lines as the first executable statements in the client
application CalendarUser.java:
System.setSecurityManager(new RMISecurityManager());
System.getProperties().put(
"java.rmi.server.codebase",
"http://ctr.cstp.umkc.edu/java/classes/");
Notice that these are the same lines we added to the server to set the value of
the URL in the stream for the stub of the remote object. When the lines are added
to the client application instead of to the server, the RMIClassLoader doesn't
find a valid URL in the object stream (the second place it looks, as listed earlier
in this section). The next place the RMIClassLoader looks for the URL is
the local java.rmi.server.codebase property. Here it finds a valid URL;
if the stub has been copied to the directory pointed to by the URL, the remote object
stub is downloaded from the server. The complete source code for this example is
included on the companion CD-ROM in the directory dynload2.
The other type of dynamic class loading supported by the RMIClassLoader
is the downloading of a complete class and all the classes used within it. The application
shown in Listings 17.4 and 17.5 demonstrates this capability.
Listing 17.4. NetworkApp.java: A class to be loaded remotely.
import java.lang.Runnable;
import java.awt.*;
public class NetworkApp implements Runnable {
Frame f;
public NetworkApp(Frame f) {
this.f = f;
};
public void run() {
Label l = new Label("Latest version of your application.",
Label.CENTER);
f.add("Center",l);
f.pack();
f.repaint();
}
}
Listing 17.5. BootstrapClient.java: A class to bootstrap
NetworkApp.
import java.lang.Runnable;
import java.rmi.server.RMIClassLoader;
import java.rmi.RMISecurityManager;
import java.net.URL;
import java.awt.*;
import java.lang.reflect.Constructor;
import java.awt.event.*;
public class BootstrapClient {
public static void main(String args[]) {
System.setSecurityManager(new RMISecurityManager());
Frame cf = new CloseableFrame("NetworkApp");
cf.show();
try {
URL url = new
URL("http://ctr.cstp.umkc.edu/java/NetworkApp/");
Class cl = RMIClassLoader.loadClass(url,"NetworkApp");
Class argTypes[] = {Class.forName("java.awt.Frame")};
Object argArray[] = {cf};
Constructor cntr = cl.getConstructor(argTypes);
Runnable client = (Runnable)cntr.newInstance(argArray);
client.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class CloseableFrame extends Frame
implements WindowListener {
public CloseableFrame(String s) {
super(s);
addWindowListener(this);
}
public void windowClosing(WindowEvent e){this.dispose();}
public void windowOpened(WindowEvent e){}
public void windowIconified(WindowEvent e){}
public void windowDeiconified(WindowEvent e){}
public void windowClosed(WindowEvent e){}
public void windowActivated(WindowEvent e){}
public void windowDeactivated(WindowEvent e){}
}
The file NetworkApp.java (shown in Listing 17.4) should be compiled and
the resulting class file moved to the directory pointed to by the URL http://ctr.cstp.umkc.edu/java/NetworkApp/.
The bootstrap application, BootstrapClient.java, should be moved to the
client machine and compiled. When started, BootstrapClient downloads the
class file NetworkApp from the HTTP server on ctr.cstp.umkc.edu
and creates an instance of the class for local execution. The complete source code
for this example is included on the companion CD-ROM in the directory netapp.
RMI to and from Applets
RMI also works with applets. An applet can look up a reference to a remote object
and invoke remote methods defined for the remote object. Applets can retrieve a reference
to a remote object only from the server from which the applet came. This restriction
exists for the same reason that an applet can open a socket only on the server from
which the applet came. If an applet could set up a communication path between any
two hosts, it could create a security risk by opening a communication path to a machine
with more restrictive access and tunneling information from outside the network to
this otherwise secure machine. For the same reason, applets also are not allowed
to listen on a port.
NOTE: The restriction that you can connect
only to the server from which the applet came is enforced by the security manager
of the applet viewer or browser. Some applet viewers can be configured to relax some
or all of the network restrictions.
These restrictions make programming some systems difficult. For example, a common
requirement for applets that make up a groupware application is the ability to be
notified by the server when some condition changes. Because applets can't listen
on a socket, the applet programmer must open a connection back to the server, keep
the connection open, and wait to be notified of any updates. One of the design goals
of RMI is to allow callbacks to an applet.
A callback is a convention used by an object to request notification of a future
event. An object that wants to receive notification from a server object of some
future event can leave with the server a reference to a method that should be called
when the information arrives.
RMI supports callbacks to applets. Rather than opening a connection back to the
server, an applet can create a remote object, export it, and send a reference back
to the server. The server can make a remote method call on the reference to send
a message to the applet.
Compiling and Running the Weather Forecast Application
Listings 17.6 through 17.11 show a complete example that uses callbacks to communicate
with applets. The example demonstrates a client/server application for delivering
weather forecasts. The forecasts originate on the server and are broadcast to remote
applets. Client applets interested in knowing the most up-to-date weather forecast
register a callback with the server. When the server detects a change in the forecast,
it sends the new forecast to all the clients that have registered a callback with
the server. (So that we can focus on the RMI concepts in this sample application,
the "forecast" is limited to the current temperature.)
Listing 17.6 shows the interface for the remote object exported by the server.
Through this interface, client applets can request the current forecast or register
a callback with the server to receive future forecasts.
To register a callback with the server, the client applet creates and exports
a remote object and then passes a reference to the server. Listing 17.7 shows the
interface for the remote object exported by the client applet. The interface declares
one remote method. The server uses this interface to pass a new forecast back to
client applets.
Listing 17.8 shows the source code for the server. The server implements the remote
methods for getting a new forecast and for registering a callback to receive a new
forecast. The server implements the Runnable interface and keeps a separate
thread running as long as there are clients waiting for a new forecast. To simulate
a fluctuating forecast, the server generates a random number between +3
and -3. If the random number generated is not zero, the number is added
to the current temperature to get the new forecast. Client references are kept in
a hash table. A client reference is a reference to the remote object exported by
the client. A new forecast is sent back to the client by invoking a remote method
on the remote object.
Listing 17.9 shows the source code for the client applet. The client applet implements
a remote interface and is therefore a remote object. The applet exports itself, gets
a reference to the remote object exported by the server, and registers a callback
with the server.
Because this chapter is about RMI and not about good programming practices, we
show how to compile and run the application from a single directory under a Web server.
In practice, you will probably want to keep your source code outside your Web server
directory tree.
The complete source code for this example is included on the companion CD-ROM
in the directory forecast. Copy the contents of this directory (the files
shown in Listings 17.6 through 17.11) to a location under your Web server. Run the
make.bat file to compile the application and start the server. You can now
connect to the server through the applet in this directory. For example, if you copy
the files to the directory /java/forecast/ off the Web server root on the
machine ctr.cstp.umkc.edu, give the following command to start the applet
viewer on the forecast applet:
appletviewer http://ctr.cstp.umkc.edu/java/forecast/forecast.html
Listing 17.6. iForecast.java: The interface to the weather
forecast server.
import java.rmi.*;
public interface iForecast extends java.rmi.Remote {
int currentTemperature()
throws RemoteException;
void requestUpdates(iUpdatedForecast client)
throws RemoteException;
}
Listing 17.7. iUpdateForecast.java: The interface for the
callback.
import java.rmi.*;
public interface iUpdatedForecast extends java.rmi.Remote {
void newForecast (int newTemperature) throws RemoteException;
}
Listing 17.8. WeatherForecastServer.java: The server and
remote object implementation.
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.LocateRegistry;
import java.util.*;
public class WeatherForecastServer extends UnicastRemoteObject
implements iForecast, Runnable {
private Hashtable clientTable = new Hashtable();
private Random rand = new Random();
private Thread notifier = null;
private int fahrenheit = 0;
public WeatherForecastServer() throws RemoteException {
fahrenheit = 78;
}
public int currentTemperature() throws RemoteException {
return fahrenheit;
}
public synchronized void requestUpdates (iUpdatedForecast client)
throws RemoteException {
iUpdatedForecast prevEntry = (iUpdatedForecast)clientTable.get(client);
if (prevEntry == null) {
clientTable.put(client,client);
}
if (notifier == null) {
notifier = new Thread(this);
notifier.start();
}
}
public void removeClient(iUpdatedForecast client) {
clientTable.remove(client);
if (clientTable.isEmpty()) {
Thread thread = notifier;
notifier = null;
thread.stop();
}
}
public void run() {
int newFahrenheit = 0;
while (true) {
try {Thread.currentThread().sleep(2000);}
catch (InterruptedException e) { }
// Generate a random number between -3 and +3.
newFahrenheit = fahrenheit + (rand.nextInt() % 4);
if (newFahrenheit != fahrenheit) {
fahrenheit = newFahrenheit;
Enumeration enum = clientTable.keys();
while (enum.hasMoreElements()) {
iUpdatedForecast client = (iUpdatedForecast)enum.nextElement();
try {
System.out.println("sending update ");
client.newForecast(fahrenheit);
} catch (RemoteException e) {
System.out.println("client passed away");
removeClient(client);
}
} // while client table has more elements
} // if temperature has changed
} // while(true)
} // void run()
public static void main(String args[]) {
try {
System.out.println("main: creating registry");
LocateRegistry.createRegistry(1099);
iForecast server = new WeatherForecastServer();
Naming.rebind("///WeatherForecastServer", server);
System.out.println("Ready for RMI's.");
} catch (Exception e) {
e.printStackTrace();
}
} // static void main()
}
Listing 17.9. WeatherForecastApplet.java: The applet with
the callback.
import java.applet.Applet;
import java.awt.*;
import java.net.URL;
import java.rmi.*;
import java.rmi.server.*;
public class WeatherForecastApplet extends Applet
implements iUpdatedForecast {
int currentTemp = 0;
public void init() {
try {
UnicastRemoteObject.exportObject(this);
URL base = getDocumentBase();
String serverName = "//" + base.getHost() +
"/WeatherForecastServer";
iForecast server = (iForecast) Naming.lookup(serverName);
currentTemp = server.currentTemperature();
server.requestUpdates(this);
} catch (Exception e) {
e.printStackTrace();
return;
}
setLayout(null);
}
public void newForecast (int newTemperature) {
currentTemp = newTemperature;
repaint();
}
public void paint(Graphics g) {
g.drawString("Temp: " + currentTemp,25,25);
}
}
Listing 17.10. make.bat: The batch file that builds the
application.
javac WeatherForecastApplet.java WeatherForecastServer.java
rmic WeatherForecastServer WeatherForecastApplet
java WeatherForecastServer
Listing 17.11. forecast.html: The HTML file that downloads
the applet.
<HTML>
<title>Weather Monitor</title>
<center> <h1>Weather Monitor</h1> </center>
<p>
<applet
code="WeatherForecastApplet"
width=100
height=50>
</applet>
</HTML>
Distributed Object-Oriented Programming
The RMI examples shown so far have focused on using RMI to pass control and simple
data types between virtual machines. This section explains how RMI can be used to
enable object-oriented solutions in a distributed environment.
Just as C++ can be used as a better C, RMI can be used as an alternative to sockets
for moving data between virtual machines. The most significant benefit of using C++
as a programming language is that it enables a new class of object-oriented solutions.
Similarly, the benefits of RMI are only fully realized when it is also used to enable
object-oriented solutions for distributed computing problems.
An object-oriented solution in a distributed environment is characterized by passing
objects--and not just simple data types--between virtual machines. Objects encapsulate
both methods and data. The objects can be of the base type or of a more derived type
that overrides the methods in the base type.
The example in Listings 17.12 through 17.15 shows an object-oriented solution
to the problem of providing personal information to organizations and acquaintances.
There are many situations in which you are required to provide personal information
about yourself. Ideally, you would have a "smart card" that contains all
your personal information that you can pass to the requester. The card has to be
"smart" because the type and amount of information you want to divulge
is most likely determined by the status and intentions of the requester. In the example
shown here, an object with a well-known interface is used as a smart card.
Listing 17.12 shows the source code for the base class of our smart card. Clients
are expected to extend the base class and specialize the methods with their own preferences.
Notice that the base class implements java.io.Serializable. The arguments
and return values of remote methods must implement the Serializable interface.
This interface has no methods defined for it; it is only there to remind the compiler
that special information must be saved with the class. This information is used by
the runtime system to package and ship the contents of the class across the network.
You don't have to write any special routines for marshaling the contents of the class;
implementing the Serializable interface is all that is required to pass
a new class you have defined as a parameter or return value.
Listing 17.13 shows the source code for the interface to the remote object exported
by the server. The server exports a remote object with only one remote method. The
method is used by clients to pass a smart card to the server.
Listing 17.14 shows the source code for the server. The server has many of the
elements discussed in earlier examples. The server is itself a remote object. It
creates and exports an instance of itself, and the server sets the codebase
property to the location where the client can find the stub for the remote object
it exports. The one new element is in the implementation for the remote method. The
server expects from the client an object rather than a primitive data type. The server
invokes a method on the object to retrieve a primitive data type. Also notice that
the server passes some data to the method on the object. In this example, the server
passes a primitive data type. In a more realistic example, the server would probably
pass another object with a well-known interface.
Listing 17.15 shows the source code for the client. The client creates MyID,
an extended class of the smart card base class. This extended class is passed to
the server in a remote method invocation. When the extended class arrives at the
server, the server searches its CLASSPATH path for the class definition
for MyID. Of course, the server won't find the class locally. The second
place the server looks is at the location specified in the marshaling stream for
the object. Because the client has specified the codebase property, the
server will find a URL in the marshaling stream. The client should have copied the
class file for MyID to the location defined by the URL specified for the
codebase property.
The complete source code for this example is included on the companion CD-ROM
in the directory ID.
Listing 17.12. SmartCard.java: The base class for a smart
ID.
public class SmartCard implements java.io.Serializable {
public String name(boolean commercial) {
return "(Name withheld)";
}
}
Listing 17.13. iRegister.java: The interface for the registration
server.
import java.rmi.*;
public interface iRegister extends Remote {
void register(SmartCard id) throws RemoteException;
}
Listing 17.14. RegistrationServer.java: The registration
server.
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
public class RegistrationServer
extends UnicastRemoteObject
implements iRegister {
public RegistrationServer() throws RemoteException {}
public void register (SmartCard id) throws RemoteException {
boolean commercial = false;
System.out.println("Name: " + id.name(commercial));
}
public static void main(String args[]) {
RegistrationServer rs;
System.setSecurityManager(new RMISecurityManager());
System.getProperties().put(
"java.rmi.server.codebase",
"http://ctr.cstp.umkc.edu/java/classes/");
try {
LocateRegistry.createRegistry(1099);
rs = new RegistrationServer();
Naming.bind("rmi:///RegistrationServer", rs);
System.out.println("Ready for RMI's");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Listing 17.15. RegistrationClient.java: The registration
client.
import java.rmi.*;
public class RegistrationClient {
public RegistrationClient() {}
public static void main(String args[]) {
iRegister regServer;
System.setSecurityManager(new RMISecurityManager());
System.getProperties().put(
"java.rmi.server.codebase",
"http://ehscott.cstp.umkc.edu/java/classes/");
try {
regServer = (iRegister)
Naming.lookup("rmi://ctr.cstp.umkc.edu/RegistrationServer");
MyID id = new MyID();
regServer.register(id);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyID extends SmartCard {
public String name(boolean commercial) {
if (commercial)
return super.name(commercial);
else
return "Eddie Burris";
}
}
Security
As a programming language, Java gets high marks for security. From the beginning,
Java has supported applets--the local execution of classes from remote, unknown,
and possibly untrusted sources.
Many of the security capabilities already provided in the language have been adapted
for RMI applications. This section identifies the potential security risks inherent
in an RMI application, describes Java's built-in capabilities for guarding against
these risks, and discusses some options for making RMI applications more secure.
Most security issues in a distributed system fall into one of the following categories:
- Runtime integrity. Dynamic class loading allows class files to be downloaded
from remote sources. Safeguards are required to ensure that when the methods of these
classes are invoked, they don't violate the integrity of the system.
- Encryption During a remote method invocation, data in the form of parameters
and return values may be sent across the network. Encryption is wanted when the data
is sensitive; it is not wanted when transfer efficiency is more important.
- Authentication For some applications, it may be desirable to allow only
authorized users to access a remote object.
Runtime integrity is provided by the RMIClassLoader and RMISecurityManager.
The RMIClassLoader is responsible for loading stubs, skeletons, and the
extended classes of parameters and return values. For example, if B extends
A, and an object of type B is passed to a remote method defined
to accept an object of type A, the extended class of A (that is,
B) is loaded by the RMIClassLoader. The RMIClassLoader
also does bytecode verification on classes loaded from remote sources. Bytecode verification
is the process of scanning the bytecodes to make sure that they represent a valid
Java class. An invalid class can violate the integrity of the VM in many ways. For
example, an invalid class could craft a pointer and write to a different stack frame,
possibly changing the behavior of another method. An authentic Java compiler does
not let you create an invalid Java class, but because the Java runtime environment
can't be sure that a "trusted" compiler was used, bytecode verification
is done inside the Java VM.
As mentioned earlier, the RMIClassLoader looks for a class file first
in the locations defined by the CLASSPATH environment variable, then it
looks at the URL encoded with the object being passed, and then it looks at the URL
defined by the java.rmi.server.codebase property. If you don't want a specific
class to be loaded over the network, you can put it in one of the directories specified
on the CLASSPATH environment variable. If you want to be sure of the location
from which classes are loaded, set java.rmi.server.useCodebaseOnly to true.
If the class isn't found on the CLASSPATH or at the URL specified by the
java.rmi.server.codebase property, you get an exception.
Certain system services (such as starting a process or writing to a file) are
considered privileged. Before the system performs one of these privileged services,
it checks to see whether a security manager is defined. If one is defined, the system
queries the security manager to determine whether the requested operation is currently
allowed. Before you can load a class from a remote source, you must set a security
manager. You can define your own security manager or use the restrictive RMISecurityManager()
with this call:
System.setSecurityManager(new java.rmi.RMISecurityManager());
The default behavior of RMISecurityManager() is to not allow most privileged
services if any of the methods on the execution stack belong to a class loaded from
a remote source.
RMI doesn't support encryption directly, but it does have built-in support that
allows you to add encryption.
RMI uses the socket abstraction for communication. The RMI transport layer requests
a client or server socket; subsequent communication is performed through the abstract
methods of these objects. RMI supports encryption using a socket factory design pattern.
If you want to communicate over an encrypted channel, you must do the following:
- Define client and server sockets that implement the encryption algorithm you
want.
- Create and set a socket factory that returns the client and server sockets you
have defined.
The one drawback to this solution is that all RMI communication is then done using
the sockets you define--including communication with the RMI registry and all clients.
It is expected that the next release of the language will contain support for a more
flexible method of communicating over a secure channel.
For more information about creating a socket factory, see the documentation for
the RMISocketFactory class in the java.rmi.server package. This
class contains the methods for setting a socket factory.
There is really no special support built into RMI to enable authentication. If
you want to authenticate clients, you have to build that support on top of RMI.
RMI Tools
There are two RMI-related command-line utilities: rmic and rmiregistry.
The rmic command is used to generate the stub and skeleton for a remote
object. It has one required parameter: the package-qualified name of the remote object.
Here is an example that shows how to call rmic:
rmic database.EmployeeImpl
A stub is a class file used as a proxy on the client side. A skeleton is a class
file used as a proxy on the server side. The stub is responsible for packaging the
arguments of a remote method invocation and passing control to the server. The skeleton
on the server side unpacks any arguments and dispatches the invocation to the implementation
of the remote object.
Both the stub and skeleton class files for a remote object must be available to
the virtual machine that is exporting the remote object. The clients of a remote
object need access to only the stub class file. You can copy the stub file to the
client or make it available from a URL.
When you create a remote object in one virtual machine, there must be a way for
a client in another VM to get a reference to the remote object. The remote object
registry is the mechanism for making references to remote objects available to clients.
The rmiregistry command creates a remote object registry on a specific port.
Here is an example of the use of this command:
rmiregistry 1088
The port number is optional. If you omit it, a remote object registry is created
on the default port, 1099. You can also create a remote object registry
from within an application with a call to java.rmi.registry.LocateRegistry.createRegistry(int
port). Other processes on the same machine can export remote objects to a registry
created with createRegistry(port), but if the process that started the registry
terminates, the registry is no longer available.
Summary
RMI expands the scope of the runtime Java object model from a single virtual machine
to a collection of networked virtual machines. Remote objects can be created and
published through a remote object registry. Clients can retrieve a reference to a
remote object and invoke methods on the remote object with the same syntax and ease
as they can make a local method invocation. Local and remote objects can be passed
between virtual machines. Local objects are passed by copy; remote objects are passed
by reference.
Two of the more interesting capabilities enabled by RMI are dynamic class loading
and callbacks to applets. Dynamic class loading allows class files to be loaded across
the network. Stubs, skeletons, and the extended classes of parameters and return
values are loaded transparently. Support is also provided for loading specific classes.
You can control how much freedom these downloaded classes have by specifying a security
manager.
The freedom to export a remote object from an applet allows applets to register
a callback with a server running on the machine from which the applet came. This
capability makes writing distributed systems using applets much easier.
|