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:
Figure A: RMI fits into the OSI Reference Model like this.
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 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.