Introduction to Java Remote Method Invocation (RMI)

From EDM2
Jump to: navigation, search

Introduction

[NOTE: Here is a link to a zip of the source code for this article. Ed]

This is a two-part series on the topic of Java's Remote Method Invocation (RMI). In this article I am going to discuss the basics of Java RMI (e.g., layers, rmic, registry, etc.) and how to apply and use it to develop distributed computing applications through examples. The second article discusses more advanced topics on Java RMI (e.g., CORBA IIOP and IDL, datagram packaging of Remote objects, distributed garbage collector, etc.).

Let's get started with the first article. In the past, developing cross-platform, distributed computing applications has been difficult to say the least. With considerations like hardware, operating system, language, serialization, etc., development and support have to be adjusted (if not reinvented) each time a developer deploys their applications for a new target system. And if a developer wants to provide robust support (e.g., marshaling/unmarshaling objects), the developer is required to create the support themselves to fit the idiosyncrasies of each system.

Now with the ever-increasing acceptance of the Java operating environment and language (e.g., the write once, run anywhere mantra), an application developer has the ability to write true, robust cross-platform distributive computing applications. He can do this without worrying about special system-specific support concerns.

Java RMI is shipped with the Java JDK 1.1 and higher. It is a true distributed computing application interface for Java. Unlike other distributed programming interfaces (e.g., RPC, IDL, etc.), Java RMI is language specific. This is a good thing because by being language specific, RMI has the ability to provide more advanced feature like serialization, security, etc. Contrast this with other distributive computing interfaces that are language independent and must write to the least common denominator to support many platforms/languages. This topic will be explored further in the second article.

Java RMI is comprised of three layers that support the interface. See illustration below.

Rmi.gif

The first layer is the Stub/Skeleton Layer. This layer is responsible for managing the remote object interface between the client and server.

The second layer is the Remote Reference Layer (RRL). This layer is responsible for managing the "liveliness" of the remote objects. It also manages the communication between the client/server and virtual machine s, (e.g., threading, garbage collection, etc.) for remote objects.

The third layer is the transport layer. This is the actual network/communication layer that is used to send the information between the client and server over the wire. It is currently TCP/IP based. If you are familiar with RPC, it is a UDP-based protocol which is fast but is stateless and can lose packets. TCP is a higher-level protocol that manages state and error correction automatically, but it is correspondingly slower than UDP.

The example used in this article is an amortization schedule application. The client requests a local schedule object from the server through a remote object and passes an amount and duration of a loan to the server. The server instantiates a local schedule object with the amount and duration along with the interest rate the server knows about. Then the schedule object is serialized and returned back to the client. The client can then print the object or modify it at this point. The client has its own private copy of the schedule object. Below is an illustration that serves as a reference for the parts of the RMI application.

RMI-files.gif

Creating the Interface Definitions File

The first thing that you must do to develop an RMI application is to define the remote interface. The interface defines what remote methods/variables are going to be exported from the remote object. Usually the interface defines methods only because variables have to be declared final (i.e., constant) if they are in an interface definition.

The remote interface needs to import the RMI package, and every exported method must throw an RMI remote exception to manage errors during invocation. Below is the code for the mathCalc.java interface definition file in our example.

  /****************************************************
   * module: mathCalc.java
   ****************************************************/
  import java.lang.*;
  import java.rmi.*;

  public interface mathCalc extends java.rmi.Remote
  {
     public schedule amortizeSchedule( float ammount,
                                       int duration ) throws
                                                 java.rmi.RemoteException;
     public void     printRate() throws java.rmi.RemoteException;
  }

If you are familiar with using Java and interfaces, converting your local objects to remote objects can be done very quickly with minor modifications to your source. You need to include the Java RMI package and manage RMI remote exceptions on all your exported local methods.

Creating the Interface Implementation File

Once the interface definition file is created, you need to define the actual code that supports the interface on the server. Below is an example of the mathCalcImp.java interface implementation file used to provide that support.

  /****************************************************
   * module: mathCalcImp.java
   ****************************************************/
  import java.rmi.*;
  import java.rmi.server.*;

  class mathCalcImp extends UnicastRemoteObject implements mathCalc
  {
     float interestRate = (float)7.5;

     mathCalcImp() throws java.rmi.RemoteException
     {
     }

     public schedule amortizeSchedule( float ammount, int duration ) throws
                                       java.rmi.RemoteException
     {
        System.out.println("Amortizeing Schedule.");
        return( new schedule( interestRate, ammount, duration ) );
     }

     public void printRate() throws java.rmi.RemoteException
     {
        System.out.println("Current Interest Rate is " + interestRate );
     }
  }

Notice the implementation file imports the package java.rmi.*. It also imports the java.rmi.server.* package. This is so you can extend the UnicastRemoteObject class to support remote clients. This class manages client/server and peer/peer connection support for RMI.

Today there is no MulticastRemoteObject class, but it should appear in some JDK 1.2 release. There is, however, enough support in JDK 1.1 to allow you to write your own MulticastRemoteObject class to support multicast remote clients.

