The Remote Method Invocation (RMI) API adds the capability to develop fully distributed applications to Java's overwhelming list of credentials. This capability is provided by a set of intuitive and easy-to-use packages that make distributed application development a natural extension of single host programming. The RMI API is much simpler to use than other currently available distributed application programming frameworks, including Common Object Request Broker Architecture (CORBA) and Distributed Component Object Model (DCOM).
In this chapter, you'll learn why the RMI API provides a superior development approach for distributed programming in Java. You'll learn about the packages of the RMI API and then use them to develop a simple distributed application. When you finish this chapter, you'll be thoroughly introduced to the java.rmi packages.
Distributed applications are those that execute across multiple host systems. Objects executing on one host invoke methods of objects on other hosts to help them perform their processing. These methods execute on the remote hosts--hence the name remote method invocation. The remotely invoked objects perform computations and may return values that are used by the local objects. Figure 38.1 illustrates this concept.
FIGURE 38.1. A distributed application.
A number of approaches to implementing distributed systems have been developed. The Internet and the Web are examples of distributed systems that have been developed using the TCP/IP client/server approach. Clients and servers communicate via TCP and UDP sockets. While the Internet and the Web are remarkably successful, the use of sockets requires separate application-level protocols for client/server communication. The overhead associated with these protocols prohibits the fine-grain parallel operation that is possible through other approaches.
The Distributed Component Object Model (DCOM) makes use of remote procedure calls to allow objects executing on one system to invoke the methods of objects that run on other systems. DCOM provides an excellent framework for developing distributed object-based systems. However, its major shortcoming, from a Java perspective, is that it is oriented toward legacy Windows applications and does not provide a 100% pure Java solution. The use of DCOM with Java is covered in Chapter 54, "Dirty Java."
The Common Object Request Broker Architecture (CORBA) supports an object-oriented framework for developing distributed systems. CORBA's strong point is that it supports a language-independent model. However, this advantage is a disadvantage when it comes to Java because it forces Java applications to use an external, albeit neutral, object model. CORBA is covered in Chapter 41, "Java IDL and ORBs."
The Remote Method Invocation (RMI) API of Java is a Java-specific approach to developing distributed systems. RMI's major advantage is that it is fully integrated with the Java object model, highly intuitive, and easy to use. The main disadvantage of RMI is that its use is limited to Java. RMI, unlike CORBA, cannot be used with objects written in other programming languages. Consequently, RMI is optimal for pure Java enterprise applications. However, if you need to build distributed systems containing objects that are written in languages other than Java, CORBA is a better solution.
Before describing the RMI API, let's cover some of the terminology it uses.
RMI is built upon the fundamental notion of local and remote objects. This concept is relative. Local objects are objects that execute on a particular host. Remote objects are objects that execute on all other hosts. Objects on remote hosts are exported so that they can be invoked remotely. An object exports itself by registering with a remote registry server. The remote registry server helps objects on other hosts to remotely access its registered objects. It does this by maintaining a database of names and the objects that are associated with those names (see Figure 38.2).
FIGURE 38.2. Remote objects register themselves for remote access.
Objects that export themselves for remote access must implement the Remote interface. This interface identifies the object as capable of being accessed remotely. Any methods that are to be invoked remotely must throw the RemoteException. This exception is used to indicate errors that may occur during an RMI.
Java's RMI approach is organized into a client/server framework. A local object that invokes a remote object's method is referred to as a client object, or simply a client. A remote object whose methods are invoked by a local object is referred to as a server object, or a server.
Java's RMI approach makes use of stubs and skeletons. A stub is a local object that acts as a local proxy for the remote object. The stub provides the same methods as the remote object. Local objects invoke the methods of the stub as if they were methods of the remote object. The stub then communicates these method invocations to the remote object via a skeleton which is implemented on the remote host. The skeleton is a proxy for the remote object that is located on the same host as the remote object. The skeleton communicates with the local stub and propagates method invocations on the stub to the actual remote object. It then receives the value returned by the remote method invocation (if any) and passes this value back to the stub. The stub, in turn, sends the return value on to the local object that initiated the remote method invocation.
Stubs and skeletons communicate through a remote reference layer. This layer provides stubs with the capability to communicate with skeletons via a transport protocol. RMI currently uses TCP for information transport, although it is flexible enough to use other protocols. Figure 38.3 shows how stubs and skeletons are used in Java RMI.
The RMI API is implemented by the following five packages:
FIGURE 38.3. Java RMI uses stubs and skeletons to support client/server communication.
The following sections describe each of these packages. Don't worry if it seems like there's a lot of material to learn. When you actually use RMI, you'll only use a fraction of the RMI API. When you read over the description of the RMI packages, try to get a feel for the classes and interfaces that are available to you.
The java.rmi package declares the Remote interface, the MarshalledObject, Naming and RMISecurityManager classes, and a number of exceptions that are used with remote method invocation.
The Remote interface must be implemented by all remote objects. This interface has no methods. It is used for identification purposes.
The MarshalledObject class was added in JDK 1.2. It is used to maintain a serialized byte stream of an object. Its get() method is used to retrieve a deserialized version of the object.
The Naming class provides static methods for accessing remote objects via RMI URLs. The bind() and rebind() methods bind a remote object name to a specific RMI URL. The unbind() method removes the binding between an object name and an RMI URL. The lookup() method returns the remote object specified by an RMI URL. The list() method returns the list of URLs that are currently known to the RMI registry.
The syntax for RMI URLs is as follows:
rmi://host:port/remoteObjectName
The host and TCP port are optional. If the host is omitted, the local host is assumed. The default TCP port is 1099. For example, the following URL names the MyObject remote object that is located on the host athome.jaworski.com and is accessible via TCP port 1234:
rmi://athome.jaworski.com:1234/MyObject
The RMISecurityManager class defines the default security policy used for remote object stubs. It only applies to applications. Applets use the AppletSecurityManager class even if they perform RMI. You can extend RMISecurityManager and override its methods to implement your own RMI security policies. Use the setSecurityManager() method of the System class to set an RMISecurityManager object as the current security manager to be used for RMI stubs.
The java.rmi package defines a number of exceptions. The RemoteException class is the parent of all exceptions that are generated during RMI. It must be thrown by all methods of a remote object that can be accessed remotely.
NOTE: A remote object is allowed to have local methods that can be invoked locally. These methods do not need to throw RemoteException.
The java.rmi.registry package provides the Registry and RegistryHandler interfaces and the LocateRegistry class. These interfaces and classes are used to register and access remote objects by name. Remote objects are registered when they are identified to a host's registry process. The registry process is created when the rmiregistry program is executed.
The Registry interface defines the bind(), rebind(), unbind(), list(), and lookup() methods that are used by the Naming class to associate object names and RMI URLs. The registry interface also defines the REGISTRY_PORT constant that identifies the default TCP port used by the registry service.
The RegistryHandler interface provides methods for accessing objects that implement the Registry interface. The registryStub() method returns the local stub of a remote object that implements the Registry interface. The registryImpl() method constructs a Registry object and exports it via a specified TCP port.
The LocateRegistry class provides the static getRegistry() method for retrieving Registry objects on the local host or a remote host. It also provides the createRegistry() method to construct a Registry object and export it via a specified TCP port.
The java.rmi.server package implements several interfaces and classes that support both client and server aspects of RMI.
The RemoteObject class implements the Remote interface and provides a remote implementation of the Object class. All classes that implement remote objects, both client and server, extend RemoteObject.
The RemoteServer class extends RemoteObject and is a common class that is subclassed by specific types of remote object implementations. It provides the static setLog() and getLog() methods for setting and retrieving an output stream used to log information about RMI accesses. It also provides the getClientHost() method that is used to retrieve the host name of the client performing the remote method invocation.
The UnicastRemoteObject class extends RemoteServer and provides the default remote object implementation. Classes that implement remote objects usually subclass UnicastRemoteObject. Objects of the UnicastRemoteObject class are accessed via TCP connections on port 1099, exist only for the duration of the process that creates them, and rely on a stream-based protocol for client/server communication.
The RemoteStub class extends RemoteObject and provides an abstract implementation of client side stubs. A client stub is a local representation of a remote object that implements all remote methods of the remote object. The static setRef() method is used to associate a client stub with its corresponding remote object.
The RemoteCall interface provides methods that are used by stubs and skeletons to implement remote method invocations.
The RemoteRef interface is used by RemoteStub objects to reference remote objects. It provides methods for comparing and invoking remote objects and for working with objects that implement the RemoteCall interface.
The ServerRef interface extends the RemoteRef interface and is implemented by remote objects to gain access to their associated RemoteStub objects.
The Skeleton interface is implemented by remote skeletons. It provides methods that are used by the skeleton to access the methods being requested of the remote object, and for working with method arguments and return values.
The Unreferenced interface is implemented by a remote object to enable it to determine when it is no longer referenced by a client.
The RMIClassLoader class supports the loading of remote classes. The location of a remote class is specified by either an URL or the java.rmi.server.codebase system property. The static loadClass() method loads a remote class, and the static getSecurityContext() returns the security context in which the class loader operates. The LoaderHandler interface defines methods that are used by RMIClassClassLoader to load classes.
The Operation class is used to store a reference to a method. The getOperation() method returns the name of the method. The toString() method returns a String representation of the method's signature.
The ObjID class is used to create objects that serve as unique identifiers for objects that are exported as remote by a particular host. It provides methods for reading the object ID from and writing it to a stream. The UID class is an abstract class for creating unique object identifiers.
The LogStream class extends the PrintStream class to support the logging of errors that occur during RMI processing.
The RMISocketFactory class is used to specify a socket implementation for transporting information between clients and servers involved in RMI. This class provides three alternative approaches to establishing RMI connections that can be used with firewalls. The static setSocketFactory() method can be used to specify a custom socket implementation. The RMIClientSocketFactory and RMIServerSocketFactory interfaces provide support for both client and server sockets. The RMIFailureHandler interface defines methods that handle the failure of a server socket creation. The RMIFailureHandler interface provides the failure() method for handling exceptions that occur in the underlying RMI socket implementation.
The java.rmi.activation package is a new RMI package that was added to JDK 1.2. It provides the capabilities to activate remote objects as needed and to use persistent object references.
NOTE: Examples of using the java.rmi.activation package are provided in the next chapter.
The Activatable class defines the basic methods implemented by activatable, persistent objects. It contains two constructors. One constructor is used to create and register (with the activation system) objects that can be accessed via specific TCP ports. The other constructor is used to activate an object based upon an ActivationID object and persistent data that has been stored for that object. The export() object methods are used to make an object available for use via a specific TCP port. The getID() method returns an object's ActivationID (used to uniquely identify the object). The register() and unregister() methods register (and unregister) an object with the runtime system. The inactive() method is used to tell the activation system that an object is inactive, or, if active, that it should be deactivated.
Objects of the ActivationID class are used to uniquely identify activatable objects and contain information about how objects are to be activated. An object's ActivationID is created when the object is registered with the activation system. The activate() method is used to activate the object referenced by an ActivationID object. The equals() and hashCode() methods are used to compare two ActivationID objects. Two ActivationID objects are equal if they reference the same object.
The ActivationDesc class encapsulates the information necessary to activate an object. It provides five methods that can be used to retrieve this information. The getClassName() method returns the described object's class name. The getCodeSource() method returns a CodeSource object that identifies the described object's location and other source information. The getData() method returns a MarshalledObject object that contains serialized information used to initialize the described object. The getGroupID() method returns the described object's ActivationGroupID object. The getRestartMode() method returns the restart mode associated with the activation descriptor.
The ActivationGroup class is used to group activatable objects so that they execute in the same JVM. ActivationGroup objects are used to create instances of the activatable objects within their group. The activeObject() method is used to inform an ActivationGroup that an activatable object has been activated. The createGroup() method is used to specify the current ActivationGroup object for the current JVM instance. The currentGroupID() method returns the ActivationGroupID object of the current ActivationGroup object. The getSystem() method returns the current ActivationSystem object. The inactiveObject() method is invoked when an object in the group is deactivated (becomes inactive). This method deactivates the object if the object is still active. The inactiveGroup() method is used to report an inactive group to the group's ActivationMonitor object. The newInstance() method creates a new instance of an activatable object. The setSystem() method sets the ActivationSystem object for the current JVM.
The ActivationGroupID class uniquely identifies an ActivationGroup object and contains information about the object's activation system. The getSystem() method returns the ActivationSystem object that is used to activate the referenced ActivationGroup object. The equals() and hashCode() methods are used to compare ActivationGroupID objects in terms of their referenced ActivationGroupID objects.
The ActivationGroupDesc class encapsulates the information necessary to create an ActivationGroup object. The getClassName() method returns the described ActivationGroup object's class name. The getCodeSource() method returns the described ActivationGroup object's CodeSource object. The getData() method returns a MarshalledObject object that contains serialized data about the described ActivationGroup object. The CommandEnvironment inner class provides support for implementation-specific options.
The ActivationSystem interface is implemented by objects that register activatable objects and activatable object groups. The SYSTEM_PORT constant identifies the TCP port used by the activation system. The registerGroup(), registerObject(), unregisterGroup(), and unregisterObject() methods are used to register and unregister Activatable and ActivationGroup objects. The activeGroup() method is used to inform the activation system about an active ActivationGroup object.
The Activator interface is implemented by objects that activate objects that are registered with an ActivationSystem (object). The activate() method activates an object based upon its associated ActivationID object.
The ActivationInstantiator interface provides methods for classes that create instances of activatable objects. The newInstance() method creates new object instances based on their associated ActivationID and ActivationDesc objects.
The ActivationMonitor provides methods for maintaining information about active and inactive objects. The activeObject(), inactiveObjet(), and inactiveGroup() methods are used to collect this information.
The java.rmi.dgc package contains classes and interfaces that are used by the distributed garbage collector. The DGC interface is implemented by the server side of the distributed garbage collector. It defines two methods: dirty() and clean(). The dirty() method indicates that a remote object is being referenced by a client. The clean() method is used to indicate that a remote reference has been completed.
The Lease class creates objects that are used to keep track of object references. The VMID class is used to create an ID that uniquely identifies a Java virtual machine on a particular host.
Now that you've been introduced to the RMI API and covered each of its packages, you're probably wondering how you go about implementing a remote method invocation. I'll summarize the process in this section and then explain it in detail in the next chapter. I'll organize the discussion according to the steps performed on the remote host (server) and local host (client).
Because a remote object must exist before it can be invoked, we'll first cover the steps involved in creating the remote object and registering it with the remote registry. In the following section, we'll look at what it takes for a local object to access a remote object and invoke its methods.
Remote objects are referenced via interfaces. In order to implement a remote object, you must first create an interface for that object. This interface must be public and must extend the Remote interface. Define the remote methods that you want to invoke within this interface. These methods must throw RemoteException.
Listing 38.1 provides an example of a remote interface. The MyServer.java file is defined in the ju.ch38.server package. This file is located in the ju\ch38\server directory. The reason that I put it in a named package is so it can be found relative to your CLASSPATH. Edit your CLASSPATH, if necessary, to make sure that the ju.ch38.server package is accessible.
MyServer defines two methods: getDataNum() and getData(). The getDataNum() method returns an integer indicating the total number of data strings that are available on the server. The getData() method returns the nth data string.
Compile MyServer.java before going on to the next section.
package ju.ch38.server; import java.rmi.*; public interface MyServer extends Remote { int getDataNum() throws RemoteException; String getData(int n) throws RemoteException; }
After creating the remote interface, you must create a class that implements the remote interface. This class typically extends the UnicastRemoteObject class. However, it could also extend other subclasses of the RemoteServer class.
The implementation class should have a constructor that creates and initializes the remote object. It should also implement all of the methods defined in the remote interface. It should have a main() method so that it can be executed as a remote class. The main() method should use the setSecurityManager() method of the System class to set an object to be used as the remote object's security manager. It should register a name by which it can be remotely referenced with the remote registry.
Listing 38.2 provides the implementation class for the MyServer interface. The MyServerImpl class is also in the ju.ch38.server package. You should change the hostName value to the name of the host where the remote object is to be located.
The data array contains five strings that are retrieved by the client object via the getDataNum() and getData() methods. The getDataNum() method returns the length of data, and the getData() method returns the nth element of the data array.
The main() method sets the security manager to an object of the RMISecurityManager class. It creates an instance of the MyServerImpl class and invokes the rebind() method of Naming to register the new object with remote registry. It registers the object with the name MyServer and then informs you that it has successfully completed the registration process.
Compile MyServerImpl.java before going on to the next section.
package ju.ch38.server; import java.rmi.*; import java.rmi.server.*; public class MyServerImpl extends UnicastRemoteObject implements MyServer { static String hostName="athome.jaworski.com"; static String data[] = {"Remote","Method","Invocation","Is","Great!"}; public MyServerImpl() throws RemoteException { super(); } public int getDataNum() throws RemoteException { return data.length; } public String getData(int n) throws RemoteException { return data[n%data.length]; } public static void main(String args[]){ System.setSecurityManager(new RMISecurityManager()); try { MyServerImpl instance = new MyServerImpl(); Naming.rebind("//"+hostName+"/MyServer", instance); System.out.println("I'm registered!"); } catch (Exception ex) { System.out.println(ex); } } }
Once you have created the class that implements the remote interface, use the rmic compiler to create the stub and skeleton classes:
rmic ju.ch38.server.MyServerImpl
Run the rmic compiler from the ju\ch38\server directory. The rmic compiler creates the files MyServerImpl_Stub.class and MyServerImpl_Skel.class in this directory.
NOTE: You must supply the fully qualified package name of the class that you compile with rmic.
You'll need the MyServer.class interface file to compile your client software, and you'll need MyServer.class and MyServerImpl_Stub.class to run your client. Before going any further, you should copy these files to an appropriate location on your client host. They must be in a path ju\ch38\server that is accessible via the client's CLASSPATH. I suggest putting them in c:\jdk1.2\ju\ch38\server and putting c:\jdk1.2 in your CLASSPATH. If you run both the client and server on the same computer, the directory structure and files should already be in position.
NOTE: In the next chapter, I'll show you how to use applets and a Web server to automatically distribute client files.
Now you must start your remote registry server. This program listens on the default port 1099 for incoming requests to access named objects. The named objects must register themselves with the remote registry program in order to be made available to requesters. You start up the remote registry server as follows:
start rmiregistry
Under Windows 95, this command creates a new DOS window and runs the remote registry program as a background task.
You're almost done with the remote server. The last thing to do is to execute the MyServerImpl program to create an object of the MyServerImpl class that registers itself with the remote registry. You do this as follows:
java ju.ch38.server.MyServerImpl I'm registered!
The program displays the I'm registered! string to let you know that it has successfully registered itself. Leave the server running (don't exit the server program by pressing Ctrl+C) while you start the client. If you run the client and server on the same computer, you'll need to open up a separate command line window for the client.
Now that you have the remote server up and running, let's create a client program to remotely invoke the methods of the MyServer object and display the results it returns.
Listing 38.3 contains the MyClient program. You must change the hostName variable to the name of the remote server host where the remote object is registered. Compile this program and copy it to a ju\ch38\client directory that is accessible from the CLASSPATH of the client host. Once you have compiled it, you can run it as follows:
java ju.ch38.client.MyClient Remote Method Invocation Is Great!
The MyClient program remotely invokes the methods of the server object and displays the data returned to the console window.
MyClient consists of a single main() method that invokes the lookup() method of the Naming class to retrieve a reference to the object named MyServer on the specified host. It casts this object to the MyServer interface. It then invokes the getDataNum() method of the remote object to retrieve the number of available data items, and the getData() method to retrieve each specific data item. The retrieved data items are displayed in the console window.
You can shut down the client and server by terminating the programs and closing their command line windows.
package ju.ch38.client; import ju.ch38.server.*; import java.rmi.*; public class MyClient { static String hostName="athome.jaworski.com"; public static void main(String args[]) { try { MyServer server = (MyServer) Naming.lookup("//"+hostName+"/MyServer"); int n = server.getDataNum(); for(int i=0;i<n;++i) { System.out.println(server.getData(i)); } } catch (Exception ex) { System.out.println(ex); } } }
This chapter introduced you to distributed applications. You learned why the RMI API provides a superior development approach for distributed programming in Java. You learned about the packages of the RMI API and then used them to develop a simple distributed application. In the next chapter, you'll discover more details about implementing distributed applications us
© Copyright, Macmillan Computer Publishing. All rights reserved.