Writing Distributed Applications in Java, Part 2

by Qusay H. Mahmoud

Welcome to the second part of a series on writing distributed applications in Java. In part 1, which appeared in the September issue of Visual J++ Developer's Journal, we showed you how to implement a distributed application (an Arithmetic Server) using low-level sockets. In this article, we'll show you how to write a distributed application using Remote Method Invocation (RMI).

Developing distributed applications using low-level sockets involves designing a protocol. A protocol is basically a set of commands (or a language) that the client and the server agree upon, through which they'll be able to understand each other and get some work done. Designing such a protocol, however, is both difficult and prone to error. One pitfall, for example, is deadlock. In a deadlock, processes never finish executing, so they may hold system resources that prevent other processes from accessing those resources.

Instead of working directly with low-level sockets, there's a better way. You can develop distributed applications using Java's RMI. Introducing RMI RMI is a core package of the JDK1.1 that you can use to develop distributed applications. Using RMI, the methods of remote objects can be invoked from other Java Virtual Machines (JVMs), possibly running on different hosts. RMI is very similar to, but easier to use than, the remote procedure call (RPC) mechanism found on other systems because the programmer has the illusion of calling a local method from a local class file. Actually, all the arguments are shipped to the remote target and interpreted, and the results are sent back to the callers. The simplicity of creating a distributed application using RMI is quite appealing to programmers interested in distributed objects.

RMI supports the features that are most valuable for building distributed applications, namely, transparent invocations, distributed garbage collection, and convenient access to streams. Remote invocations are transparent, since they're identical to local ones. Thus, their method signature is identical as well.

The RMI Specification describes the following as the goals of the RMI package:

The OSI Reference Model defines a framework that consists of seven layers of network communication. Figure A shows how this model describes RMI.

Figure A: RMI fits into the OSI Reference Model like this.
[ Figure A ]

The user application is at the top layer. The application uses a data representation scheme to transparently communicate with remote objects. Note that the RMI system itself consists of three layers:

The Stubs/Skeletons Layer serves as an interface between an application and the rest of the RMI system. Its sole purpose is to transfer data to the Remote Reference Layer as a stream of bytes (known as marshal streams). This is actually where Object Serialization comes into play, by enabling Java objects to be transmitted between different address spaces. The Remote Reference Layer is responsible for carrying out the semantics of the invocation and transmits data to the Transport Layer using connection-oriented streams (TCP rather than UDP). The reason for using TCP is that the Transport Layer in the current RMI implementation is TCP-based, but of course you could substitute the UDP-based transport layer. Finally, the Transport Layer is responsible for setting up connections and managing them. Developing distributed applications with RMI Developing a distributed application using RMI involves six simple steps: To gain a better understanding of the process of developing a distributed application using RMI, let's examine each step by developing an example. The application we'll develop is the same one (the Arithmetic Server) that we developed in part 1 of this series. The Arithmetic Server can add, subtract, and multiply arrays of integers. However, for simplicity, we'll discuss only the add method; you can add the others, if you wish, when testing.

The first step is to define a remote interface for the remote objects that are to be used by clients; in this case, the add_arrays method. You may wonder, however, why we need an interface in the first place. The reason is that the programmer should be able to tell which methods the application (the Arithmetic Server in this case) provides, and how to use them just by looking at the interface. Defining a remote interface The remote interface for the Arithmetic Server is as follows:


// FileName: Arith.java
// package rmi.arith;
public interface Arith extends java.rmi.Remote {
    int[] add_arrays(int a[], int b[]) throws
	 java.rmi.RemoteException;
}
It's very important to note that the interface is (and must be) declared public; otherwise, clients won't be able to load remote objects that implement the remote interface. The remote interface extends the class Remote in order to fulfill the requirement for making the class a remote object. Also, note that each method declared in the remote interface must throw java.rmi.RemoteException. Once we develop our remote interface, we can compile it like this:

$ javac Arith.java
Implementing the remote interface The second step in developing a distributed application using RMI is to implement the remote interface we defined above. There are a number of important things to note from the code shown in Listing A. First, we're extending the UnicastRemoteObject to indicate that ArithImpl is being used to create a single remote object that uses RMI's default communication transport. Second, the main() method is creating and installing a security manager. The purpose of this is to protect the host from malicious code from the client. Note that you can either use the default (RMISecurityManager), or you can define one yourself. Third, the line


ArithImpl obj = new ArithImpl("ArithServer") 
creates an instance of the remote object. Once this is created, the server is ready to listen for the client's requests. Finally, the line


Naming.rebind("//hostname/ArithServer", obj) 
registers the remote object with the RMI registry. The rest of the implementation is really straightforward. At this point, we're ready to compile our ArithImpl.java remote interface, as shown here:


$ javac ArithImpl.java
Listing A: ArithImpl.java

