Search the Catalog
Java Distributed Computing

Java Distributed Computing

By Jim Farley
1st Edition January 1998
1-56592-206-9, Order Number: 2069
384 pages, $34.95

Chapter 3.
Distributing Objects

In this chapter:
Why Distribute Objects?
What's So Tough About Distributing Objects?
Features of Distributed Object Systems
Distributed Object Schemes for Java
CORBA
Java RMI
RMI vs. CORBA

Distributed objects are a potentially powerful tool that has only become broadly available for developers at large in the past few years. The power of distributing objects is not in the fact that a bunch of objects are scattered across the network. The power lies in that any agent in your system can directly interact with an object that "lives" on a remote host. Distributed objects, if they're done right, really give you a tool for opening up your distributed system's resources across the board. And with a good distributed object scheme you can do this as precisely or as broadly as you'd like.

The first three sections of this chapter go over the motivations for distributing objects, what makes distributed object systems so useful, and what makes up a "good" distributed object system. Readers who are already familiar with basic distributed object issues can skip these sections and go on to the following sections, where we discuss two major distributed object protocols that are available in the Java environment: CORBA and RMI.

Although this chapter will cover the use of both RMI and CORBA for distributing objects, the rest of the book primarily uses examples that are based on RMI, where distributed objects are needed. We chose to do this because RMI is a simpler API and lets us write relatively simple examples that still demonstrate useful concepts, without getting bogged down in CORBA API specifics. Some of the examples, if converted to be used in production environments, might be better off implemented in CORBA.

Why Distribute Objects?

In Chapter 1, we discussed some of the optimal data/function partitioning capabilities that you'd like to have available when developing distributed applications. These included being able to distribute data/function "modules" freely and transparently, and have these modules be defined based on application structure rather than network distribution influences. Distributed object systems try to address these issues by letting developers take their programming objects and have them "run" on a remote host rather than the local host. The goal of most distributed object systems is to let any object reside anywhere on the network, and allow an application to interact with these objects exactly the same way as they do with a local object. Additional features found in some distributed object schemes are the ability to construct an object on one host and transmit it to another host, and the ability for an agent on one host to create a new object on another host.

The value of distributed objects is more obvious in larger, more complicated applications than in smaller, simpler ones. That's because much of the trade-off between distributed objects and other techniques, like message passing, is between simplicity and robustness. In a smaller application with just a few object types and critical operations, it's not difficult to put together a catalog of simple messages that would let remote agents perform all of their critical operation through on-line transactions. With a larger application, this catalog of messages gets complicated and difficult to maintain. It's also more difficult to extend a large message-passing system if new objects and operations are added. So being able to distribute the objects in our system directly saves us a lot of design overhead, and makes a large distributed system easier to maintain in the long run.

What's So Tough About Distributing Objects?

OK, so we think distributing objects is a good idea, but why do distributed object systems like CORBA and, to a lesser degree, Java RMI, seem so big and complicated? In Chapter 2 we saw how the core Java API, especially the java.net and java.io packages, gives us easy access to the network and key network protocols. They also let us layer application-specific operations on top of the network pretty easily. It seems like all that we'd need to do is extend these packages to allow objects to invoke each other's methods over the network, and we'd have a basic distributed object system. To get a feeling for the complexity of distributed object systems, let's look at what it would take to put together one of our own using just the core Java API, without utilizing the RMI package or the object input/output streams in the java.io package.

Creating Remote Objects

The essential requirements in a distributed object system are the ability to create or invoke objects on a remote host or process, and interact with them as if they were objects within our own process. It seems logical that we would need some kind of message protocol for sending requests to remote agents to create new objects, to invoke methods on these objects, and to delete the objects when we're done with them. As we saw in Chapter 2, the networking support in the Java API makes it very easy to implement a message protocol. But what kinds of things does a message protocol have to do if it's supporting a distributed object system?

To create a remote object, we need to reference a class, provide constructor arguments for the class, and receive a reference to the created object in return. This object reference will be used to invoke methods on the object, and eventually to ask the remote agent to destroy the object when we are done with it. So the data we will need to send over the network include class references, object references, method references, and method arguments.

The first item is easy--we already saw in Chapter 2 how the ClassLoader can be used to send class definitions over the network. If we want to create a new remote object from a given class, we can send the class definition to the remote host, and tell it to build one using a default constructor. Object references require some thought, though. These are not the same as local Java object references. We need to have an object reference that we can package up and send over the network, i.e., one that's serializable. This object reference, once we receive it, will still need to refer back to the original object on the remote host, so that when we call methods on it the method invocations are deferred to the "source" object on the remote host. One simple way to implement remote object references is to build an object lookup table into the remote agent. When a client requests a new object, the remote agent builds the requested object, puts the object into the table, and sends the table index of the object to the client. If we use sockets and streams to send requests and object references back and forth between remote agents, a client might request a remote object with something like this:

Class myClass = Class.forName("Myclass");
Socket objConn = new Socket("object.server.net", 1234);
OutputStreamWriter out =
    new ObjectStreamWriter(objConn.getOutputStream());
DataInputStream in = new DataInputStream(objConn.getInputStream());
 
out.write("new " + myClass.getName());
int objRef = in.readInt();

The integer objRef returned by the remote server can be used to reference the new remote object. On the other end of the socket, the agent receiving the request for the remote object may handle the request like this:

Hashtable objTable = new Hashtable();
ServerSocket server = ...;
Socket conn;
// Accept the connection from the client
if ((conn = server.accept()) != null) {
    DataOutputStream out =
        new DataObjectStream(conn.getOutputStream());
    BufferedReader in = new BufferedReader(
        new InputStreamReader(conn.getInputStream()));
    String cmd = in.readLine();
    // Parse the command type from the command string
    if (parseCmd(cmd).compareTo("new") == 0) {
        // The client wants a new object created,
        // so parse the class name from the command string
        String classname = parseClass(cmd);
        // Create the Class object and make an instance
        Class reqClass = Class.forName(classname);
        Object obj = reqClass.newInstance();
        // Register the object and return the integer
        // identifier/reference to the client
        Integer objID = nextID();
        objTable.put(objID, obj);
        out.writeInt(objID.intValue());
    }
}

The object server reads the class name sent by the requestor, looks up the class using the static Class.forName() method, and creates a new instance of the class by calling the newInstance() method on the Class object. Once the object has been created, the server generates a unique identifier for the object and sends it back to the requestor. Note that we've already limited our remote object scheme, by forcing the use of default constructors, e.g., those with no arguments. The remote host creates the requested object by calling newInstance() on its class, which is equivalent to creating the object by calling the class constructor with no arguments. Since we don't (yet) have a way to specify methods on classes over the network, or a way to send arguments to these methods, we have to live with this limitation for now.

Remote Method Calls

Now that the requestor has a reference to an object on the remote host, it needs a way to invoke methods on the object. Since Java, as of JDK 1.1, allows us to query a class or object for its declared methods and data members, the local agent can get a direct reference to the method that it wants to invoke on the remote object, in the form of a Method object:

Class reqClass = Class.forName("Myclass");
Method reqMethod = reqClass.getDeclaredMethod("getName", null);

In this example, the local agent has retrieved a reference to a getName() method, with no arguments, on the class Myclass. It could now use this method reference to call the method on a local Myclass instance:

Myclass obj = new Myclass();
reqMethod.invoke(obj, null);

This may seem like a roundabout way to accomplish the same thing as calling obj.getName() on the Myclass object, and it is. But in order to call a method on our remote object, we need to send a reference to the method over the network to the remote host. One way to do this is to assign identifiers to all of the methods on the class, just like we did for remote objects. Since both the object requestor and the object server can get a list of the class's methods by calling the getDeclaredMethods() method on the class, we could simply use the index of the method in the returned list as its identifier. Then the object requestor can call a method on a remote object by simply sending the remote host the object's identifier, and the identifier for the method to call. Assuming that our local agent has the same object reference from the earlier example, the remote method call would look something like this:

