Полезная информация

Exploring Java

Previous: 11.3 Simple Serialized Object ProtocolsChapter 11
Network Programming with Sockets and RMI
Next: 12. Working with URLs
 

11.4 Remote Method Invocation

The most fundamental means of interobject communication in Java is method invocation. Mechanisms like the Java event model are built on simple method invocations between objects that share a virtual machine. Therefore, when we want to communicate between virtual machines on different hosts, it's natural to want a mechanism with similar capabilities and semantics. Java's Remote Method Invocation mechanism does just that. It lets us get a reference to an object on a remote host and use it as if it were in our own virtual machine. RMI lets us invoke methods on remote objects, passing real objects as arguments and getting real objects as returned values.

Remote invocation is nothing new. For many years C programmers have used remote procedure calls (RPC) to execute a C function on a remote host and return the results. The primary difference between RPC and RMI is that RPC, being an offshoot of the C language, is primarily concerned with data structures. It's relatively easy to pack up data and ship it around, but for Java, that's not enough. In Java we don't work with simple data structures; we work with objects, which contain both data and methods for working on the data. Not only do we have to be able to ship the state of an object over the wire (the data), but the recipient has to be able to interact with the object after receiving it.

It should be no surprise that RMI uses object serialization, which allows us to send graphs of objects (objects and all of the connected objects that they reference). When necessary, RMI uses dynamic class loading and the security manager to transport Java classes safely. The real breakthrough of RMI is that it's possible to ship both code and data around the Net.

11.4.1 Remote and Non-Remote Objects

Before an object can be used with RMI, it must be serializable. But that's not sufficient. Remote objects in RMI are real distributed objects. As their name suggests, a remote object can refer to an object on a different machine; it can also refer to an object on the local host. The term remote means that the object is used through a special kind of object reference that can be passed over the network. Like normal Java objects, remote objects are passed by reference. Regardless of where the reference is used, the method invocation occurs at the original object, which still lives on its original host. If a server returns a reference to a remote object to you, you can call the object's methods; the actual method invocations will happen on the remote object's server. If a client creates a remote object and passes a reference to a server, the server can use the reference to invoke methods on the original object on the client side.

Non-remote objects are simpler. They are just normal serializable objects. The catch is that when you pass a non-remote object over the network it is simply copied. So references to the object on one host are not the same as those on the remote host. This is acceptable for many simple kinds of objects, especially objects that cannot be modified.

Stubs and skeletons

No, we're not talking about a gruesome horror movie. Stubs and skeletons are used in the implementation of remote objects. When you invoke a method on a remote object (which could be on a different host), you are actually calling some local code that serves as a proxy for that object. This is the stub. (It is called a stub because it is something like a truncated placeholder for the object.) The skeleton is another proxy that lives with the real object on its original host. It receives remote method invocations from the stub and passes them to the object.

You never have to work with stubs or skeletons directly; they are hidden from you (in the closet). Stubs and skeletons for your remote objects are created by running the rmic (RMI compiler) utility. After compiling your Java source files normally, you run rmic.

Remote interfaces

So far we've been referring to remote objects as objects (and they are, of course). But to be more specific, remote objects are objects that implement a special remote interface that specifies which of the object's methods can be invoked remotely. The remote interface must extend the java.rmi.Remote interface. Your remote object must implement its remote interface; so does the stub object that is automatically generated for you. In the rest of your code, you should refer to the remote object using its interface--not the object's actual class. Because both the real object and stub implement the remote interface, they are equivalent as far as we are concerned; we never have to worry about whether we have a reference to a stub or an actual implementation of the object locally. This "type equivalence" means that we can use normal language features like casting with remote objects.

All methods in the remote interface must declare that they can throw the exception java.rmi.RemoteException. This exception (actually, one of many subclasses to RemoteException) is thrown when any kind of networking error happens: for example, the server could crash, the network could fail, or you could be requesting an object that for some reason isn't available.

Here's a simple example of the remote interface that defines the behavior of MyRemoteObject; we'll give it two methods that can be invoked remotely, both of which return some kind of Widget object:

public interface MyRemoteObject 
        extends java.rmi.Remote {
    public Widget doSomething() throws java.rmi.RemoteException;
    public Widget doSomethingElse() throws java.rmi.RemoteException;
}

UnicastRemoteObject

The actual implementation of a remote object (not the interface we discussed previously) must extend java.rmi.server.UnicastRemoteObject. This is the RMI equivalent to the familiar Object class. It provides implementations of equals(), hashcode(), and toString() that make sense for remote objects. It also "exports" the object by preparing the Java run-time system to accept network connections for this object. It's possible to do this work yourself, but it isn't necessary.