//package rmi.arith;
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class ArithImpl extends UnicastRemoteObject implements Arith {
    private String name;

    public ArithImpl(String s) throws RemoteException {
        super();
        name = s;
    }

    public int[] add_array(int a[], int b[]) throws RemoteException {
        int c[] = new int[10];
        for (int i=0; i<10; i++) {
           c[i] = a[i] + b[i];
        }
        return c;
    }

    public static void main(String argv[]) {
        System.setSecurityManager(new RMISecurityManager());
        try {
           ArithImpl obj = new ArithImpl("ArithServer");
           Naming.rebind("//hostname/ArithServer", obj);
           System.out.println("ArithServer bound in registry");
        } catch (Exception e) { 
     System.out.println("ArithImpl err: " + e.getMessage());
           e.printStackTrace();
        }
    }
}
Writing applications that use remote objects Now, we're ready to write an application that remotely invokes any of the ArithImpl's methods. In this case, we've implemented only one method, add_arrays. Once the client invokes that remote method, it will receive some output (the sum of the arrays). The code for the client application is shown in Listing B. The most important thing to note from Listing B is the try { } statement, where we actually get a reference to the "ArithServer" from the RMI registry. We use this to invoke the remote method add_array. Now, we can compile our client application code, like this:


$ javac ArithApp.java
Listing B: ArithApp.java


//package rmi.arith;

import java.rmi.*;
import java.net.*;

public class ArithApp {

    public static String localHost() throws Exception {
        InetAddress host = null;
        host = InetAddress.getLocalHost();
        return host.getHostName();
    }

    public static void main(String argv[]) {
        int a[] = {4, 4, 4, 4, 4, 4, 4, 4, 4, 4};
        int b[] = {4, 4, 4, 4, 4, 4, 4, 4, 4, 4};
        int result[] = new int[10];
        try {
Arith obj = (Arith)Naming.lookup("//" + localHost() +
                  "ArithServer");
           result = obj.add_array(a, b);
        } catch (Exception e) {
           System.out.println("ArithApp exception:"+e.getMessage());
           e.printStackTrace();
        }
        System.out.println("The sum of the arrays is: ");
        for (int j=0; j<10; j++) {
           System.out.print(result[j] + ì   ì);
        }
    }
}
Generating stubs and skeletons Now, we're ready to generate the stubs and skeletons. A stub is a client-side proxy, and a skeleton is a server-side entity that dispatches calls to the actual remote object implementation. Stubs and skeletons are determined and dynamically loaded as needed at runtime.

The remote method invocation compiler (rmic) easily generates RMI stubs and skeletons as follows:


$ rmic ArithImpl
This command generates the files ArithImpl_Skel.class and ArithImpl_Stub.class. When running this application, you should make sure that your CLASSPATH includes a pointer to where all these classes are compiled and stored. Starting the RMI registry The RMI registry is basically a naming service that allows clients to obtain a reference to a remote object. So, before running the server and trying the client application, you need to run the RMI registry. In a UNIX environment, you can start the RMI registry like this:

$ rmiregistry &
The RMI registry will (by default) run on port 1099. If you wish to start the RMI registry on a different port number, then you need to specify the port on the command line like this

rmiregistry port# & 
where port# is the port number on which you want the registry to listen. Also, it's important to note that if you start the RMI registry on a port number other than the default, you'll then have to specify the port number when you bind it. That is, instead of writing

Naming.rebind("//hostname/
ArithServer", obj)
you'll need to write

Naming.rebind("//hostname:port /ArithServer", obj)
where port is again the port number on which the RMI registry is listening. Running the server and client Now we're ready to fire up our server and client application. You can start the server process in a UNIX environment as follows:

$ java ArithImpl &
And, finally, when running the client application

$ java ArithApp
you should see the following output:

The sum of the arrays is:
8   8   8   8   8   8   8   8   8   8 
which is the correct result. Test this distributed application for yourself! Conclusion In part 1 of our series on writing distributed applications in Java, we showed how using low-level sockets involves the design of a protocol, which can be difficult to write and error-prone. With RMI, distributed applications development is straightforward, because remote invocations in RMI are identical to local ones, so their method signature is identical.

Compared to working with low-level sockets, using RMI is much easier. If you've read part 1 in this series, you'll note that we had to write a few method utilities that can read arrays of integers to sockets. In the case of RMI, we didn't have to worry about that. However, it's important to note that RMI generates reasonably big stub and skeleton files. But, isn't that worth the simplicity?

Qusay H. Mahmoud is a senior software engineer at The School of Computer Science at Carleton University, Ottawa, Canada. Before joining Carleton University, he worked as a software designer for Newbridge Networks. Qusay holds a B.Sc. in data analysis and a master's degree in computer science, both from the University of New Brunswick, Canada. You may reach Qusay at dejavu@acm.org.