Increase the functionality in your distributed client/server apps

Create the most elegant communications and storage solutions for your client/server apps with RMI and object serialization

I've spread this series out over quite a long period of time (I must give my colleagues a chance to write, after all), so before we get started, you may want to review the previous columns in which we built and modified the Forum application.

In Part 1: Write your own threaded discussion forum, we built the 1.0.2 client application, and in Part 2: The communications and server components, we developed the client-side networking and the server for 1.0.2. In Part 3: Scale an application from two to three tiers with JDBC, we converted the server to a JDK 1.1 middleware layer that communicated with an SQL Server database via JDBC. The 1.0.2 client continued to communicate with the middleware layer via sockets. Take a moment to review these versions, and I'll wait for you here....

Introducing RMI and object serialization

Remote Method Invocation (RMI) is a new API offered in JDK 1.1 that allows for messaging between different Java virtual machines (JVMs), even if they are separated by a network. Although it's not as efficient as TCP sockets and is still a bit buggy, RMI is interesting because it's high-level, yet easy to use. We're going to use it to allow the Forum client to call methods on the Forum server across the network. Core RMI functionality relies heavily on object serialization, which we'll look at next, and is contained in the java.rmi and java.rmi.server packages.

Object serialization is a facility that enables objects to be "flattened" into and out of ObjectOutputStreams, so that they can be stored in a file, sent across a network, or any number of other things. This flattening is accomplished by attaching an ObjectOutputStream to an appropriate lower-level OutputStream, such as a FileOutputStream, and writing the object into it. An instance of ObjectInputStream is then used to resurrect the object from the corresponding lower-level InputStream. Object serialization is a wonderful capability because it enables us to deal with live objects as objects instead of having to constantly break them into parts manually to transport or store them. We're going to use object serialization to clean up the loading and saving of the Forum server's article database. Object serialization facilities are contained in the java.io package.

Moving the Forum to RMI

We'll now begin the process of moving the Forum app from sockets to RMI and implementing object serialization to store and retrieve the articles database. For the sake of simplicity, we'll use the code and functionality in part 2 of our series as the basis for the new version we're developing. First, let's take a more detailed look at what is involved in using RMI.

RMI in-depth

RMI is used to transport method calls from a local client to a remote RMI server, which is a special object located on a remote machine. The RMI server usually extends java.rmi.server.UnicastRemoteObject and must implement at least one programmer-defined interface that itself is derived from the java.rmi.Remote "marker" interface. The methods declared in the derived interface will be the methods that the RMI server will export.

The remote machine providing RMI services must be running an RMI registry, which handles incoming requests for methods. The registry provides a name service lookup that allows RMI clients to refer to RMI services using the syntax:

rmi://machine.domain.tld/<<resourcename>>

The registry can handle multiple RMI servers on the remote machine. By default, the registry listens to TCP port 1099 and can be run in either a process (that is, started on the command line with rmiregistry & or the equivalent) or in a thread started by an application that is providing an RMI service.

The following steps are necessary to create and provide an RMI service:

  • Derive an interface from java.rmi.Remote that contains the methods to be made available to RMI clients.

  • Define a class that extends the appropriate subclass of java.rmi.server.RemoteServer. In most cases, this class is UnicastRemoteObject.

  • Implement the derived interface in the derived class.

  • Compile the code.

  • Create stub and skeleton classes with the JDK rmic utility, and make sure they are accessible to the client and server, respectively.

  • Start the RMI registry on the local machine (unless you do this in your code).

  • Start the main application, which should instantiate the RMI server class and register it with the local registry.

Once the RMI service is available, the RMI client looks up the remote RMI server object and obtains a reference to it using the "rmi URL" syntax shown above. It then calls the remote object's methods directly.

Implementing the Forum API with RMI

The first step in implementing RMI and object serialization in the Forum is to revisit the 1.0 Forum client/server communication API. This API provides the service contract between the Forum server and its clients.

As before, the Forum client has a communications library, ForumComm. In the new version, this communications library is implemented with RMI calls to the server instead of socket-based messages. The server has a corresponding communications library called ForumRMIServerComm, which acts as an RMI server. Both of these libraries explicitly implement the interface ForumRMICommInterface.

The Forum RMI API is declared in interface ForumRMICommInterface. This interface consists of the same method calls present in the 1.0 interface. The only difference between the new ForumRMICommInterface and its predecessor is that the new version extends java.rmi.Remote and its methods are declared to throw RemoteException.

import java.util.*;
import java.rmi.*;
interface ForumRMICommInterface extends Remote {
  Hashtable loadAllThreads () throws RemoteException;
  Vector loadThreadArticles (String t) throws RemoteException;
  boolean postArticle (String art, String t) throws RemoteException;
}

Let's examine a few things about this interface. As I mentioned a moment ago, ForumRMICommInterface extends the java.rmi.Remote interface. Remote contains no method definitions; its sole purpose is to mark derived interfaces that contain methods to be exported by the RMI server. RemoteException is a subclass of java.io.IOException, which represents an I/O exception that occurs during the course of an RMI call. RemoteException is thrown at the server and propagates to the client, where it originates in the same manner as any other exception generated by a method call on the client.

The most exciting thing about ForumRMICommInterface is that it is not only an interface contract between client and server, it is a mandatory contract because both ForumComm and ForumRMIServerComm are declared to implement it. This approach keeps client and server APIs in synch.

Implementing the RMI client

The 1.0 Forum client is composed of three classes: ForumLauncher, Forum, and ForumComm. We'll leave ForumLauncher and Forum as is, and rewrite only the communications library ForumComm, which allows us to plug in different communications implementations without affecting the main client class.

