TOC
BACK
FORWARD
HOME

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.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.