home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam    

Book Home Java Distributed Computing Search this book

3.5. 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 Object Request Broker (ORB), which provides clients and servers of distributed objects with the means to make and receive requests of each other. ORBs can also provide object services, such as a Naming Service that lets clients look-up objects by name, or Security Services that provide for secure inter-object communications.

  • Methods for specifying the interfaces that objects in the system support. These interfaces specify the operations that can be requested of the object, and any data variables available on the object. CORBA offers two ways to specify object interfaces: an Interface Definition Language (IDL) for static interface definitions, and a Dynamic Invocation Interface (DII), which lets clients access interfaces as first-class objects from an Interface Repository. The DII is analogous in some ways to the Java Reflection API, which was introduced in JDK 1.1.

  • A binary protocol for communication between ORBs, called the Internet Inter-ORB Protocol (IIOP).

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.

3.5.1. 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.

3.5.2. 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.

3.5.3. 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]

[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.

3.5.4. 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.

3.5.5. 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.

3.5.5.1. 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.

3.5.5.2. 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:

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

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:

  • Our module definitions from the IDL file have been converted into a package specification in the base Java interface. In the IDL, we declared that the ProblemSet and Solver interfaces were contained within the examples module, which was further contained within the DCJ module. In the generated Java code, the classes are kept in the DCJ.examples package.

  • Where we've declared inout method arguments that are objects, the idltojava compiler has put "holder" types into the Java base class. For each interface defined in IDL, the IDL-to-Java compiler not only generates a Java base class for the interface, it also generates "holder" Java classes for the object. The "holder" class name is just the interface name with an appended "Holder" (e.g., SolverHolder, ProblemSetHolder). The holder classes act as streamable versions of the main class; the ORB uses these holder classes to transmit instances of the interface as remote method arguments. You can access the actual class by using the value attribute on the Holder objects. (For example, SolverHolder.value is a reference to the actual Solver object.)

Example 3-6. CORBA-Generated ProblemSet Base Interface

/*
 * File: ./DCJ/examples/ProblemSet.java
 * From: Solver.idl
 *   By: idltojava JavaIDL Wed Mar 5 17:02:26 1997
 */

package DCJ.examples;
public interface ProblemSet
    extends org.omg.CORBA.Object {
    double getValue();
    void setValue(double v);
    double getSolution();
    void setSolution(double s);
}

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.

3.5.5.3. 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-5. 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();
}

3.5.5.4. 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);
    }
  }
}

3.5.5.5. 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


Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.







??????????????@Mail.ru