Here's a remote object class that matches the MyRemoteObject interface; we haven't supplied implementation for the two methods or the constructor:

public class RemoteObjectImpl 
        implements MyRemoteObject
        extends java.rmi.UnicastRemoteObject {
    public RemoteObjectImpl() throws java.rmi.RemoteException {...}
    public Widget doSomething() throws java.rmi.RemoteException {...}
    public Widget doSomethingElse() throws java.rmi.RemoteException {...}
    // other non-public methods
    ...
}

This class can have as many additional methods as it needs; presumably, most of them will be private, but that isn't strictly necessary. We have to supply a constructor explicitly, even if the constructor does nothing, because the constructor (like any method) can throw a RemoteException; we therefore can't use the default constructor.

The name UnicastRemoteObject begs the question, "what other kinds of remote objects are there?" Right now, none. It's possible that JavaSoft will develop remote objects using other protocols or multicast techniques in the future.

The RMI registry

The registry is the RMI phone book. You use the registry to look up a reference to a registered remote object on another host. We've already described how remote references can be passed back and forth by remote method calls. But the registry is needed to bootstrap the process; the client needs some way of looking up some initial object.

The registry is implemented by a class called Naming and an application called rmiregistry, which must be running before you start a Java program that uses the registry. To use the registry, create an instance of a remote object and have it bind itself to a particular name in the registry. (Remote objects that bind themselves to the registry usually provide a main() method for this purpose.) The name can be anything you choose; it takes the form of a slash (/) separated path. When a client object wants to find your object, it constructs a special URL with the rmi: protocol, the hostname, and the object name. On the client, the RMI Naming class then talks to the registry and returns the remote object reference.

Which objects need to register themselves with the registry? Certainly, any object that the client has no other way of finding. A call to a remote method can return another remote object without using the registry. Likewise, a call to a remote method can have another remote object as its argument, without requiring the registry. You could design your system so that only one object registers itself, and then serves as a factory for any other remote objects you need. In other words, it wouldn't be hard to build a simple object request "bouncer" (I won't say "broker") that returns references to various objects. Why avoid using the registry? The current RMI registry is not very sophisticated, and lookups tend to be slow. It is not intended to be a general purpose directory service but simply to bootstrap RMI communications. It wouldn't be surprising if JavaSoft releases a much improved registry in the future, but that's not the one we have now.

11.4.2 The Example

The first thing we'll implement using RMI is a duplication of the simple serialized object protocol from the previous section. We'll make a remote RMI object called Server on which we can invoke methods to get a Date object or execute a WorkRequest object. First, we'll define our Remote interface:

import java.rmi.*;
import java.util.*;

public interface Server extends java.rmi.Remote {

    Date getDate() throws java.rmi.RemoteException;
    Object execute( WorkRequest work ) throws java.rmi.RemoteException;
}
The Server interface extends the java.rmi.Remote interface, which identifies objects that implement it as remote objects. We supply two methods that take the place of our old protocol: getDate() and execute().

Next, we'll implement this interface in a class called MyServer that holds the bodies of these methods. (In this example, we're not using the convention of adding Impl to the interface name to create the actual object name. Using this convention, the name of the server would be ServerImpl.)

public class MyServer 
    extends java.rmi.server.UnicastRemoteObject implements Server {   
 
    public MyServer() throws RemoteException { }

    // Implement the Server interface

    public Date getDate() throws RemoteException {
        return new Date();
    }
    public Object execute( WorkRequest work ) throws RemoteException {
        return work.execute();
    }

    public static void main(String args[]) {
        System.setSecurityManager(new RMISecurityManager());
        try {
            Server server = new MyServer();
            Naming.rebind("NiftyServer", server);
        } catch (java.io.IOException e) {
            // Problem registering server
        }
    }
}

MyServer extends java.rmi.UnicastRemoteObject, so when we create an instance of MyServer it will automatically be exported and start listening to the network. We start by providing a constructor that throws RemoteException. This exception accommodates errors that might occur in exporting an instance. We can't use the default constructor provided by the compiler, because the automatically generated constructor won't throw the exception. Next, MyServer implements the methods of the remote Server interface. These methods are straightforward.

The last method in this class is main(). This method lets the object set itself up as a server. main() starts by installing a special security manager, RMISecurityManager. This is a special security manager that watches any stub classes loaded over the network by RMI. It prevents someone from handing you a misbehaving stub, in addition to performing the other functions of a security manager. main() creates an instance of the MyServer object and then calls the static method Naming.rebind() to register the object with the registry. The arguments to rebind() are the name of the remote object in the registry (NiftyServer), which clients will use to look up the object, and reference to the server object itself. We could have called bind() instead, but rebind() is less prone to problems: if there's already a NiftyServer registered, rebind() replaces it.

