Introduction to Java Remote Method Invocation (RMI)

   

What is this course about:

  • 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. 
Introduction
  • Java RMI is shipped with the Java JDK 1.4. 
  • It is a true distributed computing application interface for Java. Unlike other distributed programming interfaces (e.g., Corba, 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. 
How RMI works
  • RMI uses a network-based registry to keep track of the distributed objects. The server object makes a method available for remote invocation by binding it to a name in the registry. The client object, in turn, can check for availability of an object by looking up its name in the registry . The registry acts as a limited central management point for RMI . The registry is simply a name repository . It does not address the problem of actually invoking the remote method. 
  • The two objects may physically reside on different machines. A mechanism is needed to transmit the client's request to invoke a method on the server object to the server object and provide a response. RMI uses an approach similar to RPC in this regard. The code for the server object must be processed by an RMI compiler called rmic, which is part of the JDK. 
  • The rmic compiler generates two files: a stub and a skeleton. The stub resides on the client machine and the skeleton resides on the server machine. The stub and skeleton are comprised of Java code that provides the necessary link between the two objects. 
  • When a client invokes a server method, the JVM looks at the stub to do type checking (since the class defined within the stub is an image of the server class). The request is then routed to the skeleton on the server, which in turn calls the appropriate method on the server object. In other words, the stub acts as a proxy to the skeleton and the skeleton is a proxy to the actual remote method. 
  • 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 machines, (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. 
The example

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. 

Creating the Interface Definition 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 mathCalc.java 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 only 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 a locally created server object to the client
        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. There is, however, enough support in JDK 1.3 to allow you to write your own MulticastRemoteObject class to support multicast remote clients with the Factory Pattern which we will discuss in a subsequent course (see also here ).
  • You can also create an activatable object, by extending java.rmi.activation.Activatable instead of the UnicastRemoteObject class. A remote object can be accessed "on demand", i.e. the server is started on "demand", rather than running all the time. We will discuss this issue in the next course.
  • 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 (out parameter). The schedule object is a local object to the server 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 through remote interfaces, this local class must be 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 class, inheritance classes, instances within the class, etc.) (in deep copy). 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. 
Argument Passing
  • In order for RMI to be useful, it must do more than just plain method invocation. It must be able to pass arguments to these invoked methods (in parameters) and also handle return values from the invoked methods (out parameters). While at first this may sound like a basic operation, it's really quite complex. 
  • For one thing, there are no restrictions on the types of arguments to be passed. These arguments can, in example, be remote objects, which complicates the handling of the arguments. 
  • Practically speaking, RMI allows two ways to handle the passing of arguments; the deciding factor on which to use is whether or not the arguments refer to remote objects. At the risk of stating the obvious, any object that is an instance of RemoteObject is considered a remote object. So here's the rule: 
    • If an argument or return value is a remote object, a reference to the object is passed. This requires a stub that refers to the remote object. 
    • If an argument or return value is not a remote object , it is in deep copied over the network. That means the argument is passed by value. Any other objects referenced by the remote object argument are also passed by value, in a chain-like manner. 
    • This may sound restrictive or inefficient, but it has at least one virtue: It works. Admittedly, this isn't as extensive or flexible as argument passing in CORBA. But that doesn't mean RMI isn't useful. One should always use the right tool for the job. Some applications will demand the depth of a robust object broker model like CORBA, and others will be served just as well by RMI. 

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. 
                      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. 
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 target = new String( "//"+ args[0] + "/calcMath");
            boolean notBound = true;
               
             
            while (notBound)  {
                        try {
                                cm = (mathCalc)Naming.lookup( target );
                                notBound = false;
                        } catch (NotBoundException e) {
                                System.out.println("Trying to connect " + target);
                                try { Thread.sleep(100000); } catch (InterruptedException e2) {}
                        } catch (RemoteException e) {
                                System.out.println("Trying to connect " + target);
                                try { Thread.sleep(100000); } catch (InterruptedException e2) { }
                        } catch (java.net.MalformedURLException e) {
                                System.out.println("URL error: " + e);
                                System.exit(0);
                        } // try
                } // while
            System.out.println("Connection established to " + target);
            System.out.println("Calc Server Lookup: url =" + 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. 
  • 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. 
  • 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 may look like this: 
        rmi://pl01-itec.uni-klu.ac.at:Portnumber/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 (see above)
  • Portnumber is optional, if rmiregistry is started on the server with 1099.

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 may start the registry from the program, too. Below is an alternative calcServ.java code for the server: 
  /****************************************************
   * module: calcServ.java
   ****************************************************/
  import java.util.*;
  import java.rmi.*;
  import java.rmi.registry.*;
  import java.rmi.RMISecurityManager;

  public class calcServ
  {
      public static void main( String args[] )
      {
         
         try
         {
            System.out.println("Starting calcServer");
           
            Registry reg = null;
            System.setSecurityManager(new RMISecurityManager());
            try { reg = LocateRegistry.createRegistry(DefaultPort);}  
            catch (RemoteException e) { 
                        try { reg = LocateRegistry.getRegistry(); }  
                        catch (RemoteException e2) {
                        System.out.println("Registry could not be established" + e); 
                        System.exit(0);
                        }  // try-catch-e2
                }  // try-catch-e
            System.out.println("Registry established");
            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 object. This is the actual remote object the server exports. Since the server uses the rmiregistry, you must bind an instance of the object (e.g. cm) with the name (e.g. calcMath) that will be used to look up the object.
  • 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. 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. 

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: 
  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 "rmiregistry 1095 &". ] 

Next, start the server as follows: 

  java -Djava.security.policy=java.policy calcServ &

with the java.policy file as e.g. :

grant {
       permission java.net.SocketPermission "*:1024-65535","connect,accept,resolve";
       permission java.net.SocketPermission "*:80,"connect";
};

or a general one :

grant {
	// Allow everything for now
	permission java.security.AllPermission;
};
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: 

  java -Djava.security.policy=java.policy calcClient pl01-itec.uni-klu.ac.at
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. 

 

 

harald.kosch@itec.uni-klu.ac.at - Institute HomePage
Last updated 20/02/2003.