AN
INTRODUCTION TO RMI
The Remote
Method Invocation API is a very powerful tool when it comes to implementing
client/server communication in Java. It eliminates the need for creating
protocols by the passing of Java objects and primitives between Virtual
Machines. This ability allows the developer to create methods that facilitate
communication between the client and server transparently. All of this
functionality does require some initial setup, and this article will present
how to create a simple client/server using RMI.
Before introducing
the code, some of the underlying RMI architecture must be discussed. The
most important layer that the novice developer must be concerned with
is the Stub/Skeleton layer. This layer provides the blueprints of the
client and server methods that the lower layers will use to actually communicate.
Stubs are located on the client-side, and handle the initialization of
methods called by the server, along with sending parameters. Skeletons
reside on the server-side, and they deal with the receipt of parameters
and returning values to the client. Both skeletons and stubs are created
with the RMI compiler, rmic, which is used on both the client and server
classes after they have been compiled. Examples will be given later on.
The client/server
example presented in this article is a very simple library system, where
the clients connect to the server and can either check in or out the available
books. In order to implement this system, we first need one interface
for the client and one for the server, where both extend the empty interface
java.rmi.Remote. They each define the methods that the actual client and
server must implement. In addition, each method must declare that it throws
java.rmi.RemoteException , the superclass of all RMI exceptions.
Here is LibraryClient.java :
import java.rmi.*;
import java.util.*;
public interface LibraryClient extends Remote {
void setBooks(Vector books) throws RemoteException;
}
The method setBooks()
is called by the server to update the book lists of all active clients.
And here is LibraryServer.java:
import java.rmi.*;
import java.util.*;
public interface LibraryServer extends Remote {
void processRequests(LibraryClient client) throws
RemoteException;
void stopClient(LibraryClient client) throws RemoteException;
Vector getBooks() throws RemoteException;
void checkin(String title) throws RemoteException;
void checkout(String title) throws RemoteException;
}
The method processRequests()
tells the server that a particular client wants its requests processed.
stopClient() kills a client's connection to the server and removes it
from the server's list of attached clients. Clients call getBooks() to
retrieve the list of books from the server. The methods checkin() and
checkout() actually change the book status on the server-side. Now the
concrete client and server classes must be implemented, and like any class
that implements an interface, it must implement all of that interface's
methods. Explanations will be given for the methods dealing directly with
RMI calls, and the complete source code can be retrieved from the link
at the end of this article. First, here is LibraryClientImpl.java:
import java.awt.*;
import java.awt.event.*;
import java.rmi.*;
import java.rmi.server.*;
import java.util.*;
public class LibraryClientImpl extends Frame
implements LibraryClient, ActionListener, ItemListener {
public static final int DEF_PORT = 1099;
protected LibraryServer server = null;
protected static final int WIDTH = 400;
protected static final int HEIGHT = 200;
protected Vector vBooks = null;
protected Hashtable books = new Hashtable();
private List bookList = null;
private TextField title = null, author = null, publisher
= null, date = null, status = null;
public static void main(String args[]) {
try {
new LibraryClientImpl();
}
catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}
public LibraryClientImpl() {
super("Library Client");
connect("localhost");
try {
vBooks = server.getBooks();
} catch (RemoteException e) {
e.printStackTrace();
}
// setup remaining panels
constructLayout();
// handle window close requests
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) { close(0); }
});
this.pack();
this.show();
}
protected void connect(String sName) {
try {
UnicastRemoteObject.exportObject(this);
String serverName = "//" + sName + "/LibraryServerImpl";
server = (LibraryServer)Naming.lookup(serverName);
server.processRequests(this);
} catch (java.rmi.ConnectException ce) {
System.err.println("Error: server not started");
close(-1);
} catch (java.rmi.ConnectIOException cioe) {
System.err.println("Error: Cannot connect to server at" + sName);
close(-1);
} catch (Exception e) {
e.printStackTrace();
close(-1);
}
}
...
}
The most
important method here is connect() , which actually sets up the connection to the Library server. The first line
(after the try) exports the client (the remote object) to make it available
to receive incoming calls. The second line initializes the address of
the server in the URL format:
//host:port/servername
In this
example, the arbitrary port used is 1099, the host is the local machine,
and the server name is LibraryServerImpl. The third line retrieves an
instance of the server from the RMI registry using the address above.
Line four tells the server that this client is ready to have its requests
processed. The remaining lines take care of the errors that could occur.
A java.rmi.ConnectException is thrown when the RMI server has not been started, and java.rmi.ConnectIOException when there is a problem connecting to an instance of the server. This is all
there is to initializing the client connection to the server.
The server setup
is a little more complex, as the source code for LibraryServerImpl.java shows:
import
java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.util.*;
public class LibraryServerImpl extends UnicastRemoteObject
implements LibraryServer, Runnable {
public static final int DEF_PORT = 1099;
protected Hashtable clientTable = new Hashtable();
protected Thread notifier = null;
protected Vector vBooks = new Vector();
protected Hashtable books = new Hashtable();
public static void main(String[] args) {
int port = DEF_PORT;
try {
String url = null;
// Create and install the security manager
System.setSecurityManager(new RMISecurityManager());
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
LocateRegistry.createRegistry(port);
LibraryServer server = new LibraryServerImpl();
url = "//:" + port + "/LibraryServerImpl";
Naming.rebind(url, server);
} catch (ExportException ee) {
System.err.println("Error: server already started on
port " + port);
System.exit(-1);
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
} // end main
public LibraryServerImpl() throws RemoteException {
super();
loadBooks();
System.out.println("Books loaded");
}
public synchronized void processRequests(LibraryClient client)
throws RemoteException {
LibraryClient prevEntry = (LibraryClient)clientTable.get(client);
if (prevEntry == null) {
clientTable.put(client, client);
}
if (notifier == null) {
notifier = new Thread(this);
v notifier.start();
}
}
protected synchronized void removeClient(LibraryClient client) {
clientTable.remove(client);
if (clientTable.isEmpty()) {
Thread thread = notifier;
notifier = null;
thread.stop();
}
}
public void stopClient(LibraryClient client) throws RemoteException {
removeClient(client);
}
...
}
The first
thing to notice is that the server extends java.rmi.server.UnicastRemoteObject . This class defines a remote object that provides point-to-point communication
using TCP streams, and therefore all RMI servers must extend it. Two important
member variables are the clientTable, which contains all active clients,
and the notifier thread, which tells the server the are no more clients.
The server is initialized is in the main method which can take an optional
argument containing the port number that the server should start on. First,
the default security manager is set to a java.rmi.RMISecurityManager . Note that this manager cannot be used in applets since it loosens the security
model as to allow remote classes to be downloaded from remote systems.
Next, a new RMI registry is created on the specified port. Then, a new
instance of the server is constructed, and then the registry binds that
instance to the specified port using the same URL as in the client, minus
the host machine. The only major exception thrown during this process
is java.rmi.server.ExportException , which tells the user that the server has already been started
on the port specified.
The server's
constructor is used to initialize static objects to be referenced by the
client. The method processRequests() adds a particular client to the clientTable so all clients can be notified of
certain events. stopClient() is called by a client before it exits so the server can clean up
after that particular client.
Those are all the
steps to setup both the RMI server and clients. The remaining methods
are application specific. There is a pattern to these methods, and that
is all server methods that are called by the client must be defined in
the server's interface and also must throw java.rmi.RemoteException . The same is true for all client methods called by the server.
One last topic
must be covered, and that is the compilation procedure for the classes.
First, the interfaces for the client and server must be compiled with
javac . Next, compile the implementations of the client and server, also with javac. The final step is to compile the stubs and skeletons for the generated implementation
classes using rmic. The procedure for the example code is given below:
javac LibraryClient.java LibraryServer.java
javac LibraryClientImpl.java LibraryServerImpl.java
rmic LibraryClientImpl LibraryServerImpl
Remember
not to append .java to the class names in the rmic step! Also, in order to run the client, make sure the server is started before
the client.
The Remote Method
Invocation API is a very useful addition to the core functionality of
Java. It allows developers to implement distributed client/server systems
much faster and more reliably without the overhead of implementing connection
and parameter-passing protocols. Another benefit of using RMI is that
it is 100% Java, so any architecture that implements Java can use it.
In Java 1.2, RMI will be enhanced even more with the addition of secure
connections using Secure Socket Layer (SSL) and seamless integration with
CORBA objects using Internet InterORB Protocol (IIOP). All of these features
will make Java a viable choice when implementing enterprise-wide solutions
in the future.
Download the
source code for this article.
Please
send feedback to articles@thirdeyeconsulting.com
|