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.Remotethat 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 isUnicastRemoteObject.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).
Unlike their counterpart methods in ForumComm, these methods do indeed throw RemoteException. If/when this happens, the RemoteException will propagate to the RMI clientโs ForumComm to the point where the remote method call was made.
Class ForumRMIServer
The main server class is ForumRMIServer. As such, this class has the responsibility for setting up and maintaining the articles database, creating and setting up a copy of ForumRMIServerComm, and setting up the RMI registry.
import java.net.*;
import java.util.*;
import java.io.*;
import java.rmi.*;
import java.rmi.registry.*;
public class ForumRMIServer {
static final String DEFAULT_FORUM_CONFIGFILE = "forum.cfg";
static final String CONFIGFILE_THREADS = "threads";
static final String CONFIGFILE_DATABASE = "database";
Hashtable articles = new Hashtable ();
String configFilename, dbFilename;
Properties config = new Properties ();
Vector threads = new Vector ();
ForumRMIServerComm serverComm;
void init (String configFilename) throws Exception {
this.configFilename = configFilename;
loadConfig ();
loadArticles ();
initRMI ();
printConfig ();
}
There is no explicit constructor for ForumRMIServer. The init () method is called by the main () method to perform initialization functions for the server. This approach serves to group all the initialization operations in one place and to prevent the constructor from blocking on the initRMI () method.
void loadConfig () throws IOException {
if (! (new File (configFilename).exists ())) {
throw new IOException ("Config file " + configFilename + " not
found.nSpecify a an alternate config
file or provide a file named " +
DEFAULT_FORUM_CONFIGFILE + ".n");
}
BufferedInputStream BFin;
BFin = new BufferedInputStream (new FileInputStream (configFilename));
try {
config.load (BFin);
} catch (IOException ex) {
throw new IOException ("Error reading config file!");
} finally {
if (BFin != null)
BFin.close();
}
String t = config.getProperty (CONFIGFILE_THREADS);
if (t == null || t.equals("")) {
throw new RuntimeException ("You must define at least one discussion
thread in the configuration file.n");
}
StringTokenizer strtok = new StringTokenizer (t, ",");
while (strtok.hasMoreTokens())
threads.addElement (strtok.nextToken());
}
The serverโs configuration is loaded from a serialized Properties object that is stored on the file system under the file name DEFAULT_FORUM_CONFIGFILE. The threads instance variable is used to keep track of the discussion threads defined in the configuration file. These threads are the only ones made available to clients.
void loadArticles () {
dbFilename = config.getProperty (CONFIGFILE_DATABASE);
BufferedInputStream BFin = null;
ObjectInputStream Oin = null;
Enumeration legalThreads = threads.elements ();
Hashtable readArticles = new Hashtable ();
try {
BFin = new BufferedInputStream (new FileInputStream (dbFilename));
Oin = new ObjectInputStream (BFin);
readArticles = (Hashtable) Oin.readObject ();
} catch (Exception ex) {
System.out.println ("Article database load from file " + dbFilename +
" failed.");
} finally {
while (legalThreads.hasMoreElements ()) {
Object key = legalThreads.nextElement ();
Object val = readArticles.get (key);
articles.put (key, (val != null) ? val : new Vector ());
}
try {
Oin.close ();
BFin.close ();
} catch (Exception ex) {}
}
}
Here we see object serialization at work. loadAllArticles () creates a FileInputStream from file dbFilename and attempts to read a Hashtable from it. If that is successful (that is, if no exception is thrown), only articles for discussion threads present in the threads listing are put into the articles database. Those that do not match are discarded. This approach allows for a discussion thread policy based on a listing of accepted threads in the configuration file.
void initRMI () throws RemoteException {
System.out.print ("RMI status: ");
serverComm = new ForumRMIServerComm (articles);
bindRegistry ();
if (!bindRegistry ())
System.out.println ("waiting for RMI registry to come up...");
while (!bindRegistry ()) {}
System.out.println ("RMI commlib bound to local registry.n");
}
boolean bindRegistry () {
try {
Registry registry = LocateRegistry.getRegistry ();
registry.rebind ("ForumRMIServer", serverComm);
} catch (RemoteException ex) {
ex.printStackTrace ();
return false;
}
return true;
}
The initRMI () method does all the setup for the serverโs RMI server functions. The first thing it does is create an instance of ForumRMIServerComm. Next, the method registers the instance with the RMI registry under the service name โForumRMIServerโ by calling the bindRegistry () method and blocking the main thread until its call to bindRegistry () returns true. This process allows time for the registry thread to come up and ensures that binding actually takes place.
bindRegistry () tries to bind the registry to the server. If the binding of โForumRMIServerโ to the registry succeeds, the method returns true. Otherwise, most notably in the case in which the registry thread has not yet come up, the method returns false.
initRMI () throws RemoteException, which allows us to signal the main thread to exit if an exception is generated in the construction of ForumRMIServerComm. An exception can occur because the superclass constructor can generate one.
void saveArticles () {
if (articlesIsEmpty ()) {
System.out.println ("");
System.out.println ("No articles to save to database file.n");
return;
}
BufferedOutputStream BFOut = null;
ObjectOutputStream Oout = null;
try {
BFOut = new BufferedOutputStream (new FileOutputStream (dbFilename));
Oout = new ObjectOutputStream (BFOut);
Oout.writeObject (articles);
System.out.println ("");
System.out.println ("Article database written to " + dbFilename + ".n");
} catch (Exception ex) {
System.out.println ("Article database write FAILED!");
} finally {
try {
Oout.close ();
BFOut.close ();
} catch (Exception ex) {}
}
}
The saveArticles () method is the counterpart to the loadArticles () method. First, a FileOutputStream is constructed to dbFilename. The articles database, which is a Hashtable, is streamed to the file and then the file is closed. Any exception generated during this process will be caught and cause a message to be displayed to the serverโs console.
boolean articlesIsEmpty () {
boolean empty = true;
if (articles == null || articles.size() == 0)
return true;
Enumeration en = articles.keys();
while (en.hasMoreElements()) {
Vector threadArts = (Vector) articles.get (en.nextElement ());
if (threadArts.size() > 0) {
empty = false;
break;
}
}
return empty;
}
void printConfig () {
System.out.println ("");
System.out.println ("*** ForumRMIServer Configuration ***n");
System.out.println ("Discussion threads:n");
Enumeration en = articles.keys ();
while (en.hasMoreElements ())
System.out.println (en.nextElement ());
System.out.println ("");
System.out.println ("Database file: " + dbFilename + "n");
System.out.println ("Client requests:n");
}
These methods are just helper methods. articlesIsEmpty () checks to see if the articles database contains any articles, and printConfig () prints out configuration information to the serverโs console.
public static void main (String argv[]) {
ForumRMIServer server = new ForumRMIServer ();
Thread registry = new Thread () {
public void run () {
sun.rmi.registry.RegistryImpl.main (new String [0]);
}
};
registry.start ();
The main () method first creates its RMI communications library server. Then it puts the RMI registry in an anonymous subclass of Thread, using the anonymous class capability provided with JDK 1.1, and starts the registry.
try {
server.init ((argv.length > 0) ? argv[0] : DEFAULT_FORUM_CONFIGFILE);
while (true) {
try {
Thread.sleep (120000);
} catch (InterruptedException ex) {}
server.saveArticles ();
}
} catch (Exception ex) {
System.out.println ("Server shut down abnormally:");
ex.printStackTrace ();
} finally {
registry.stop ();
System.out.println ("RMI registry thread stopped.");
}
}
}
The main () method then initializes server, which may block on waiting for the registry to become operational in the registry thread. After server initialization, the main thread enters an endless delay loop, which writes the articles database to disk every two minutes.
Stub and skeleton classes
After you compile the above classes, you have to create the RMI stubs using the JDKโs rmic utility from the RMI server class ForumRMIServerComm. The stub classes provide the actual implementation for the underlying RMI functionality.
To generate the stub classes, issue the command rmic ForumRMIServerComm in the directory that contains ForumRMIServerComm. If for some reason rmic complains that it canโt find the class, you can specify your classpath with something like this:
rmic -classpath /usr/local/java/lib/classes.zip:.ForumRMIServerComm
Starting the server
Starting the server is as simple as issuing the command java ForumRMIServer. Because the Forum server starts the registry itself, there is no need to start the registry from the command line.
Conclusion
RMI is a great abstraction for communicating between Java virtual machines. Itโs not as efficient as a direct sockets-based method, but it sure is elegant. And speaking of elegance, object serialization takes the cake. These are just a couple of features of 1.1 that you can use to provide nicer client/server functions in your apps.