Method reqMethod = reqClass.getDeclaredMethod("getName", null);
Method[] methodList = reqClass.getDeclaredMethods();
int methodIdx = 0;
for (int i = 0; i < methodList.length; i++) {
    if (reqMethod == methodList[i]) {
        methodIdx = i;
        break;
    }
}
String cmd = "call " + methodIdx + " on " + objRef;
out.writeUTF(cmd);

This approach to handling remote method invocation is a general one; it will work for any class that we want to distribute. So far so good. But what about the arguments to the remote methods? And what about the return value from the remote method? Our example used a getName() method with no arguments, but if the method does take arguments, we'll need to send these to the remote host as well. We can also assume that a method called "getName" will probably return some kind of String value, which we'll need to get back from the remote host. This same problem exists in the creation of the remote object. With our method reference scheme we can now specify which constructor to use when the remote host creates our object, but we still need a way to send the constructor arguments to the remote host.

By now this exercise is beginning to look a lot more serious than we might have expected. In distributed object systems, the task of packaging up method arguments for delivery to the remote object, and the task of gathering up method return values for the client, are referred to as data marshaling. One approach we can take to data marshaling is to turn every object argument in a remote method call into a remote object just like we did previously, by generating an object reference and sending that to the remote agent as the method argument. If the method returns an object value, then the remote host can generate a new object reference and send that back to the local host. So now the remote host and the local host are acting as both object servers and object requestors. We started out with the remote host creating objects for the local host to invoke methods on, but now the local host is "serving" objects for method arguments, and the remote host is serving a bunch of new objects for method return values. And if the remote host needs to call any methods on objects that are arguments to other methods, or if the local host needs to call methods on object return values, then we'll need to send method references back and forth for these remote method calls as well.

To further complicate matters, we also have to worry about situations where you don't want a remote object reference sent as the method argument. In some cases, you may want to send objects by copy rather than by reference. In other words, you may just need to send the value of the object from one host to another, and not want changes to an object propagated back to the original source object. How do we serialize and transmit an object's value to a remote agent? One way is to tell the other agent to create a new object of the right type, as we did to create our original remote object, and then indicate the new object as the method argument or return value.

Other Issues

Our hypothetical remote object scheme, using object tables, integer object references based on table location, and integer method references based on the method's index/position in the class definition, is a bit ad-hoc and not very elegant. It will work, but probably not very well. For one thing, it is not very scalable in terms of development complexity and runtime complexity. Each agent on the network is maintaining its own object table and its own set of object identifiers. Each remote method call could potentially generate more entries in the object tables on both ends of the call, for method arguments and for method return values. And since there's no guarantee that two agents won't use the same identifier for two different objects, each agent using remote objects will need to keep its own table of remote object identifiers and the agent they came from. So now each agent has to maintain two object reference tables: one for objects that it is serving to other agents, and another for objects that it is using remotely. A more elegant way to handle this would be to create a naming service for objects, where an agent serving an object could register its objects with the naming service and generate a unique name/address for the object. The naming service would be responsible for mapping named objects to where they actually live. Users of the object could then find the object with one name, rather than a combination of an object ID and the object's host.

Another issue with this remote object scheme is the distribution of workload across the distributed system. In returning an object by value as the result of a method call, for example, the object server instructs the client to create the returned object value. The creation of this object could be a significant effort, depending on the type of object. Under normal, non-distributed conditions the creation of the return value is considered a part of the overhead of calling the method. You would hope that when you invoke this method remotely, all of the overhead, including the creation of the return value, would be off-loaded to the remote host. Instead, we're pushing some of the work to the remote host and keeping some locally. The same issue comes up when an agent invokes a remote method and passes method arguments by value instead of by reference. The calling agent tells the serving agent to create the method argument values on its side, which increases the net overhead on the server side for the remote method call.

Hopefully this extended thought experiment has highlighted some of the serious issues that arise when trying to distribute objects over the network. In the next section, we'll look at the features that a distributed object system needs to have in order to address these issues.

Features of Distributed Object Systems

From our exercise in the previous section, we uncovered some of the features that distributed object systems need. These features, plus some others, are illustrated in Figure 3-1. An object interface specification is used to generate a server implementation of a class of objects, an interface between the object implementation and the object manager, sometimes called an object skeleton, and a client interface for the class of objects, sometimes called an object stub. The skeleton will be used by the server to create new instances of the class of objects and to route remote method calls to the object implementation. The stub will be used by the client to route transactions (method invocations, mostly) to the object on the server. On the server side, the class implementation is passed through a registration service, which registers the new class with a naming service and an object manager, and then stores the class in the server's storage for object skeletons.

Figure 3-1. General architecture for distributed object systems

 

With an object fully registered with a server, the client can now request an instance of the class through the naming service. The runtime transactions involved in requesting and using a remote object are shown in Figure 3-2. The naming service routes the client's request to the server's object manager, which creates and initializes the new object using the stored object skeleton. The new object is stored in the server's object storage area, and an object handle is issued back to the client in the form of an object stub interface. This stub is used by the client to interact with the remote object.

Figure 3-2. Remote object transactions at runtime

 

While Figure 3-2 illustrates a client-server remote object environment, a remote object scheme can typically be used in a peer-to-peer manner as well. Any agent in the system can act as both a server and a client of remote objects, with each maintaining its own object manager, object skeleton storage, and object instance storage. In some systems the naming service can be shared between distributed agents, while in others each agent maintains its own naming service.

In the following sections we'll look at each element of this general distributed object architecture in more detail.

Object Interface Specification

To provide a truly open system for distributing objects, the distributed object system should allow the client to access objects regardless of their implementation details, like hardware platform and software language. It should also allow the object server to implement an object in whatever way it needs to. Although in this book we are talking about implementing systems in Java, you may have valuable services already implemented in C, C++, or Smalltalk, and these services might be expensive to reimplement in Java. In this situation you'd like the option of wrapping your existing services with object interfaces and using them directly via the remote object system.

Some distributed object systems provide a platform-independent means for specifying object interfaces. These object interface descriptions can be converted into server skeletons, which can be compiled and implemented in whatever form the server requires. The same object interfaces can be used to generate client-side stub interfaces. If we're dealing with a Java-based distributed system, then the server skeletons and client stubs will be generated as Java class definitions, which will then be compiled into bytecodes.

In CORBA, object interfaces are described using a platform-independent language called the Interface Definition Language (IDL). Other similar languages are the Interface Specification Language (ISL) in Xerox's Inter-Language Unification (ILU) system, and the Distributed Component Object Model (DCOM, comprised of COM and an extended version of DCE-RPC) used in Microsoft's DCOM system.

Object Manager

The object manager is really at the heart of the distributed object system, since it manages the object skeletons and object references on an object server. The object manager plays a role similar to that of an Object Request Broker (ORB) in a CORBA system, or the registry service in RMI, both of which will be discussed in more detail shortly. When a client asks for a new object, the object manager locates the skeleton for the class of object requested, creates a new object based on the skeleton, stores the new object in object storage, and sends a reference to the object back to the client. Remote method calls made by the client are routed through the manager to the proper object on the server, and the manager also routes the results back to the client. Finally, when the client is through with the remote object, it can issue a request to the object manager to destroy the object. The manager removes the object from the server's storage and frees up any resources the object is using.