Class ForumComm

Let's take a look at the revamped communications class.

import java.net.*;
import java.util.*;
import java.io.*;
import java.rmi.*;
class ForumComm implements ForumRMICommInterface {
  ForumLauncher grandParent;
  ForumRMICommInterface server;
  public ForumComm (ForumLauncher gp) {
    grandParent = gp;
    initRMI ();
  }
  void initRMI () {
    try {
      String rmiHost = grandParent.getCodeBase ().getHost();
      server = (ForumRMICommInterface) Naming.lookup ("rmi://" + rmiHost 
                                                       + "/ForumRMIServer");
      System.out.println ("Connected to RMI host " + rmiHost + ".");
    } catch (Exception ex) {
      System.out.println ("RMI setup failed.");
    }
  }

The main job of the ForumComm constructor is to call the initRMI () method, which sets up the RMI session with the RMI server. Because of browser security restrictions on applet connections, which we all know and love, the RMI server must be on the same machine that served the applet.

The java.rmi.Naming class provides methods for accessing remote RMI objects. In this case, we use Naming.lookup () to obtain a reference to the remote reference, which we cast to the appropriate interface ForumRMICommInterface. The location "rmi://" + rmiHost + "/ForumRMIServer" represents an RMI session with the RMI server registered on the applet's originating host under the name "ForumRMIServer".

If anything goes wrong with the RMI setup attempt, a simple error message is printed to the client console.

  // 1.0 API methods 
  public Hashtable loadAllThreads () {
    Hashtable a = new Hashtable ();
    try {
      a = server.loadAllThreads ();
    } catch (Exception ex) {
      System.out.println ("Error reading threads from server.");
      initRMI ();
    }
    return a;
  }
  public Vector loadThreadArticles (String t) {
    Vector ta = new Vector ();
    try {
      ta = server.loadThreadArticles (t);
    } catch (Exception ex) {
      System.out.println ("Error reading articles for thread '" + t  
                           + "' from server.");
      System.out.println ("Attempting to reset RMI.");
      initRMI ();
    }
    return ta;
  }
  public boolean postArticle (String art, String t) {
    try {
      if (server.postArticle (art, t))
        return true;
    } catch (Exception ex) {
      System.out.println ("Error posting article in thread '" + t  
                           + "' to server.");
      System.out.println ("Attempting to reset RMI.");
      initRMI ();
    }
    return false;
  }
}

The above methods are implementations for the methods declared in ForumRMICommInterface. ForumComm's method implementations are basically just simple wrappers for method invocations on the RMI server object.

Note that in the client we're not throwing RemoteException as the interface declares. We do this because we want to handle exceptions by trying to re-establish the RMI transport to the server instead of passing them on to Forum.

Implementing the 1.1 RMI server

The 1.0 Forum server is composed of two classes, ForumServer and ForumConnectionHandler. In this version, we can replace multiple connection handlers with a single instance of ForumRMIServerComm, which is constructed by ForumRMIServer. ForumRMIServerComm implements the ForumRMICommInterface and listens for RMI client calls to its methods.

Class ForumRMIServerComm

ForumRMIServerComm is the server's counterpart to ForumComm. It provides the methods that RMI clients call to manipulate articles stored in the Forum server's database.

import java.net.*;
import java.util.*;
import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
class ForumRMIServerComm extends UnicastRemoteObject 
                                   implements ForumRMICommInterface {
  Hashtable articles;
  public ForumRMIServerComm (Hashtable a) throws RemoteException {
    articles = a;
  }

In order to qualify as an RMI server, ForumRMIServerComm must extend java.rmi.server.UnicastRemoteObject. To make Forum methods available to RMI clients, ForumRMIServerComm must also implement ForumRMICommInterface, which, as I noted earlier, extends the RMI marker interface java.rmi.Remote. When ForumRMIServer constructs its copy of ForumRMIServerComm, the new ForumRMIServerComm receives a reference to the articles database.

  public Hashtable loadAllThreads () throws RemoteException {
    System.out.println ("loadAllThreads () called");
    Hashtable threads = new Hashtable ();
    Enumeration keys = articles.keys ();
    while (keys.hasMoreElements ())
      threads.put (keys.nextElement (), new Vector ());
    return threads;
  }
  public Vector loadThreadArticles (String t) throws RemoteException {
    System.out.println ("loadThreadsArticles (" + t + ") called");
    Vector arts = (Vector) articles.get (t);
    if (arts != null)
      return arts;
    else
      return new Vector ();
  }
  public boolean postArticle (String art, String t) throws RemoteException {
    System.out.println ("postArticle to thread " + t + " called");
    Vector threadArts = (Vector) articles.get (t);
    if (threadArts != null) {
      threadArts.addElement (art);
      return true;
    }
    else
      return false;
  }

Here we see the implementations for ForumRMICommInterface's methods. These methods access the articles database and return a Hashtable, Vector, and boolean, respectively. Because RMI uses object serialization, the RMI client ends up with a copy of any object returned by an RMI server method. This means that we can return a reference to a Vector that is contained in the articles database. Any modifications that the client chooses to make to the Vector will not reflect in the server JVM's original copy.

When multiple client threads access a central server data structure, it's important to pay attention to synchronization issues. In this case, we don't have to synchronize any of the API methods on articles because we're not deleting elements such as discussion threads or articles. Also, adding articles in postArticles () poses no problems because Vector itself is threadsafe. For basic information on threadsafe programming in Java, consult JavaSoft's online Java tutorial (a link is provided in the Resources section of this article).

Related:
1 2 Page 1
Page 1 of 2