Statement of purpose.

The purpose of this tutorial is to demonstrate how RMI may be used for basic distributed computing using the Java programming language. Through this tutorial, following issues will be demonstrated:

  • writing client- and server objects,
  • using the JDK utilities for generating client stubs and server skeletons,
  • running Java programs using RMI.
  • Disclaimer.

    The API presented by this tutorial is just a narrow selection of primitives from (java.rmi.*). The examples are developed for educational purpose thus aim for clarity rather than exploiting "nifty details" of the language or achieving outstanding performance of the applications.

    A basic understanding of the Java programming language is assumed, thus basic concepts of the language (object oriented programming, exception handling etc.) is not described into details.

    Some of the programs included as examples or exercises in this tutorial accept incoming connections thus they are "servers". Running such servers on a permanent basis may compromise security of the departmental computer network. Furthermore, running servers and thereby offering "network services" without prior written permission from the system administration is violation of "responsible usage of the computer systems at cs.aau.dk". Temporary experimenting with e.g. the examples and exercises from this tutorial does however not require special permission.

    Introduction to Java RMI.

    The Java RMI (Remote Method Invocation) is a package for writing and executing distributed Java programs. The Java RMI provides a framework for developing and running servers (server objects). The services (methods) provided by those server objects can be accessed by clients in a way similar to method invocation. I.e. Java RMI hides almost all aspects of the distribution and provides a uniform way by which objects (distributed or not) may be accessed.

    Writing Java programs using RMI can be described by the following steps:

  • write server interface(s),
  • write server implementation,
  • generate server skeleton and client stub,
  • write client implementation.
  • Executing distributed Java programs using RMI can be described by the following steps:

  • start the rmi registry,
  • start the server object,
  • run the client(s).
  • The remainder of this tutorial will detail and exemplify those steps for writing and executing RMI programs.

    The example.

    The example, which will be used throughout this tutorial, will be rather simple. A server (RMIServer.java) will provide the methods String getString() and void setString(String s). A client (RMIClient.java) may use those two methods for retrieving and storing a string in the server, i.e. the client may modify and inspect the local state of the server object.

    The following sections will develop this server and a corresponding client.

    The server interface.

    The server interface is used by the stub/skeleton compiler when generating the client stub and the server skeleton files. This interface thus defines the methods in the server, which may be invoked by the clients.

    There are two issues to remember when writing such an interface. First, the interface has to be written as extending the java.rmi.Remote interface. Second, all methods in the interface must throw java.rmt.RemoteException. This exception must thus be caught when the clients are invoking any of the servers methods, thus the clients may have a way to determine if a method invocation was successful.

    The complete source code for the server interface (ServerInterface.java) is included below.

    package examples.rmi;
    import java.rmi.*;

    public interface ServerInterface extends Remote
    {

       public String getString() throws RemoteException;
       public void setString(String s) throws RemoteException;

    }

    The interface is compiled by the javac compiler to generate the file ServerInterface.class. The source code for the interface may be downloaded here.

    Writing the server object.

    The server must be written as a "regular" Java program, i.e. a program with a method public static void main(String argv[]). Through this main method, server objects may be instantiated and registered with the rmi registry.

    Each distributed object is identified by a string, specifying the object name. This string is registered with the rmi registry and is used by the clients when requesting a reference to the server object. The following lines of code indicates how an instance of RMIServer can be registered with the rmi registry under the string name "RMIServer". The following code would typically appear in the main method of the server:

    String name = "RMIServer";
    RMIServer theServer = new RMIServer();
    Naming.rebind(name,theServer);

    Server objects must - of course - implement the defined interfaces and in addition typically extend java.rmi.server.UnicastRemoteObject. At the abstract level, this should potentially enable various kinds of distribution schemes (e.g. replication of objects, multicast groups of objects etc.) by extending different classes. However in current implementations of Java, only java.rmi.server.UnicastRemoteObject is available. The only practical advantage of inheriting from java.rmi.sever.UnicastRemoteObject is that it will preserve the usual semantics of the methods hashCode(), equals() and toString() in a distributed environment.

    The source code for the RMIServer.java is included below and may furthermore be downloaded from here.

    package examples.rmi;
    import java.rmi.server.UnicastRemoteObject;
    import java.rmi.*;

    public class RMIServer extends UnicastRemoteObject implements ServerInterface
    {
       private String myString = "";

       // The default constructor
       public RMIServer() throws RemoteException
         {
           super();
         }

       // Implement the methods from the ServerInterface
       public void setString(String s) throws RemoteException
         {
           this.myString = s;
         }

       public String getString() throws RemoteException
         {
           return myString;
         }

       // The main method: instantiate and register an instance of the
       // RMIServer with the rmi registry.
       public static void main(String argv[])
         {
           try
             {
               String name = "RMIServer";
               System.out.println("Registering as: \""+name+"\"");
               RMIServer theServer = new RMIServer();
               Naming.rebind(name,theServer);
               System.out.println(name+" ready...");
            }
          catch(Exception e)
            {
               System.out.println("Exception while registering: "+e);
             }
         }
    }

    The RMIServer.java is compiled using the default javac to generate the file RMIServer.class.

    Generating skeleton and stub.

    Based on the compiled ServerInterface and RMIServer files, a client stub and a server skeleton can be generated. The server skeleton acts as interface between the rmi registry and the server objects residing on a host. Likewise, the client stub of the server is returned to the client when a reference to the remote object is requested. The exact details (using the skeleton and stub) are all taken care of by the runtime environment.

    To generate the skeleton and the stub, the rmic compiler is used. Like the Java virtual machine, the rmic compiler requires fully qualified class names, i.e. to generate the server skeleton and the client stub for the RMIServer, the following command must be invoked:

    rmic examples.rmi.RMIServer

    This generates the file RMIServer_Skel.class and RMIServer_Stub.class .

    UPDATE: rmic only generates RMIServer_Stub.class

    UPDATE: as of java 1.6 no stub need to be generated - all done through reflection now. Normally no need for rmic unless you need to generate stubs for old version or IIOP. But be sure that jre and jdk are both java 1.6 or higher/compatible.
    Check java -version and javac -version

    Writing the client implementation.

    When writing a client implementation, three things must be done. First, a "security manager" must be installed. Such a security manager specifies the security policy, i.e. decides which constraints are imposed on the server stubs. An appropriate security manager for these examples can be installed by the following code:

    System.setSecurityManager(new java.rmi.RMISecurityManager());

    (Java security is a world of its own and definitely worth investigating when writing distributed applications. [6] is dedicated to describing security aspects of the Java programming language and is highly recommended).

    Second, a reference to the remote object must be requested. To do so, the hostname of the host where the object resides as well as the string name, under which the object is registered, is required.

    In this example, the object was registered with the string name "RMIServer". Assuming that the server was started on the host "objecthost.domain.com", the following line of code may be used to get a reference to the object:

    String name = "rmi://objecthost.domain.com/RMIServer"
    server = (ServerInterface)Naming.lookup(name);

    The code above contacts the rmi registry at "objecthost.domain.com" and asks for the stub for the object, registered under the name "RMIServer".

    Thus in reality, Naming.lookup() returns an instance of the RMIServer_stub. However the available methods in the server object (and thus in the stub) are defined by ServerInterface. Thus whatever Naming.lookup() returns is typecast into a ServerInterface.

    The complete code for the RMIClient is included below and may furthermore be downloaded here:

    package examples.rmi;
    import java.rmi.server.*;
    import java.rmi.*;

    public class RMIClient
    {
       public static void main (String argv[])
         {
           // Parse the commandline to get the hostname where
           // the server object resides
           String host = "";

           if (argv.length == 1)
             {
               host = argv[0];
             }
          else
             {
               System.out.println("Usage: RMIClient server");
               System.exit(1);
             }

           // Install a security manager.
           System.setSecurityManager(new RMISecurityManager());

           // Request a reference to the server object
           String name = "rmi://"+host+"/RMIServer";
           System.out.println("Looking up: "+name);

           ServerInterface server = null;
           try
             {
               // In reality, Naming.lookup() will return an instance of
               // examples.rmi.RMIServer_stub.
               // This is typecast into the ServerInterface, which is what
               // specifies the available server methods.
               server = (ServerInterface)Naming.lookup(name);
             }
           catch(Exception e)
             {
               System.out.println("Exception " +e);
               System.exit(1);
             }

           // Given a reference to the server object, it is now
           // possible to invoke methods as usual:
           try
             {
               server.setString("Foobar");
               System.out.println("String in server: "
                          +server.getString());
            }
           catch(Exception e)
             {
               System.out.println("Exception " +e);
               System.exit(1);
             }
         }
    }

    Running the example.

    The first thing to do when running Java programs using RMI is to start the rmi registry:

    install:~> rmiregistry &
    [1] 1449
    install:~>

    Next, the server must be started:

    install:~> java examples.rmi.RMIServer
    Registering as: "RMIServer"
    RMIServer ready...

    Finally, the client may be started and the setup tested:

    install:~> java examples.rmi.RMIClient install.cs.aau.dk
    Looking up: rmi://install.cs.aau.dk/RMIServer
    String in server: Foobar
    install:~>
    (where install is the name of the machine you are using)

    In case you are getting security errors, the following small hack should be able to deal with it. Put the following peice of text in a file called Grant.java and place it in the working directory:

    grant
    
    {
    
        permission java.security.AllPermission;
    
    };
    
    
    
    And use it in the following way:
    
    
    
    java -Djava.security.policy=Grant.java 
    
     examples.rmi.RMIClient install.cs.aau.dk

    Update: To start the registry on a different port execute the command below, and change the url in the client and server to reflect this "rmi://hostname:port/RMIServer".

     install:~> rmiregistry <PORT>&

    Update: The registry needs to be started from the rigth directory (install:~> ), or the the codebase property in client or server must be set.

     

    Exercise 1.

    Develop a simple, RMI-based server (CatServer), which will allow a client to read a text-file line by line. I.e. the server provides the following methods:

  • public boolean openFile(String filename)
    Opens a file, returns true if file is successfully opened, false otherwise.
  • public String nextLine()
    Reads and returns the next line from the file. Returns null if end-of-file is reached or if no file has been opened.
  • public boolean closeFile()
    Close the file which is currently open. Returns true if the file is successfully closed, false otherwise.
  • Develop a client (CatClient), which can connect to the server. The client works like a "remote cat", i.e. it requests a file to be opened by the server and reads the file line-by-line (through nextLine()). The client sends the lines one-by-one to std. out while reading from the server.

    The following code may be useful when opening a file for reading:

    try
        {
          File myFile = new File(file);
          if (myFile.canRead() && myFile.isFile())
             {
               BufferedReader mrf = new BufferedReader(new FileReader
                                                           (myFile));
             }
        }
    catch (Exception e)
        {
          // In case of an exception, handle it...
          // and the server continiues
        }

    Reading line-by-line from a BufferedReader:

    String s = mrf.readLine();

    Bibliography / further reading

  • [1] "Concurrent Programming in Java", Doug Lea, Addison-Wesley, 1997
  • [2] "Distributed Systems", Sape Mullender, Addison-Wesley / ACM-Press, 1993
  • [3] "Distributed Systems - concepts and design", George Coulouris et. al, Addison-Wesley, 1996
  • [4] "Java in a nutshell", David Flanagan, O'Reilly, 1997
  • [5] "Java Distributed Computing", Jim Farley, O'Reilly, 1998
  • [6] "Java Security", Scott Oakes, O'Reilly, 1998
  • Last modified: 07/09-2006.