We wouldn't need the main() method or this Naming business if we weren't expecting clients to use the registry to find the server. That is, we could omit main() and still use this object as a remote object. We would be limited to passing the object in method invocations or returning it from method invocations--but in many situations (not ours) those aren't big limitations.

Now we need our client:

public class MyClient {

    public static void main(String [] args) throws RemoteException {
        System.setSecurityManager(new RMISecurityManager());
        new MyClient( args[0] );
    }

    public MyClient(String host) {
        try {
            Server server = (Server)
                Naming.lookup("rmi://"+host+"/NiftyServer");
            System.out.println( server.getDate() );
            System.out.println( server.execute( new MyCalculation(2) ) );
        } catch (java.io.IOException e) { 
			// I/O Error or bad URL
        } catch (NotBoundException e) { 
			// NiftyServer isn't registered
        }
    }
}

When we run MyClient, we pass it the hostname of the server on which the registry is running. The main() method installs the RMISecurityManager and then creates an instance of the MyClient object, passing the hostname from the command line as an argument to the constructor.

The constructor for MyClient uses the hostname to construct a URL for the object. The URL will look something like this: rmi://hostname/NiftyServer, where NiftyServer is the name under which we registered our Server. We pass the URL to the static Naming.lookup() method. If all goes well, we get back a reference to a Server! Of course, the registry has no idea what kind of object it will return; lookup() therefore returns an Object, which we cast to Server.

Compile all of the code. Then run RMI compiler to make the stub and skeleton files for MyServer:

% rmic MyServer
Let's run the code. For the first pass, we'll assume that you have all of the class files, including the stubs and skeletons generated by rmic, available in the class path on both the client and server machines. (You can run this example on a single host to test it if you want.) Make sure your class path is correct and then start the registry; then start the server:
% rmiregistry &
% java MyServer

On a Windows system, run rmiregistry in another window by preceding it with the start command. Finally, on the client machine, run MyClient, passing the hostname of the server:

% java MyClient myhost
The client should print the date and the number four, which the server graciously calculated.

Dynamic class loading

Before running the example, we told you to distribute all the class files to both the client and server machines. However, RMI was designed to ship classes, in addition to data, around the network; you shouldn't have to distribute all the classes in advance. Let's go a step further, and have RMI load classes for us, as needed.

