Implementing Multiple Remote GUIs with RMI

Copyright © Gordon R. Durand, 1999-2000
Technical or typographical errors? Please email me.

You can download the code for this article.

Introducing RMI

The Remote Method Invocation API allows Java objects to send messages to Java objects running in other Java Virtual Machines anywhere on a TCP/IP network.

Declare the remote methods in an interface which extends the Remote interface

import java.rmi.*; public interface HelloIntf extends Remote { public String getGreeting() throws RemoteException; }
Implement the interface in a class that extends UnicastRemoteObject

import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; public class HelloRmt extends UnicastRemoteObject implements HelloIntf { public HelloRmt () throws RemoteException {} public String getGreeting() { return "Hello from HelloRmt!"; } public static void main (String[] args) { HelloRmt remote; System.setSecurityManager(new RMISecurityManager()); try { LocateRegistry.createRegistry(Registry.REGISTRY_PORT); remote = new HelloRmt(); Naming.rebind("HelloRmt", remote); } catch (Exception e) { e.printStackTrace(); } System.out.println("HelloRmt bound in registry"); } }
Notice that HelloRmt's constructor throws a RemoteException, but does nothing else. HelloRmt's main method does all the work. First we install a SecurityManager. Then we create a Registry object (most RMI examples run the registry in a separate process, but it's not necessary). Finally, we create the remote object and bind it to a name in the Registry.

Now we need a client to test the remote object

public class HelloRMI { public static void main (String[] args) { HelloIntf remote = null; try { remote = (HelloIntf) Naming.lookup ("HelloRmt"); System.out.println(remote.getGreeting()); } catch (Exception e) { e.printStackTrace(); } } }
HelloRMI never actually gets instantiated. A static main method does all the work. First it calls Naming.lookup, which returns a reference to a Remote type. It casts that to the HelloIntf type, and call its getGreeting method. The RMI implementation does the rest, calling the remote object's getGreeting method, serializing the returned String, and delivering it back to HelloRMI.

Compile the classes with javac

>javac *.java
Generate the stub class with rmic

>rmic -v1.2 HelloRmt
Create a text file called policy with your security policy

grant { // Allow everything for now permission java.security.AllPermission; };
Then start HelloRmt in its own window

>start "HelloRmt" java -Djava.security.policy=policy HelloRmt
Wait until you see the "HelloRmt bound in registry" message and then start HelloRMI

>java HelloRMI Hello from HelloRmt!
You can see that RMI can make remote objects appear to be local. RMI can also serialize objects (such as our return String) and send them over the network as the parameters and return values of remote methods.

You Want Fries With That?

RMI can do even more. It can request the bytecodes for the classes themselves. If an RMI call returns a reference to a class unknown to the local VM, the VM will first try to load the class from the local classpath. Failing that, it will request the class from the remote object's codebase, typically through an HTTP GET. This ability to ship an object's behavior along with the object itself makes RMI an extremely powerful tool for distributed applications.

You don't even need a full scale HTTP server. With Java, you can write your own class server with just a few lines of code.

A ClassServer would create a ServerSocket to listen on a specified port

server = new ServerSocket(port);
and then wrap itself in a Thread to wait for a request

private void newListener() { (new Thread(this)).start(); }
The heart of the ClassServer is in its run method

public void run() { Socket socket; try { socket = server.accept(); } catch (IOException e) { e.printStackTrace(); return; } newListener(); try { DataOutputStream out = new DataOutputStream(socket.getOutputStream()); try { BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); String path = getPath(in); byte[] bytecodes = getBytes(path); System.out.println("Sending " + socket.getInetAddress() + " " + path); try { out.writeBytes("HTTP/1.0 200 OK\r\n"); out.writeBytes("Content-Length: " + bytecodes.length + "\r\n"); out.writeBytes("Content-Type: application/java\r\n\r\n"); out.write(bytecodes); out.flush(); } catch (IOException ioe) { ioe.printStackTrace(); return; // execution will actually go to finally block first } } catch (Exception e) { out.writeBytes("HTTP/1.0 400 " + e.getMessage() + "\r\n"); out.writeBytes("Content-Type: text/html\r\n\r\n"); out.flush(); } } catch (IOException ex) { ex.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { } } }
On receiving a request, the ClassServer spins off a new Thread to continue listening, and then parses the input stream for a GET header and a class file name. It reads the class file and send the bytecodes, along with the appropriate header, as a response to the GET.

Skinny Client Fattens Up

Since RMI can request bytecodes when it needs them, a remote GUI only needs to know about a single remote method. Suppose a GUIServer implements

public interface GUIServerIntf extends Remote { public Panel getGuiPanel() throws RemoteException; }
A GUIClient only needs to request a GuiPanel and display it

Panel panel; try { panel = (Panel) guiServer.getGuiPanel(); } catch (RemoteException e) { e.printStackTrace(); } if (panel != null) { add(panel); panel.setVisible(true); }
When GUIClient request a GUIPanel, the GUIServer instantiates the class, serializes it (and all the objects it references), and returns a reference to GUIClient. GUIClient, finding itself with a reference to an unknown object which itself references other unknown objects, attempts to load the bytecodes for those objects. When it fails to find them in the local classpath, it GETs them from a remote ClassServer. The beauty of this scenario is that the GUIClient always gets fresh bytecodes in their latest revision, without the user ever needing to update GUIClient itself.

See Diagram 1: GUI Server

Setting Up Communications

Since GUIPanel brings its own cohorts and their behavior with it, it can look up Remote objects on the server to make calls home, and even set up a remote object on the client side to handle callbacks from the server. A couple minor problems must be overcome.

First, because GUIPanel's constructor is actually called on the server side (prior to it being serialized and sent to the client), you don't want to attempt to set up communications in the constructor. Rather, you need a setup() method that the GUIClient can call once GUIPanel gets to the client side. In order to do this you'll have to have your GUI panel extend RemoteGUI, which extends Panel

public abstract class RemoteGUI extends Panel public abstract void setup(String host);
GUIClient can cast the object reference to a RemoteGUI and call its setup method

try { panel = (RemoteGUI) guiServer.getGuiPanel(); } catch (RemoteException e) { e.printStackTrace(); } if (panel != null) { panel.setup(host); add(panel); panel.setVisible(true); }
Second, since GUIPanel extends RemoteGUI which extends Panel, it can't also extend UnicastRemoteObject, so it can't handle remote method calls itself. It might be better, anyway, to encapsulate that functionality in another class

public class GUIPanelRmt extends UnicastRemoteObject implements GUIPanelIntf { // ... all the gory details here.
And have GUIPanel create GUIPanelRmt in its setup method

public void setup(String host) { try { rmt = new GUIPanelRmt (this, name, host); } catch (RemoteException e) { e.printStackTrace(); } }

Handling Multiple Clients

In order for a server to handle multiple GUIs we must first give each new instance of GUIPanel a unique name

static int panelCount = 0; public Panel getGuiPanel() throws RemoteException { String name = "GUIPanel" + panelCount++; return new GUIPanel(name); }
Then, in GUIPanel's setup routine, when we call GUIPanelRmt's constructor, GUIPanelRmt can register itself in a local registry with its unique name

Naming.rebind(name, this);
and then call a remote method on the server side

String localHost = (InetAddress.getLocalHost()).getHostAddress(); guiServer.addGUI(localHost + "/" + name);
which will add its remote reference to a Vector of remote references

private Vector guis; public void addGUI(String name) throws RemoteException { try { Object gui = Naming.lookup ("rmi://" + name); guis.add(gui); } catch (Exception e) { e.printStackTrace(); } }
Finally, whenever the server side object's state changes, it broadcasts a message to all registered GUIs

public void updateGUIs(int len) { GUIPanelIntf gui; Iterator i = guis.iterator(); while (i.hasNext()) { gui = (GUIPanelIntf) i.next (); try { gui.updateGUI(len); } catch (RemoteException e) { i.remove(); } } }
Notice that if we get an exception in the broadcasting loop, we simply remove that reference from our Vector, as a quick-and-dirty way of handling GUI termination without implementing a removeGUI method.

See Diagram 2: Multiple Remote GUIs

A Complete Demonstration

You can download the code for a complete demonstration of a GUIServer/GUIClient which supports multiple remote GUIs. On the server side we have a GUIServer and a ClassServer. On the client side we start with a simple GUIClient. The server side has a JBoard object which interacts with remote JBoardGUls. The adapter classes which actually handle the RMI methods are JBoardRmt on the server side and JBoardGUIRmt on the client side.

The GUI has a slider which adjusts a value in a text box. Changing the value in the JBoardGUI panel sends setLength messages via JBoardGUIRmt to JBoardRmt, which passes it on to JBoard. JBoard then sends a updateGUls message to JBoardRmt, which broadcasts updateLength messages to all GUIs.

See Diagram 3: Remote GUI Classes

Notes

The java commands to start up GUIServer and GUIClient require some additional options. To simplify things, we put these commands into DOS batch files. To simplify further, we can create a shortcut to the batch files.

Building the GUIServer and GUIClient class files involves a number of steps, including running the javac compiler, copying files, and running the rmic stub compiler. To simplify things, we put these commands into a batch file.

If calls to Naming take longer than one or two seconds, it's not Java, it's a DNS lookup timeout problem. Edit your \winnt\system32\drivers\etc\hosts file to specify IP addresses for each machine involved.

Diagrams

Diagram 1: GUI Server       return to text

Diagram 1: GUI Server

Diagram 2: Multiple Remote GUIs       return to text

Diagram 2: Multiple Remote GUIs


Diagram 3: Remote GUI Classes       return to text

Diagram 3: Remote GUI Classes