Notice how the file defined above implements mathCalc, the remote interface definition that was defined earlier. Each method in the implementation file that is going to be externalized needs to throw a remote exception.

Object Serialization

The amortizeSchedule() method prints a message on the server and instantiates a new local schedule object that is returned to the client. The schedule object is a local object that will be serialized and marshaled into a data stream to be sent back to the client.

Now is a good time to discuss the serialization of remote objects. To begin that discussion, the schedule.java.local class is presented below.

  /****************************************************
   * module: schedule.java
   ****************************************************/
  import java.lang.*;
  import java.util.*;
  import java.io.*;

  class schedule implements Serializable
  {
    float  totalLoanAmt;
    float  usrAmmount;
    float  interestRate;
    int    loanDuration;

    schedule( float rate, float ammount, int duration )
    {
       interestRate = rate;
       usrAmmount   = ammount;
       loanDuration = duration;
       totalLoanAmt = ammount + (ammount / rate);
    }

    void print()
    {
       System.out.println("Schedule Created.");
       System.out.println("Calculation information based on:");
       System.out.println("            Rate        [%" + interestRate + "]" );
       System.out.println("            Ammount     [$" + usrAmmount + "]" );
       System.out.println("            Duration    [ " + loanDuration + "]" );
       System.out.println("            Total Loan  [$" + totalLoanAmt + "]" );

       int   couponNum        = 0;
       float balanceRemaining = totalLoanAmt;
       float monthlyPayment   = 0;

       System.out.println();
       System.out.println( "Payment Monthly Payment Ammount   Balance Remaining");
       System.out.println( "------- -----------------------   -----------------");

       while( balanceRemaining > 0 )
       {
          couponNum++;
          monthlyPayment = totalLoanAmt/loanDuration;
          if( balanceRemaining < monthlyPayment )
          {
            monthlyPayment   = balanceRemaining;
            balanceRemaining = 0;
          }
          else
          {
            balanceRemaining = balanceRemaining - monthlyPayment;
          }

          System.out.println( couponNum + " " + monthlyPayment + " " +
                              balanceRemaining );
       }
    }
  }

If you are passing local objects around through remote interfaces, you have to make the defining local class serializable. Notice that the schedule class implements the serializable interface, but it does not have to provide any code. This is because Java manages the serialization of serializable interfaces for you. If we were to implement externalizable instead of serializable, then the schedule.java class would have to provide the serialize/deserialize methods. This would require the schedule class to serialize and deserialize its own data. If you try to pass a local object that has not implemented the serializeable/externalizeable interface, Java will throw a marshaling exception on the server/client.

Note: Be careful when marking a class serializable, because Java will try to "flatten" everything related to that cl., inheritance classes, instances within the class, etc.). As an example, I would not recommend trying to serialize anything like the root drive on your disk. There is also a lot of overhead involved in the serialization/deserialization process. Use serialization with care.

The topic of serialization will be revisited in the second article when I discuss encryption of remote objects for security.

Creating the Stubs/Skeletons

Now that the interface and implementation files have been created, you need to generate the stubs and skeleton code. This is done by using the rmic compiler provided by the JDK. The following command will generate the stub and skeleton .class files, but it will not create the .java files. If you want to see the Java-generated code, use the -keepgenerated option. This will leave the .java files files around, but don't try to modify these files. The rmic compiler also has a -show option that runs it as an AWT application.

Command Line > rmic mathCalcImp

After running the rmic compiler, you should see mathCalcImp_Skel.class and mathCalcImp_Stub.class. These classes are where your references to the remote objects will resolve to in the client's address space. The RRL will manage the mapping of these objects to the server's address space.

Creating the Client

Now we need to create the client-side application that will use the remote objects. Below is the sample code for calcClient.java.

  /****************************************************
   * module: calcClient.java
   ****************************************************/
  import java.util.*;
  import java.net.*;
  import java.rmi.*;
  import java.rmi.RMISecurityManager;

  public class calcClient
  {
      public static void main( String args[] )
      {
         mathCalc cm = null;
         int i = 0;
         System.setSecurityManager( new RMISecurityManager());

         try
         {
            System.out.println("Starting calcClient");

            String url = new String( "//"+ args[0] + "/calcMath");

            System.out.println("Calc Server Lookup: url =" + url);
            cm = (mathCalc)Naming.lookup( url );

            if( cm != null )
            {
              String testStr = "Requesting Current Interest Rate...";

              // Print Current Interest Rate from the server
              cm.printRate();

              // Amortize a schedule using the server interest
              // rate.

              float    amount   = (float)10000.50;
              int      duration = 36;
              schedule curschd  = cm.amortizeSchedule( amount, duration );

              // Print the schedule
              curschd.print();
            }
            else
            {
              System.out.println("Requested Remote object is null.");
            }
         }
         catch( Exception e )
         {
            System.out.println("An error occured");
            e.printStackTrace();
            System.out.println(e.getMessage());
         }
      }
  }