Some distributed object systems support things like dynamic object activation and deactivation, and persistent objects. The object manager typically handles these functions for the object server. In order to support dynamic object activation and deactivation, the object manager needs to have an activation and deactivation method registered for each object implementation it manages. When a client requests activation of a new instance of an interface, for example, the object manager invokes the activation method for the implementation of the interface, which should generate a new instance. A reference to the new instance is returned to the client. A similar process is used for deactivating objects. If an object is set to be persistent, then the object manager needs a method for storing the object's state when it is deactivated, and for restoring it the next time a client asks for the object.

Depending on the architecture of the distributed object system, the object manager might be located on the host serving the objects, or its functions might be distributed between the client and the server, or it might reside completely on a third host, acting as a liaison between the object client and the object server.

Registration/Naming Service

The registration/naming service acts as an intermediary between the object client and the object manager. Once we have defined an interface to an object, an implementation of the interface needs to be registered with the service so that it can be addressed by clients. In order to create and use an object from a remote host, a client needs a naming service so that it can specify things like the type of object it needs, or the name of a particular object if one already exists on the server. The naming service routes these requests to the proper object server. Once the client has an object reference, the naming service might also be used to route method invocations to their targets.

If the object manager also supports dynamic object activation and persistent objects, then the naming service can also be used to support these functions. If a client asks the service to activate a new instance of a given interface, the naming service can route this request to an object server that has an implementation of that interface. And if an object manager has any persistent objects under its control, the naming service can be notified of this so that requests for the object can be routed correctly.

Object Communication Protocol

In order for the client to interact with the remote object, a general protocol for handling remote object requests is needed. This protocol needs to support, at a minimum, a means for transmitting and receiving object references, method references, and data in the form of objects or basic data types. Ideally we don't want the client application to need to know any details about this protocol. It should simply interact with local object interfaces, letting the object distribution scheme take care of communicating with the remote object behind the scenes. This minimizes the impact on the client application source code, and helps you to be flexible about how clients access remote services.

Development Tools

Of course, we'll need to develop, debug, and maintain the object interfaces, as well as the language-specific implementations of these interfaces, which make up our distributed object system. Object interface editors and project managers, language cross-compilers, and symbolic debuggers are essential tools. The fact that we are developing distributed systems imposes further requirements on these tools, since we need a reasonable method to monitor and diagnose object systems spread across the network. Load simulation and testing tools become very handy here as well, to verify that our server and the network can handle the typical request frequencies and types we expect to see at runtime.

Security

As we have already mentioned, any network interactions carry the potential need for security. In the case of distributed object systems, agents making requests of the object broker may need to be authenticated and authorized to access elements of the object repository, and restricted from other areas and objects. Transactions between agents and the remote objects they are invoking may need to be encrypted to prevent eavesdropping. Ideally, the object distribution scheme will include direct support for these operations. For example, the client may want to "tunnel" the object communication protocol through a secure protocol layer, with public key encryption on either end of the transmission.

Distributed Object Schemes for Java

While there are several distributed object schemes that can be used within the Java environment, we'll only cover two that qualify as serious options for developing your distributed applications: CORBA and RMI. Both of them have their advantages and their limitations, which we'll look at in detail in the following sections.

During this discussion, we'll be using an example involving a generic problem solver, which we'll distribute using both CORBA and RMI. We'll show in each case how instances of this class can be used remotely using these various object distribution schemes. A Java interface for the example class, called Solver, is shown in Example 3-1. The Solver acts as a generic compute engine that solves numerical problems. Problems are given to the Solver in the form of ProblemSet objects; the ProblemSet interface is shown in Example 3-2. The ProblemSet holds all of the information describing a problem to be solved by the Solver. The ProblemSet also contains fields for the solution to the problem it represents. In our highly simplified example, we're assuming that any problem is described by a single floating-point number, and the solution is also a single floating-point value.

Example 3-1: A Problem Solver Interface
package dcj.examples;
 
import java.io.OutputStream;
 
//
// Solver:
// An interface to a generic solver that operates on ProblemSets
//
 
public interface Solver
{
  // Solve the current problem set
  public boolean solve(); 
 
  // Solve the given problem set
  public boolean solve(ProblemSet s, int numIters); 
 
  // Get/set the current problem set
  public ProblemSet getProblem();
  public void setProblem(ProblemSet s);
 
  // Get/set the current iteration setting
  public int getInterations();
  public void setIterations(int numIter);
 
  // Print solution results to the output stream
  public void printResults(OutputStream os);
}
  

Example 3-2: A Problem Set Class

package dcj.examples;
 
public class ProblemSet
{
  protected double value = 0.0;
  protected double solution = 0.0;
 
  public double getValue() { return value; }
  public double getSolution() { return solution; }
  public void setValue(double v) { value = v; }
  public void setSolution(double s) { solution = s; }
}

Our Solver interface represents a pretty simple compute engine, but it has some features meant to highlight the attributes of a distributed object scheme. As we said before, the Solver accepts problems in the form of ProblemSet objects. It also has a single compute parameter, the number of iterations used in solving the problem. Most computational algorithms have parameters that can be used to alter the way a problem is solved: basic iterative methods usually have a maximum number of iterations to run, so we're using that as the only parameter on our simplified Solver.

A Solver has two solve() methods. One has no arguments and causes the Solver to solve the default problem using the default settings. The default problem for the Solver can be set using the setProblem() method, and the default iteration limit can be set using the setIterations() method. You can also get these values using the getProblem() and getIterations() methods on the interface. The other solve() method includes arguments that give the problem to be solved, and the iteration limit to use for solving the problem.

This Solver interface acts as a sort of litmus test for distributed object schemes. It includes methods that accept Object arguments (ProblemSets, specifically), it can maintain its own state (the default problem and default iteration limit), which needs to be kept consistent across method calls from multiple clients, and it includes a method that involves generic I/O. We'll see in the following examples how well both CORBA and RMI support remote implementation of this interface.

CORBA

CORBA, the Common Object Request Broker Adapter, is a distributed object standard developed by members of the Object Management Group (OMG) and their corporate members and sponsors. The first versions of the CORBA standard were developed long before Java was publicized by Sun (the OMG was formed in 1989, the CORBA 1.1 specification was released in 1991, and the first pre-release versions of Java and the HotJava browser were made public in the 1994-1995 timeframe). CORBA is meant to be a generic framework for building systems involving distributed objects. The framework is meant to be platform- and language-independent, in the sense that client stub interfaces to the objects, and the server implementations of these object interfaces, can be specified in any programming language. The stubs and skeletons for the objects must conform to the specifications of the CORBA standard in order for any CORBA client to access your CORBA objects.

The CORBA framework for distributing objects consists of the following elements:

An earlier version of the CORBA standard did not include a low-level binary specification for the inter-ORB network protocol. Instead, it described the protocol in terms of more generic features that a "compliant" system had to implement. This turned out to be a stumbling block, since vendors were implementing CORBA object servers that couldn't talk to each other, even though they all followed the "standard." The binary protocol for the IIOP was specified in the 2.0 release of the CORBA standard, which closed this hole in the standard.

The Object Request Broker (ORB)

The Object Request Broker is at the core of the CORBA model for distributed objects. It fills the Object Manager role that we described earlier in the generic description of distributed object systems. Both the client and the server of CORBA objects use an ORB to talk to each other, so instead of a single Object Manager (as described in our generic distributed object system), CORBA has an object manager on both the client and server side of a distributed object. This lets any agent in a CORBA system act as both a client and a server of remote objects.

