Distributed Objects Programming Using Java RMI Qusay H. Mahmoud Introduction Developing Client/Server applications using sockets involves the design of a protocol that consists of a language agreed upon by the client and server. The design of protocols is hard and error-prone. One issue, for example, is deadlock. In a deadlock, processes never finish executing; these processes may be holding system resources, preventing other processes from accessing these resources. Instead of working directly with Sockets, Client/Server applications can be developed using Java's Remote Method Invocation. Java RMI is a package that can be used to build distributed systems. It allows you to invoke methods on other Java Virtual Machines (possibly on different hosts). The RMI system is very similar to (but more general and easier to use) than the Remote Procedure Call (RPC) mechanisms found on other systems, in that the programmer has the illusion of calling a local method from a local class, where in fact all the arguments are shipped to the remote target, interpreted, and results are sent back to the callers. One distinguishing aspect of RMI is its simplicity. The set of features supported by RMI are those that are most valuable for building distributed applications, namely: transparent invocations, distributed garbage collection, convenient access to streams. Remote invocations are transparent since they are identical to local ones, so their method signature is identical. Distributed garbage collection is an interesting feature of the JavaRMI system! The goals of the system as described in the Java Remote Method Invocation Specification, Prebeta Draft, Revision 1.1 Support seamless remote invocations on objects in different Java Virtual Machines Support callbacks from servers to clients Integrate the distributed object model into the Java language in a natural way while retaining most of the Java language's object semantics Make differences between the distributed object model and the local Java object model apparent. Passing objects by reference vs. passing objects by copying make writing reliable distributed applications as simple as possible Preserve the safety provided by the Java runtime system RMI and the OSI Reference Model The OSI Reference Model defines a framework that consists of seven layers of network communication. The figure below shows how RMI can be described by this model. The User's application is at the top layer, it uses a data representation scheme to transparently communicate with remote objects, possibly on other Java Virtual Machine hosts. The RMI system itself consists of three layers: The Stubs/Skeletons Layer The Remote Reference Layer The Transport Layer The Stubs/Skeletons Layer serves as an interface between an application and the rest of the RMI system. It is purpose is to transfer data to the Remote Reference Layer via marshal streams. This is where Object Serialization comes in to play by enabling Java objects to be transmitted between address spaces. The Remote Reference Layer is responsible for carrying out the semantics of the invocation, it transmits data to the Transport Layer using connection-oriented streams - i.e. TCP (rather than UDP). The Transport Layer in the current RMI implementation is TCP-based, but a UDP-based tarnsport layer could be substituted. Finally, the Transport Layer is responsible for setting up connections, and managing those connections. Developing Client-Server applications using JavaRMI Writing Client-Server applications using JavaRMI involves six simple steps: 1. Defining a remote interface 2. Implementing the remote interface 3. Writing an application (or an applet) that uses the remote objects 4. Generating stubs (client proxies) and skeletons (server entities) 5. Starting the registry 6. Running the server and client Let's look at each step by developing a simple application as an example to help us in understanding each step. The sample application is an arithmetic server. The arithmetic server will perform four mathematical operations on arrays of integers: addition, subtraction, multiplication, and division. Each of these operations takes two arrays of 10 integers as arguments and returns an array after performing the operation. The first step is to define a remote interface for the remote objects. Why do we need an interface? The programmer of a client should be able to tell what operations the arithmetic server provides and how to use these operations just by looking at the interface we define. Defining the remote interface First, the remote interface for the arithmetic server is: // File: Arith.java public interface Arith extends java.rmi.Remote { int[] add_arrays(int a[], int b[]) throws java.rmi.RemoteException; int[] sub_arrays(int a[], int b[]) throws java.rmi.RemoteException; /* ... * and the same goes for mul_arrays and div_array */ } Note: the remote interface must be declared public, otherwise clients won't be able to load remote objects that implement that remote interface. The remote interface extends Remote. This is to fulfill the requirement for making the class a remote object. Also, note that each method must throws java.rmi.RemoteException. Now we can compile our interface: % javac Arith.java Implementing the remote interface The second step in the development life cycle is implementing the remote interface. // File: ArithImpl.java import java.rmi.*; import java.rmi.server.UnicastRemoteObject; //We are extending UnicastRemoteObject to indicate that ArithImpl is //being used to create a single remote object that uses RMI's default //communication transport. public class ArithImpl extends UnicastRemoteObject implements Arith { private String name; // constructor for the remote object // it must throw java.rmi.RemoteException public ArithImpl(String s) throws RemoteException { // Java constructs the super class before the class super(); name = s; } // implementation for the remote object add_array // Note that remote objects are passed by reference 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; } Note that the main() method needs to create and install a security manager. You can either use the default - RMISecurityManager, or one that you define yourself. The role of the security manager is to protect the host from malicious code from the client. public static void main(String argv[]) { System.setSecurityManager(new RMISecurityManager()); try { // create an instance of the remote object // once it is created it is ready to listen for client's requests ArithImpl obj = new ArithImpl("ArithServer"); // register the remote object // replace MachineName with the machine you're going to run the server and rmiregistry on. Naming.rebind("//MachineName/ArithServer", obj); System.out.println("ArithServer bound in registry"); } catch (Exception e) { System.out.println("ArithImpl err: " + e.getMessage()); e.printStackTrace(); } } } We can now compile our ArithImpl.java file: % javac ArithImpl.java Writing a client application that uses the above interface This program remotely invokes any of the ArithServer's methods, in this case we have only implemented add_array. Once the client invokes that operation, the client should receive some output - the sum of two arrays. The code for the client looks as follows: // File: ArithApp.java 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[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9}; int b[] = {2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; int result[] = new int[10]; try { // we need to get a refernece to the "ArithServer" from the server's // registry. Arith obj = (Arith)Naming.lookup("//" + localHost() + "/ArithServer"); // next, we invoke the remote object add_array and save the return value result = obj.add_array(a, b); } catch (Exception e) { System.out.println("ArithApp exception:"+e.getMessage()); e.printStackTrace(); } // print the saved results on the client's screen System.out.println("The sum of the arrays is: "); for (int j=0; j<10; j++) { System.out.print(result[j]+" "); } } } Now, we can compile our client: % javac ArithApp.java Generate Stubs and Skeletons Now we are ready to generate the stubs and skeletons. RMI stubs and skeletons are generated by the rmic compiler. A stub is a client-side proxy and a skeleton is a serverside entity that disptaches calls to the actual remote object implementation. Stubs and skeletons are determined and dynamically loaded as needed at run time. To generate the needed stubs and skeletons for our applications we type: % rmic ArithImpl This will generate the files: ArithImpl_Skel.class and ArithImpl_Stub.class. Note that your CLASSPATH should include a pointer to where all these classes we are compiling are stored. Before running our client and server application, we need to run the RMI registry, which is a name server that allows clients to obtain a reference to a remote object. Running the rmiregistry In a UNIX environment, he RMI registry can be started as follows: % rmiregistry & The registry, by default, runs on port 1099. If you would like to start the RMI registry on a different port number, specify the port on the command line, i.e. rmiregistry port &, where port is the port number you want to start the registry on. Note that if you start the registry on a port number other than the default, you will then have to specify the port number when you bind it - i.e., instead of saying: Naming.rebind("//MachineName/ArithServer", obj); you will need to say: Naming.rebind("//MachineName/ArithServer:port", obj); where port is the port number on which you are running the rmiregistry. Running the client and server Now we are ready to start our server and client. The server process can be started by typing: % java ArithImpl & Finally, we can run our client, by typing: % java ArithApp and this should result in the following output: The sum of the arrays is: 3 4 5 6 7 8 9 10 11 11 If you look at the client's code you will note that our two arrays were: int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9}; int b[] = {2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; Now, if you look at the results we got when we ran the client, you will note that the operation was performed correctly. We are getting correct results. Test it for yourself! :-) Conclusion: Developing Client/Server applications using sockets involves the design of a protocol that consists of a language agreed upon by the client and server. The design of protocols is hard and error-prone. Using Java Remote Method Invocation or JavaRMI, developing client-server applications is straight forward because remote invocations in RMI are identical to local ones, so their method signature is identical. About the author: Qusay H. Mahmoud is a graduate student in Computer Science at the University of New Brunswick, Saint John, Canada. This term he is teaching a course on Multimedia and the Information Highway at the university. As part of his thesis, he is developing a Webbased distributed computing system using Java. You can reach him at: qusay@scs.carleton.ca Copyright (c)1997 Qusay H. Mahmoud. All rights reserved.