The client code imports the java.rmi package along with the java.rmi.RMISecurityManager. The first thing the client needs to do is register a security manager with the system. The RMI package provides an RMI security manager, but if you like writing security managers, you can register your own. If a security manager is not registered with the system, Java will only allow resolution of classes locally. This, of course, defeats the purpose of distributed computing.

If you are writing an applet instead of an application, the security manager has already been registered for you by the browser. You cannot register another security manager for the applet. In the second article I will go into more details about closed and open systems for Java and RMI.

Once you have registered the security manager, you need to create a URL string that is comprised of the server name and remote object name you are requesting. This will enable the client to look up the remote object on the server via the rmiregistry. Your client code will call the Naming.lookup method that makes a request to the server to return a remote object reference. Notice the object returned from the Naming.lookup method is cast to the actual interface class. This is because the lookup call returns a reference of type Object, an abstract type that needs to be casted to a concrete class (e.g., the interface definition file, mathCalc). The URL name lookup format for an RMI object via the registry looks like this:

  rmi://www.myserver.com/myObject
       or
  //www.myserver.com/myObject

If the client is successful in retrieving the remote reference, it can invoke remote methods on the remote object at this point. The example makes a call to print the interest rate on the server, and it makes a request to amortize a schedule. If the amortize schedule is successful, the client gets a local copy of the schedule object. Then the client can call routines in the schedule object, modify the object, etc. This is the client's private copy of the object, and the server has no knowledge of any changes to this object made by the client. Local objects are by copy, and remote objects are by reference.

Creating the Server

The server has very simple code that is similar to the client. Below is the calcServ.java code for the server:

  /****************************************************
   * module: calcServ.java
   ****************************************************/
  import java.util.*;
  import java.rmi.*;
  import java.rmi.RMISecurityManager;

  public class calcServ
  {
      public static void main( String args[] )
      {
         System.setSecurityManager( new RMISecurityManager());

         try
         {
            System.out.println("Starting calcServer");
            mathCalcImp cm = new mathCalcImp();

            System.out.println("Binding Server");
            Naming.rebind("calcMath", cm );
            System.out.println("Server is waiting");
         }
         catch( Exception e )
         {
            System.out.println("An error occured");
            e.printStackTrace();
            System.out.println(e.getMessage());
         }
      }
  }

The server has the same requirements as the client has regarding the security manager. Once the server has registered properly with the security manager, the server needs to create an instantiation of the mathCalcImp implementation objec This is the actual remote object the server exports. Since the server uses the rmiregistry, you must bind (i.e., alias) an instance of the object with the name that will be used to look up the object. In the second article I will talk about an alternative way to look up/pass objects around without using the registry.

Note: The server sample uses rebind instead of bind. This is to avoid the following problem with bind; i.e., if you start your server and bind an object to the registry then later start a newer version of the server, the bind will not take place because a previous version already exists. When your client references the server, it will get the original reference to the object and not the latest. Also, when the client tries to reference the remote object, the server will throw an exception because the object is no longer valid. If you instead use rebind, then each time you start a new server, it will bind the latest object for the name lookup and replace the old object.

You can export as many objects as you like. For the sake of simplicity, the example only exports one object. Additionally, you can have a factory class that returns object references or you can use the registry to look up multiple names of objects. You normally only need one registry running, but Java does not preclude running multiple registries on different ports. The client needs to use the correct lookup method to gain access to the correct registry on a port number.

If you are looking at this server application and wondering how it continues to run after it has seemingly completed its mission, the answer is that the main thread goes away at this point. However, when the server calls the registry to bind the object, it creates another thread under the covers that blocks waiting in a loop for a registry derigstration event. This keeps the server from terminating. If you don't use the registry, the server example needs to be modified to stay alive to support the references to remote objects.

Building the Sample

You need to compile the client and the server code by doing the following:

javac calcClient.java
javac calcServ.java

Starting the Sample

Now you are ready to run the sample RMI application. The first thing to do is to start the rmiregistry on the server. Ensure that your CLASSPATH is set up so that the registry can find your server classes in its path. Start the rmiregistry as follows:

start rmiregistry (optional port : default port 1099 )

[The optional port number can be left out, in which case it defaults to 1099. If this is not the desired port, specify one as in "start rmiregistry 1095". Ed.]

Next, start the server as follows:

start java calcServ

The server will start and print a message that it is waiting for requests.

Now you are ready to start the client application as follows:

start java calcClient www.myserver.com

At this point you should see a request come into the server to print the interest rate and request a remote object reference. The client will then display the contents of the schedule object returned from the server.

Conclusion

Hopefully this article has helped illuminate the basic concepts and design of Java's RMI. This is just an introduction to RMI, but I will be talking about more advanced topics for writing a real-world Java RMI application in the next article.

The code for this article is downloadable from the top of this article. If you have any questions, you can send me e-mail. Also, as a side note, the ICAT debugger for OS/2 Java (ICATJVO) can debug Java RMI client and server code through the JNI interface on OS/2. So far, this is the only debugger I have found that can do this.