On the client side of an object request, the ORB is responsible for accepting client requests for a remote object, finding the implementation of the object in the distributed system, accepting a client-side reference to the remote object, routing client method calls through the object reference to the remote object implementation, and accepting any results for the client. On the server side, the ORB lets object servers register new objects. When a client requests an object, the server ORB receives the request from the client ORB, and uses the object's skeleton interface to invoke the object's activation method. The server ORB generates an object reference for the new object, and sends this reference back to the client. The client ORB converts the reference into a language-specific form (a Java stub object, in our case), and the client uses this reference to invoke methods on the remote object. When the client invokes a method on a remote object, the server ORB receives the request and calls the method on the object implementation through its skeleton interface. Any return values are marshaled by the server ORB and sent back to the client ORB, where they are unmarshaled and delivered to the client program. So ORBs really provide the backbone of the CORBA distributed object system.

The Interface Definition Language (IDL)

Distributed objects in a CORBA application are described in the Interface Definition Language (IDL). The IDL provides a platform- and implementation-independent way to define what kinds of operations an object is capable of performing. Example 3-3 shows an IDL interface for a simplified bank account server. The IDL specification indicates that the BankServer object will have three methods: one to verify a PIN number against an account, one to get specifics about an account, and one to process a transaction against an account.

Example 3-3: A Basic IDL Interface
module Examples {
  interface BankServer {
    boolean verifyPIN(in long acctNo, in long pin);
    void getAcctSpecifics(in long acctNo, in string customerName,
                          out double balance, out boolean isChecking);
    boolean processTransaction(in Transaction t, in long acctNo);
  }
}

The IDL language shares a lot of the syntax of C++ in terms of defining interfaces and their methods. Since we're talking about distributed objects, however, IDL forces you to specify additional information about your object's interface, like which method arguments are input-only, output-only, or two-way data transfers. This is done using additional keywords on method arguments, before their type specifications. These keywords are in, out, and inout. In the BankServer interface, the two arguments to the verifyPIN() method are declared as in parameters, since they are only used as input to the method and don't need to be read back when the method returns. The getAcctSpecifics() method has two in parameters and two out parameters. The two out arguments are read back from the server when the method returns as output values. An inout argument is both fed to the method as an input parameter, and read back when the method returns as an output value. When the IDL interface is compiled into a client stub and a server skeleton, the input/output specifiers on method arguments are used to generate the code to marshal and unmarshal the method arguments correctly.

Server Implementations

Once an IDL interface for a distributed object has been written, it can be translated into a client stub and a server skeleton. IDL translators exist for C, C++, Smalltalk, Ada, Java, and other common languages. The stub and skeleton don't have to be compiled into the same programming language--this is a principle feature of the CORBA architecture. The client could be a Java applet, for example, and use a stub in the form of a Java class definition. The server could implement the same object interface in C++ using an object skeleton defined as a C++ class definition.

The first step in creating a server-side implementation of an object is to compile its IDL interface into both a native-language interface ( Java, in our case), and an implementation skeleton. The native interface is simply a mapping of the IDL specification into our implementation language. It acts as the basis for both the server skeleton and the client stub, which will also be specified in the implementation language. The server-side skeleton acts as the base class for implementations of the object interface, and includes CORBA-specific methods that the server ORB can use to map client requests to method calls on the object implementation. You provide an implementation of the object by deriving from the skeleton and writing the implementations for the methods on the object interface. Later we'll see an example of creating a server implementation when we distribute our Solver class using CORBA.

Once you've defined an implementation for the object, you need to register the object implementation with the server ORB and, optionally, with a CORBA Naming Service, so that clients can find the object on the network and get references to it. You create an ORB from within your server process by creating an instance of an ORB interface. In Java, this is done by creating an instance of the org.omg.CORBA.ORB object. The interface to the Naming Service might also be provided in the form of a Java class that you create by requesting a reference to a Naming Service object from the ORB object. Once you have access to the Naming Service interface, you can create an instance of your object implementation and register it. Clients can then connect to your object through their own ORBs and Naming Services, assuming that they know your host name and the name that you used to register the object.[1]

Client Stubs

The client uses a stub to access the data and methods on the remote instance of the object. A stub is generated using an IDL compiler, the same way that the server implementation skeleton was generated. Like the skeleton, the stub contains CORBA-specific methods that the client ORB can use to marshal method arguments to be sent to the server, and to unmarshal return values and output parameters. When a client requests a remote object reference, it's given the reference in the form of an instance of the stub interface.

A client can get a reference to a remote object by creating an ORB that is connected to the remote server hosting the object, and then asking the ORB to find the object on the remote server. The ORB initialization process will typically include arguments that let you specify, as a client, which remote host and port to talk to for remote object transactions. Note that the CORBA standard doesn't include host and port number parameters in the required initialization interface, but several vendors extend the initialization parameters to include these options. Once the ORB has been created, you can use the ORB's Naming Service to ask for a remote object by name. The name would have to match the name used by the server when it registered the object implementation. The client ORB makes a connection to the server ORB and asks for the named object. If it's found, the client ORB creates a reference to the object as an instance of the stub generated from the IDL interface. The client can then call methods on the stub interface, which are routed by the client ORB to the server ORB, where the method calls are executed on the actual server object implementation.

A CORBA Solver

Now let's see how we would both serve and use our Solver class in a CORBA environment. For our example we are going to use the JavaIDL package provided by Sun as our CORBA implementation. It provides an IDL-to-Java compiler, a basic ORB implementation, and a basic Naming Service implementation. Since a standard IDL-to-Java mapping has been submitted to the OMG by a group of the most prominent CORBA software vendors, almost all of the details about using CORBA in a Java environment will apply to any other CORBA implementation in Java.

The IDL interface

First, we need an IDL version of the class, which is shown in Example 3-4. This IDL interface represents some, but not all, of the functionality that I originally expressed in terms of the Java interface in Example 3-1. It also includes an IDL specification for the ProblemSet interface in Example 3-2.

Example 3-4: IDL Solver Interface
module DCJ {
  module examples {
 
    interface ProblemSet {
      double getValue();
      void setValue(in double v);
      double getSolution();
      void setSolution(in double s);
    };
 
    interface Solver {
      // Solve the current problem set
      boolean solveCurrent();
 
      // Solve the given problem set
      boolean solve(inout ProblemSet s, in long numIters);
 
      // Get/set current problem
      ProblemSet getProblem();
      void setProblem(inout ProblemSet p);
 
      // Get/set current iteration setting
      unsigned long getIterations();
      void setIterations(in unsigned long i);
    };
  };
};

You can see that there are some subtle differences between the IDL and Java interfaces for our classes. All the method arguments in the IDL interface are preceded by in, out, or inout, which indicates whether the argument is write-only, read- only, or read/write, respectively (from the perspective of the client). Since the purpose of IDL is strictly to define an interface to an object, there's no need to specify constructors for the object. Notice that we had to change the name of no-argument solve() method to be solveCurrent() in the IDL interface. IDL doesn't support overloading the names of methods, so we had to give one of our solve() methods a more descriptive name. The rest of the methods declared in the IDL interface directly correspond to methods on the original Java interface.

The client stubs

Now that we have our IDL interface, we can run it through our IDL-to-Java compiler (called, predicatably enough, idltojava) to generate a base Java interface, along with a Java stub for the client and a Java skeleton for the class implementation. Using JavaIDL,[2] the base interface and client stub are created by simply executing this command:

myhost% idltojava -fclient Solver.idl

Other CORBA implementations will have their own toolset and command-line arguments for compiling the IDL interfaces for your application. The Java base interface for the Solver generated by the IDL-to-Java compiler is shown in Example 3-5. Since we also included an interface for the ProblemSet object in our IDL file, the compiler also generated a base Java interface for it, shown in Example 3-6.

Example 3-5: CORBA-Generated Solver Base Interface
/*
 * File: ./DCJ/examples/Solver.java
 * From: Solver.idl
 *   By: idltojava JavaIDL Wed Mar 5 17:02:26 1997
 */
 