First, we need to tell RMI where to find any other classes it needs. We can use the system property java.rmi.server.codebase to specify a URL on an HTTP server when we run our client or server. This URL specifies the base directory in which RMI will begin its search for classes. When RMI sends a serialized object (i.e., an object's data) to some client, it also sends this URL. If the recipient needs the class file in addition to the data, it fetches the file via HTTP. To be more precise: if the object needed is a remote object, the recipient fetches the desired class's stub, which was created by rmic. Remember that stubs are stand-ins for the objects themselves; their job is to talk to the object, which remains on the server. If the object needed doesn't implement the Remote interface, the recipient fetches the object's class file itself, and uses the object locally. Therefore, we don't have to distribute class files; we can let clients download them as necessary. In Figure 11.3, we see MyClient going to the registry to get a reference to the Server object. Then MyClient dynamically downloads the stub class for MyServer from the HTTP daemon running on the server host.

Figure 11.3: RMI clients load classes dynamically

Figure 11.3

We can now split our class files between the server and client machines. For example, we could withhold the MyCalculation class from the server, since it really belongs to the client. Instead, we can make the MyCalculation class available via an HTTP daemon on some machine (probably our client's) and specify the URL when we run MyClient:

% java -Djava.rmi.server.codebase='http://myserver/foo/' MyClient
In this case we would expect that MyCalculation would be accessible at the URL http://myserver/foo/MyCalculation.class.

Passing remote object references

So far, we haven't done anything that we couldn't have done with the simple object protocol. We only used one remote object, MyServer, and we got its reference from the RMI registry. Now we'll extend our example to pass some remote references between the client and server. We'll add two methods to our remote Server interface:

public interface Server extends java.rmi.Remote {
	...

    StringEnumeration getList() throws java.rmi.RemoteException;

    void asyncExecute( WorkRequest work, WorkListener listener ) 
		throws java.rmi.RemoteException;
}
getList() retrieves a new kind of object from the server: a StringEnumeration. The StringEnumeration is a simple list of strings, with some methods for accessing the strings in order. We will make it a remote object so that implementations of StringEnumeration can stay on the server.

Next we'll spice up our work request feature by adding an asyncExecute() method. asyncExecute() lets us hand off a WorkRequest object as before, but it does the calulation on its own time. The return type for asyncExecute() is void, because it doesn't actually return a value; we get the result later. With the request, our client passes a reference to a WorkListener object that is to be notified when the WorkRequest is done. We'll have our client implement WorkListener itself.

Because this is to be a remote object, our interface must extend Remote, and its methods must throw RemoteExceptions:

public interface StringEnumeration extends Remote {
    public boolean hasMoreItems() throws RemoteException;
    public String nextItem() throws RemoteException;
}
Next, we provide a simple implementation of StringEnumeration, called StringEnumerator:
public class StringEnumerator 
    extends java.rmi.server.UnicastRemoteObject implements StringEnumeration {

    String [] list;
    int index = 0;
 
    public StringEnumerator( String [] list ) throws RemoteException { 
        this.list = list;
    }
    public boolean hasMoreItems() throws RemoteException {
        return index < list.length;
    }
    public String nextItem() throws RemoteException {
        return list[index++];
    }
}
The StringEnumerator extends UnicastRemoteObject. Its methods are simple: it can give you the next string in the list, and it can tell you whether there are any strings that you haven't seen yet.

Next, we'll define the WorkListener remote interface. This is the interface that defines how an object should listen for a completed WorkRequest. It has one method, workCompleted(), which the server that is executing a WorkRequest calls when the job is done:

public interface WorkListener extends Remote {
    public void workCompleted( WorkRequest request, Object result ) 
		throws RemoteException;
}
Next, let's add the new features to MyServer. We need to add implementations of the getList() and asyncExecute() methods, which we just added to the Server interface:
public class MyServer 
    extends java.rmi.server.UnicastRemoteObject implements Server {   
	... 

    public StringEnumeration getList() throws RemoteException {
        return new StringEnumerator( 
            new String [] { "Foo", "Bar", "Gee" } );
    }
    public void asyncExecute( WorkRequest request , WorkListener listener ) 
        throws java.rmi.RemoteException {

        Object result = request.execute();
        listener.workCompleted( request, result );
    }
}
getList() just returns a StringEnumerator with some stuff in it. asyncExecute() calls a WorkRequest's execute() method and notifies the listener when it's done. (Our implementation of asyncExecute() is a little cheesy. If we were forming a more complex calculation we would want to start a thread to do the calculation, and return immediately from asyncExecute(), so the client won't block. The thread would call workCompleted() at a later time, when the computation was done. In this simple example, it would take longer to start the thread than to perform the calculation.)

We have to modify MyClient to implement the remote WorkListener interface. This turns MyClient into a remote object, so we must make it a UnicastRemoteObject. We also add the workCompleted() method that the WorkListener interface requires:

public class MyClient extends java.rmi.server.UnicastRemoteObject 
	implements WorkListener {

    ...
    public void workCompleted( WorkRequest request, Object result ) 
        throws RemoteException {
        System.out.println("Async work result = " + result);
    }
}

Finally, we want MyClient to exercise the new features. Add these lines after the calls to getDate() and execute():

    // MyClient constructor
    ...
    StringEnumeration se = server.getList();
    while ( se.hasMoreItems() )
        System.out.println( se.nextItem() );

    server.asyncExecute( new MyCalculation(100), this );

We use getList() to get the enumeration from the server, then loop, printing the strings. We also call asyncExecute() to perform another calculation; this time, we square the number 100. The second argument to asyncExecute() is the WorkListener to notify when the data is ready; we pass a reference to ourself (this).

Now all we have to do is compile everything and run rmic to make the stubs for all our remote objects:

rmic MyClient MyServer StringEnumerator
Restart the RMI registry and MyServer on your server, and run the client somewhere. You should get the following:
Fri Jul 11 23:57:19 PDT 1997
4
Foo
Bar
Gee
Async work result = 10000

11.4.3 Alternatives to RMI

Java supports one important alternative to RMI, called CORBA (Common Object Request Broker Architecture). We won't say much about CORBA, but you should know it exists. CORBA is a standard developed by the Object Management Group (OMG), of which Sun Microsystems is one of the founding members. Its major advantage is that it works cross language: a Java program can use CORBA to talk to objects written in other languages, like C or C++. This is a considerable advantage if you want to build a Java front end for an older program that you can't afford to reimplement. CORBA also provides some other services that aren't yet available in Java. CORBA's major disadvantage is that it's complex. JavaSoft has announced that they will be making efforts to integrate RMI and CORBA, but it's too early to see where these efforts will lead.


Previous: 11.3 Simple Serialized Object ProtocolsExploring JavaNext: 12. Working with URLs
11.3 Simple Serialized Object ProtocolsBook Index12. Working with URLs

Other Books in this LibraryJava in a NutshellJava Language ReferenceJava AWTJava Fundamental ClassesExploring Java