package DCJ.examples;
public interface Solver
    extends org.omg.CORBA.Object {
    boolean solveCurrent();
    boolean solve(DCJ.examples.ProblemSetHolder s, int numIters);
    DCJ.examples.ProblemSet getProblem();
    void setProblem(DCJ.examples.ProblemSetHolder p);
    int getIterations();
    void setIterations(int i);
}

Here are some important things to note about the Solver base class in Example 3-5:

The compiler also generated the client stubs for the interfaces in our IDL file. In each case, the generated client stub implements the Java base interface for the object. The client stubs also extend the org.omg.CORBA.portable.Object-Impl class, which provides the interface used by the client ORB to marshal and unmarshal remote method arguments, among other things. The start of the generated client stub for the Solver looks like this:

public class _SolverStub
    extends org.omg.CORBA.portable.ObjectImpl
    implements dcj.examples.Solver {
        ...

We'll leave out the remainder of the client stub definitions, since they primarily include low-level details about the interface between the stub and the ORB that we won't be concerned with here. When you're developing systems using CORBA, you should never have to be concerned with the internal details of the client stub or the server skeleton anyway. The IDL-to-Java compiler does the right thing with your IDL definitions, and all the client needs to know is the base Java interface for the remote object.

The server skeleton and implementation

The same IDL Solver interface can be used to generate a skeleton for a server implementation of a class. This is done by invoking the following:

myhost% idltojava -fserver Solver.idl

This will regenerate the Java base interface, but will also generate a skeleton for our object implementation. Like the client stub, our server skeleton contains mostly code related to the ORB/skeleton interface, details that you won't need to be concerned with most of the time. The only aspect we'll mention is that the server skeleton also implements the base Java interface, as well as the org.omg.CORBA.portable.ObjectImpl class. It also implements the interface org.omg.CORBA.portable.Skeleton, which the server ORB will be looking for when invoking methods on the object implementation:

public abstract class _SolverImplBase
    extends org.omg.CORBA.portable.ObjectImpl
    implements DCJ.examples.Solver, org.omg.CORBA.portable.Skeleton {

Note that the _SolverImplBase class is declared abstract by the compiler, since it doesn't implement any of the methods that we declared in our IDL interface. Again, since the ProblemSet interface was defined in the same IDL file, a Java skeleton for the ProblemSet was also generated.

The last step in setting up our remote object for business is to extend the _Sol-verImplBase class and the _ProblemSetImplBase class, and implement the methods defined in their base interfaces. Our implementation of the Solver interface is shown in Example 3-7. The CORBASolverImpl provides implementations of all of the methods from the base Solver interface in Example 3-5, including the ever-critical solve() method. In this case, our Solver simply performs a square-root operation on the problem value. Our implementation of the ProblemSet interface is shown in Example 3-8.

Example 3-7: Java Implementation of the Solver Interface
package DCJ.examples;
 
import java.lang.*;
import java.io.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
 
public class CORBASolverImpl extends _SolverImplBase {
 
  protected int numIterations = 1; // not used for this Solver...
  protected ProblemSetHolder currProblem = null;
 
  // Constructors
  public CORBASolverImpl() { super(); }
  public CORBASolverImpl(int numIter) {
    super();
    numIterations = numIter;
  }
 
 
  public ProblemSet getProblem() {
    return currProblem.value;
  }
 
  public void setProblem(ProblemSetHolder ph) {
    currProblem = ph;
  }
 
  public int getIterations() {
    return numIterations;
  }
 
  public void setIterations(int i) {
    numIterations = i; 
  }
 
  public boolean solveCurrent() {
    System.out.println("Solving current problem...");
    return solve(currProblem, numIterations);
  }
 
  public boolean solve(ProblemSetHolder sh, int numIters) {
    ProblemSet s = sh.value;
    boolean success = true;
 
    if (s == null) {
      System.out.println("No problem to solve.");
      return false;
    }
 
    System.out.println("Problem value = " + s.getValue());
 
    // Solve problem here...
    try {
      s.setSolution(Math.sqrt(s.getValue()));
    }
    catch (ArithmeticException e) {
      System.out.println("Badly-formed problem.");
      success = false;
    }
 
    System.out.println("Problem solution = " + s.getSolution());
 
    return success;
  }
 
  public static void main(String argv[]) {
    
    try {
      // create and initialize the ORB
      System.out.println("Initializing ORB...");
      ORB orb = ORB.init(argv, null);
  
      // Create a Solver and register it with the ORB
      System.out.println("Connecting solver to ORB...");
      CORBASolverImpl solver = new CORBASolverImpl();
      orb.connect(solver);
  
      // Get the naming service from the ORB
      System.out.println("Getting reference to Naming Service...");
      org.omg.CORBA.Object ncObj = 
        orb.resolve_initial_references("NameService");
      NamingContext ncRef = NamingContextHelper.narrow(ncObj);
  
      // Bind the Solver object to a name
      System.out.println("Registering Solver with Naming Service...");
      NameComponent comp = new NameComponent("Solver", "");
      NameComponent path[] = {comp};
      ncRef.rebind(path, solver);
  
      // Wait for client requests
      System.out.println("Waiting for clients...");
      java.lang.Object dummySync = new java.lang.Object();
      synchronized (dummySync) {
        dummySync.wait();
      }
    }
    catch (Exception e) {
      System.err.println(e);
      e.printStackTrace(System.out);
    }
  }
}

Example 3-8: Java Implementation of the ProblemSet Interface

package DCJ.examples;
 
public class ProblemSetImpl extends _ProblemSetImplBase {
  protected double value;
  protected double solution;
 
  public double getValue() { return value; }
  public void setValue(double v) { value = v; }
  public double getSolution() { return solution; }
  public void setSolution(double s) { solution = s; }
}

In addition to implementations for the Solver interface methods, our CORBASolverImpl class also includes a main() routine that creates a Solver instance and registers it with a local ORB. The registration routine first creates a local server ORB:

ORB orb = ORB.init(argv, null);

The command-line arguments to the main routine are passed into the ORB initialization routine so that it can parse any ORB-specific parameters that the user may provide. Next, the routine creates an instance of our Solver implementation, and connects the object to the ORB:

CORBASolverImpl solver = new CORBASolverImpl();
orb.connect(solver);

The next step is to get a reference to the ORB's naming service and register the object under a name:

org.omg.CORBA.Object ncObj = 
    orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(ncObj);
    ...
NameComponent comp = new NameComponent("Solver", "");
NameComponent path[] = {comp};
ncRef.rebind(path, solver);

The NameComponent that we create is the thing that tells the naming service what the name of the object is supposed to be on this ORB. Finally, we need to keep the server process alive while we wait for clients to invoke methods on our Solver. If the main() routine exits, then the surrounding process will exit and the ORB object we created will be destroyed. So to keep the main() routine from exiting, we enter an infinite wait:

java.lang.Object dummySync = new java.lang.Object();
synchronized (dummySync) {
    dummySync.wait();
}

The Solver client

OK, we've got our client stubs and server skeletons generated, we've written Java implementations for the interfaces, and our Solver implementation includes a main() routine that registers a Solver object with a server ORB. Now all we need is a client to use the Solver. Example 3-9 shows a simple client. All it does is create a client ORB, get a reference to the ORB's naming service, and ask it for a reference to the Solver by asking for it by name. The initial reference that we get from the ORB is a generic CORBA Object, which needs to be "narrowed" to get a reference to the actual Solver object reference using the SolverHelper.narrow() method. We had to do the same thing when getting a reference to the NamingContext from the ORB. The SolverHelper interface is generated automatically by the idltojava compiler from the Solver's IDL interface. Once the client has a Solver stub reference, it creates a problem and asks the Solver to solve it. If we're successful, the remote Solver object will receive our request, solve the problem, and return the results to the client.

Example 3-9: A Client for the Remote Solver
package DCJ.examples;
 
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
 
public class CORBASolverClient {
  public static void main(String argv[]) {
    try {
      // Create an ORB
      ORB orb = ORB.init(argv, null);
 
      // Get a reference to the Naming Service
      org.omg.CORBA.Object obj =
        orb.resolve_initial_references("NameService");
      NamingContext nc = NamingContextHelper.narrow(obj);
 
      // Get a reference to the Solver on the remote host
      NameComponent comp = new NameComponent("Solver", "");
      NameComponent path[] = {comp};
      org.omg.CORBA.Object sobj = nc.resolve(path);
      Solver solver = SolverHelper.narrow(sobj);
 
      // Make a problem and ask the solver to solve it
      ProblemSet s = new ProblemSetImpl();
      s.setValue(173.39);
      solver.solve(new ProblemSetHolder(s), 1);
 
      // Print out solution
      System.out.println("Problem = " + s.getValue());
      System.out.println("Solution = " + s.getSolution());
    }
    catch (Exception e) {
      System.out.println(e) ;
      e.printStackTrace(System.out);
    }
  }
}

Pulling it all together

At this point, we've got all the Java code for our CORBA Solver and the sample client. To see the system in practice, we have to compile all of the Java code using the javac compiler, and copy the bytecodes to both the server and client hosts. Both hosts will also need to have a Java CORBA implementation available in the form of its class files. On the object implementation server, we need to have a CORBA Naming Service running, which listens to a port for object requests. In the JavaIDL system, we start the Naming Service with a command like the following:

objhost% nameserv -ORBInitialPort 1050

This starts the Naming Service process listening to port 1050 on the host. Next, we need to run our server implementation process to register one of our Solver objects with an ORB on the server. We can run our server process with this command:

objhost% java DCJ.examples.CORBASolverImpl -ORBInitialPort 1050

The ORBInitialPort command-line argument is provided for initialization of the server ORB. When the arguments are passed into the ORB's initialization routine, the ORB start-up routine will parse out this argument, and will know that the ORB needs to work with the Naming Service running on port 1050.

Now all we need to do is run our client:

client% java DCJ.examples.CORBASolverClient -ORBInitialHost objhost \ 
    -ORBInitialPort 1050

The ORBInitialHost and ORBInitialPort arguments are passed into the client's ORB initialization call. The ORB will use these arguments to connect itself to the specified remote host and port for naming services. When the client asks the Naming Service for a reference to the object named "Solver," it gets a reference to the Solver object being served by the server process. The remote Solver solves our problem and returns the results, which our client prints out for us:

Problem = 173.39
Solution = 13.1678

Java RMI

The Java Remote Method Invocation (RMI) package is a Java-centric scheme for distributed objects that is now a part of the core Java API. RMI offers some of the critical elements of a distributed object system for Java, plus some other features that are made possible by the fact that RMI is a Java-only system. RMI has object communication facilities that are analogous to CORBA's IIOP, and its object serialization system provides a way for you to transfer or request an object instance by value from one remote process to another.

Remote Object Interfaces

Since RMI is a Java-only distributed object scheme, all object interfaces are written in Java. Client stubs and server skeletons are generated from this interface, but using a slightly different process than in CORBA. First, the interface for the remote object has to be written as extending the java.rmi.Remote interface. The Remote interface doesn't introduce any methods to the object's interface; it just serves to mark remote objects for the RMI system. Also, all methods in the interface must be declared as throwing the java.rmi.RemoteException. The RemoteException is the base class for many of the exceptions that RMI defines for remote operations, and the RMI engineers decided to expose the exception model in the interfaces of all RMI remote objects. This is one of the drawbacks of RMI: it requires you to alter an existing interface in order to apply it to a distributed environment.

Server Implementations

Once the remote object's Java interface is defined, a server implementation of the interface can be written. In addition to implementing the object's interface, the server also typically extends the java.rmi.server.UnicastRemoteObject class. UnicastRemoteObject is an extension of the RemoteServer class, which acts as a base class for server implementations of objects in RMI. Subclasses of RemoteServer can implement different kinds of object distribution schemes, like replicated objects, multicast objects, or point-to-point communications. The current version of RMI (1.1) only supports remote objects that use point-to-point communication, and UnicastRemoteObject is the only subclass of RemoteServer provided. RMI doesn't require your server classes to derive from a RemoteServer subclass, but doing so lets your server inherit specialized implementations of some methods from Object (hashCode(), equals(), and toString()) so that they do the right thing in a remote object scenario. If you decide that you don't want to subclass from a RemoteServer subclass for some reason, then you have to either provide your own special implementations for these methods or live with the fact that these methods may not behave consistently on your remote objects. For example, if you have two client stubs that refer to the same remote object, you would probably want their hashCode() methods to return the same value, but the standard Object implementation will return independent hash codes for the two stubs. The same inconsistency applies to the standard equals() and toString() methods.

The RMI Registry

In RMI, the registry serves the role of the Object Manager and Naming Service for the distributed object system. The registry runs in its own Java runtime environment on the host that's serving objects. Unlike CORBA, the RMI registry is only required to be running on the server of a remote object. Clients of the object use classes in the RMI package to communicate with the remote registry to look up objects on the server. You start an RMI registry on a host by running the rmiregistry command, which is included in the standard JDK distribution. By default, the registry listens to port 1099 on the local host for connections, but you can specify any port for the registry process by using a command-line option:

objhost% rmiregistry 4001

Once the registry is running on the server, you can register object implementations by name, using the java.rmi.Naming interface. We'll see the details of registering server implementations in the next section. A registered class on a host can then be located by a client by using the lookup() method on the Naming interface. You address remote objects using a URL-like scheme. For example,

MyObject obj1 =
    (MyObject)Naming.lookup("rmi://objhost.myorg.com/Object1");

will look up an object registered on the host objhost.myorg.com under the name Object1. You can have multiple registries running on a server host, and address them independently using their port assignments. For example, if you have two registries running on the objhost server, one on port 1099 and another on port 2099, you can locate objects in either registry using URLs that include the port numbers:

MyObject obj1 = 
    (MyObject)Naming.lookup("rmi://objhost.myorg.com:1099/Object1");
MyObject obj2 =
    (MyObject)Naming.lookup("rmi://objhost.myorg.com:2099/Object2");

Client Stubs and Server Skeletons

Once you've defined your object's interface and derived a server implementation for the object, you can create a client stub and server skeleton for your object. First the interface and the server implementation are compiled into bytecodes using the javac compiler, just like normal classes. Once we have the bytecodes for the interface and the server implementation, we have to generate the linkage from the client through the RMI registry to the object implementation we just generated. This is done using the RMI stub compiler, rmic. Suppose we've defined a remote interface called MyObject, and we've written a server implementation called MyObjectImpl, and compiled both of these into bytecodes. Assuming that we have the compiled classes in our CLASSPATH, we would generate the RMI stub and skeleton for the class with the rmic compiler:

myhost% rmic MyObject

The rmic compiler bootstraps off of the Java bytecodes for the object interface and implementation to generate a client stub and a server skeleton for the class. A client stub is returned to a client when a remote instance of the class is requested through the Naming interface. The stub has hooks into the object serialization subsystem in RMI for marshaling method parameters.

The server skeleton acts as an interface between the RMI registry and instances of the object implementation residing on a host. When a client request for a method invocation on an object is received, the skeleton is called on to extract the serialized parameters and pass them to the object implementation.

Registering and Using a Remote Object

Now we have a compiled interface and implementation for our remote object, and we've created the client stub and server skeleton using the rmic compiler. The final hurdle is to register an instance of our implementation on a remote server, and then look up the object on a client. Since RMI is a Java-centric API, we can rely on the bytecodes for the interface, the implementation, the rmic-generated stub, and skeleton being loaded automatically over the network into the Java runtimes at the clients. A server process has to register an instance of the implementation with a RMI registry running on the server:

MyObjectImpl obj = new MyObjectImpl();
Naming.rebind("Object1", obj);

Once this is done, a client can get a reference to the remote object by connecting to the remote registry and asking for the object by name:

System.setSecurityManager(new java.rmi.RMISecurityManager());
MyObject objStub = (MyObject)Naming.lookup("rmi://objhost/Object1");

Before loading the remote object stub, we installed a special RMI security manager with the System object. The RMI security manager enforces a security policy for remote stubs to prevent them from doing illicit snooping or sabotage when they're loaded into your local Java environment from a network source. If a client doesn't install an RMI security manager, then stub classes can only be loadable from the local file system.

Serializing Objects

Another Java facility that supports RMI is object serialization. The java.io package includes classes that can convert an object into a stream of bytes and reassemble the bytes back into an identical copy of the original object. Using these classes, an object in one process can be serialized and transmitted over a network connection to another process on a remote host. The object (or at least a copy of it) can then be reassembled on the remote host.

An object that you want to serialize has to implement the java.io.Serializable interface. With this done, an object can be written just as easily to a file, a string buffer, or a network socket. For example, assuming that Foo is a class that implements Serializable, the following code writes Foo on an object output stream, which sends it to the underlying I/O stream:

Foo myFoo = new Foo();
OutputStream out = ... // Create output stream to object destination
ObjectOutputStream oOut = new ObjectOutputStream(out);
oOut.writeObject(myFoo);

The object can be reconstructed just as easily:

InputStream in = ... // Create input stream from source of object
ObjectInputStream oIn = new ObjectInputStream(in);
Foo myFoo = (Foo)oIn.readObject();

We've simplified things a bit by ignoring any exceptions generated by these code snippets. Note that serializing objects and sending them over a network connection is very different from the functionality provided by the ClassLoader, which we saw earlier in this book. The ClassLoader loads class definitions into the Java runtime, so that new instances of the class can be created. The object serialization facility allows an actual object to be serialized in its entirety, transmitted to any destination, and then reconstructed as a precise replica of the original.

When you serialize an object, all of the objects that it references as data members will also be serialized, and all of their object references will be serialized, and so on. If you attempt to serialize an object that doesn't implement the Serializable interface, or an object that refers to non-serializable objects, then a NotSerializableException will be thrown. Method arguments that aren't objects are serialized automatically using their standard byte stream formats.

In RMI, the serialization facility is used to marshal and unmarshal method arguments that are objects, but that are not remote objects. Any object argument to a method on a remote object in RMI must implement the Serializable interface, since the argument will be serialized and transmitted to the remote host during the remote method invocation.

An RMI Solver

Now let's go back to our Solver example and distribute it using RMI. First, we would have to rewrite the Solver interface so that it implements the java.rmi.Remote interface. The methods on the interface also have to be specified as throwing RemoteExceptions. This modified version of the Solver interface, the RMISolver, is shown in Example 3-10.

Example 3-10: Interface for a RMI Solver
package dcj.examples.rmi;
 
import java.rmi.*;
import java.io.OutputStream;
 
public interface RMISolver extends java.rmi.Remote
{
  public boolean solve() throws RemoteException;
  public boolean solve(RMIProblemSet s,
                       int numIters) throws RemoteException;
 
  public RMIProblemSet getProblem() throws RemoteException;
  public boolean setProblem(RMIProblemSet s) throws RemoteException;
  public int getIterations() throws RemoteException;
  public boolean setIterations(int numIter) throws RemoteException;
}
  

There are two methods in this interface, the solve() method with arguments, and the setProblem() method, where we have problem set arguments that we want to pass into the remote method invocation. We could achieve this by creating a version of the ProblemSet class that implements the Serializable interface. If we did that, the problem set would be sent to the remote host by value--the remote object would be operating on a copy of the problem set. But in both of these cases we want to pass the problem set by reference; we want the remote Solver to operate on the same problem set object that we have on the client, so that when the solution is stored in the problem set, we will see it automatically on the client. We can do this in RMI by making a remote version of the ProblemSet class. With an RMI-enabled ProblemSet interface, we can use an instance of an implementation of the interface as an argument to remote methods, and the remote object will receive a stub to the local ProblemSet. The RMI version of the ProblemSet interface, the RMIProblemSet, is shown in Example 3-11.

Example 3-11: Interface for an RMI ProblemSet
package dcj.examples.rmi;
 
import java.rmi.*;
 
public interface RMIProblemSet extends Remote {
  public double getValue() throws RemoteException;
  public double getSolution() throws RemoteException;
  public void setValue(double v) throws RemoteException;
  public void setSolution(double s) throws RemoteException;
}

Now we'll need to write server-side implementations of these interfaces. Our server-side implementation of the RMISolver derives from java.rmi.Uni-castRemoteObject, and is shown in Example 3-12. The implementation of the RMIProblemSet interface is shown in Example 3-13. It also extends the UnicastRemoteObject class.

Example 3-12: Implementation of the RMISolver
package dcj.examples.rmi;
 
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.io.*;
 
public class RMISolverImpl
    extends UnicastRemoteObject
    implements RMISolver {
 
  // Protected implementation variables
  protected int numIterations = 1; // not used for this Solver...
  protected RMIProblemSet currProblem = null;
 
  // Constructors
  public RMISolverImpl() throws RemoteException { super(); }
  public RMISolverImpl(int numIter) throws RemoteException {
    super();
    numIterations = numIter;
  }
 
  // Public methods
  public boolean solve() throws RemoteException {
    System.out.println("Solving current problem...");
    return solve(currProblem, numIterations);
  }
 
  public boolean solve(RMIProblemSet s, int numIters) 
      throws RemoteException {
    boolean success = true;
 
    if (s == null) {
      System.out.println("No problem to solve.");
      return false;
    }
 
    System.out.println("Problem value = " + s.getValue());
 
    // Solve problem here...
    try {
      s.setSolution(Math.sqrt(s.getValue()));
    }
    catch (ArithmeticException e) {
      System.out.println("Badly-formed problem.");
      success = false;
    }
 
    System.out.println("Problem solution = " + s.getSolution());
 
    return success;
  }
 
  public RMIProblemSet getProblem() throws RemoteException {
    return currProblem;
  }
  
  public boolean setProblem(RMIProblemSet s) throws RemoteException {
    currProblem = s;
    return true;
  }
 
  public int getIterations() throws RemoteException {
    return numIterations;
  }
  
  public boolean setIterations(int numIter) throws RemoteException {
    numIterations = numIter;
    return true;
  }
 
  public static void main(String argv[]) {
    try {
      // Register an instance of RMISolverImpl with the
      // RMI Naming service
      String name = "TheSolver";
      System.out.println("Registering RMISolverImpl as \"" + name + "\"");
      RMISolverImpl solver = new RMISolverImpl();
      Naming.rebind(name, solver);
      System.out.println("Remote Solver ready...");
    }
    catch (Exception e) {
      System.out.println("Caught exception while registering: " + e);
    }
  }
}

Example 3-13: Implementation of the RMIProblemSet

package dcj.examples.rmi;
 
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
 
public class RMIProblemSetImpl
    extends java.rmi.server.UnicastRemoteObject
    implements RMIProblemSet {
 
  protected double value;
  protected double solution;
  
  public RMIProblemSetImpl() throws RemoteException {
    value = 0.0;
    solution = 0.0;
  }
  
  public double getValue() throws RemoteException {
    return value;
  }
  
  public double getSolution() throws RemoteException {
    return solution;
  }
  
  public void setValue(double v) throws RemoteException {
    value = v;
  }
  
  public void setSolution(double s) throws RemoteException {
    solution = s;
  }
}

These implementations of our Solver and ProblemSet interfaces are very similar to those that we created for the earlier CORBA examples. As in our earlier examples, the Solver simply performs a square root on the ProblemSet floating-point value. The RMISolverImpl has a main() method that registers a RMISolverImpl object with the local RMI registry.

Now we compile our interfaces and our server implementations into bytecodes, then generate their client stubs and server skeletons using the rmic compiler:

myhost% rmic dcj.examples.rmi.RMIProblemSetImpl
myhost% rmic dcj.examples.rmi.RMISolverImpl

The last required item is a client to use our remote object. The RMISolverClient in Example 3-14 is a simple client for the remote solver. The client has a single main() method where it gets a stub for the remote solver and asks it to solve a problem. The first line of the main() method installs the RMISecurityManager. Next, the client looks up the solver registered on the remote server through the Naming.lookup() method. Once it has the RMISolver stub, it creates a RMIProblemSetImpl object (our RMI-enabled ProblemSet implementation), and passes it into the solver's solve() method. The remote solver receives a stub to the RMIProblemSetImpl object on the client host, and solves the problem it represents. The methods that the remote RMISolver calls on the RMIProblemSet stub are invoked remotely on the RMIProblemSetImpl object on the client. Once the solve() method returns, our client can get the problem solution from the RMIProblemSetImpl object that it passed into the remote method call.

Example 3-14: An RMISolver Client
package dcj.examples.rmi;
 
import java.rmi.*;
import java.rmi.server.*;
 
public class RMISolverClient {
  public static void main(String argv[]) {
    // Install a security manager that can handle remote stubs
    System.setSecurityManager(new RMISecurityManager());
 
    // Get a remote reference to the RMISolver class
    String name = "rmi://objhost.myorg.com/TheSolver";
    System.out.println("Looking up " + name + "...");
    RMISolver solver = null;
    try {
      solver = (RMISolver)Naming.lookup(name);
    }
    catch (Exception e) {
      System.out.println("Caught an exception looking up Solver.");
      System.exit(1);
    }
 
    // Make a problem set for the solver
    RMIProblemSetImpl s = null;
    
    try {
      s = new RMIProblemSetImpl();
      s.setValue(Double.valueOf(argv[0]).doubleValue());
    }
    catch (Exception e) {
      System.out.println("Caught exception initializing problem.");
      e.printStackTrace();
    }
 
    // Ask solver to solve
    try {
      if (solver.solve(s, 1)) {
        System.out.println("Solver returned solution: " +
                           s.getSolution());
      }
      else {
        System.out.println(
          "Solver was unable to solve problem with value = " +
          s.getValue());
      }
    }
    catch (RemoteException e) {
      System.out.println("Caught remote exception.");
      System.exit(1);
    }
  }
}

Finally, we're ready to try our distributed object system. First, we start a registry on the host that is serving objects through the Naming service:

objhost% rmiregistry &

Now we can register a RMISolverImpl object by running the main() method on the RMISolverImpl class:

objhost% java dcj.examples.rmi.RMISolverImpl
Registering RMISolverImpl as "TheSolver"
Remote Solver ready...

Back on our client host, we can run the client class:

client% java dcj.examples.rmi.RMISolverClient 47.0
Looking up "rmi://objhost.myorg.com/TheSolver"...
Solver returned solution: 6.855654600401044

Our remote solver has solved our problem for us.

It's important to note here that the ProblemSet we're sending to the remote Solver object through a remote method call isn't being served in the same way as the Solver object. The Solver server doesn't need to lookup the ProblemSet object through the RMI registry. A stub interface to the client-side RMIProblemSetImpl object is automatically generated on the server side by the underlying RMI system.

RMI vs. CORBA

In this chapter we've implemented the simple distributed compute engine using both CORBA and RMI, and we've seen many similarities between the two in terms of functionality. There are also some critical differences between the two technologies. In order for you to understand which distributed object scheme is right for whatever system you're facing, it's important to spell out these differences.

The Language Barrier: Advantage or Disadvantage?

As we mentioned before, RMI is a Java-centric distributed object system. The only way currently to integrate code written in other languages into a RMI system is to use the Java native-code interface to link a remote object implementation in Java to C or C++ code. This is a possibility, but definitely not something for the faint of heart. The native-code interface in Java is complicated, and can quickly lead to fragile or difficult-to-maintain code. CORBA, on the other hand, is designed to be language-independent. Object interfaces are specified in a language that is independent of the actual implementation language. This interface description can then be compiled into whatever implementation language suits the job and the environment.

This distinction is really at the heart of the split between the two technologies. RMI, as a Java-centric system, inherits all of the benefits of Java. An RMI system is immediately cross-platform; any subsystem of the distributed system can be relocated to any host that has a Java virtual machine handy. Also, the virtual machine architecture of Java allows us to do some rather interesting things in an RMI system that just aren't possible in CORBA. For example, using RMI and the object serialization in the java.io package, we can implement an agent-based system where clients subclass and specialize an Agent interface, set the operating parameter values for the agent, and then send the object in its entirety to a remote "sandbox" server, where the object will act in our name to negotiate on some issue (airline ticket prices, stocks and bonds, order-fulfillment schedules, etc.). The remote server only knows that each agent has to implement an agreed-upon interface, but doesn't know anything about how each agent is implemented, even though the agent is running on the server itself. In CORBA, objects can never really leave their implementation hosts; they can only roam the network in the virtual sense, sending stub references to themselves and to clients. We don't have the option of offloading an object from one host to another.

However, CORBA doesn't require a commitment to a single implementation language. We can pick and choose how different elements of a distributed system are implemented based on the issues at hand. Legacy systems may dictate our implementation language in some cases (large COBOL systems like to be spoken to in COBOL, for example). Performance may be an issue in other cases. Heavy computational tasks like computational fluid dynamics and finite-element modeling are best written in languages that can be compiled down to native hardware instructions, like C and C++. The Java virtual machine architecture is a disadvantage here, since an additional interpretation layer is added to the processing of instructions. The Java just-in-time compilers (JIT) are capable of generating native instructions from Java bytecodes, but there is still an additional piece of overhead in running each piece of Java code. If we know that migrating system elements around the network is not necessary, then natively compiled Java code can be permanently installed, but by doing this we're sacrificing the critical "run everywhere" aspect of Java.

If we're using CORBA in these cases, we can take IDL interface definitions for our objects, compile them into COBOL, C, C++, or whatever languages we need at the various nodes in our distributed system. As long as the ORB implementations that we use at each node support a standard inter-ORB protocol like IIOP, the various CORBA objects implemented in various languages can interact with each other just fine.

Other Differences

In addition to this core distinction between CORBA and RMI, there are other differences to keep in mind:

The Bottom Line

So which is better, CORBA or RMI? Basically, it depends. If you're looking at a system that you're building from scratch, with no hooks to legacy systems and fairly mainstream requirements in terms of performance and other language features, then RMI may be the most effective and efficient tool for you to use. On the other hand, if you're linking your distributed system to legacy services implemented in other languages, or if there is the possibility that subsystems of your application will need to migrate to other languages in the future, or if your system depends strongly on services that are available in CORBA and not in RMI, or if critical subsystems have highly-specialized requirements that Java can't meet, then CORBA may be your best bet.


1. Clients can also access your CORBA objects without knowing this information, if they have obtained an encoded reference to your objects. This topic is beyond the scope of our discussion here, however.

2. The CORBA examples shown in this chapter were compiled and tested using the early-access release of JavaIDL.

Back to: Java Distributed Computing


O'Reilly Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com