Govind is a software developer specializing in distributed-object computing and Java/legacy system integration. He can be contacted at [email protected].
Java's Remote Method Invocation (RMI) brings distributed-object computing to Java. With RMI, Java objects are not restricted to operating solely within the confines of the host JVM. Rather, they can freely roam the Internet, seeking remote object implementations to service their needs.
Simply having remote objects whose methods can be invoked by distributed client objects is not sufficient for complex interactions, however. For instance, applications supporting any kind of collaborative functionality need to implement peer-to-peer relationships between interacting client and server objects. But how do you enable the server object to communicate with the client object in a truly asynchronous manner? While there are several approaches to implementing peer-to-peer asynchronous communication, one of the more elegant techniques involves the use of callbacks.
Event-driven programming in languages like C has traditionally used function pointers to pass references to functions, which are then asynchronously invoked in response to an "event" (timers, mouse clicks, and the like). Java uses interfaces to give you access to the same functionality in an object-oriented world. Here, the interface defines the methods that may be invoked by any object with access to the interface. The real "functionality" is present within some other object that actually implements this interface and it is "called back" by the target object.
Implementing callbacks, however, is not without challenges -- particularly in a distributed-object environment like RMI. Callbacks are implemented in RMI by setting the client itself as an exportable remote object (that is, make it implement some remote interface), then registering a reference of this with the remote-server object. This way, the server can asynchronously invoke the remote methods of any connected client in the same way the client can asynchronously invoke the methods implemented within the remote-server objects.
In this article, I'll examine the intricacies of enabling true peer-to-peer RMI interaction and present a step-by-step approach to implementing callbacks. For more information on RMI, see "How Do I Use Java Remote Method Invocation from an Applet?" by Cliff Berg (DDJ, March 1997).
Implementing Callbacks
The steps required to implement RMI callbacks have much in common with those needed to implement a simple remote-server object. The key difference is that, with RMI, you also have to set up the client applet or application as a "remote object," and register its reference with the remote-server object.
To illustrate how this works, I'll implement an application using RMI callbacks. In this example, the remote-server object (QuoteServer.java) maintains an updatable list of quotes. The RMI client applet (QuoteApplet.java) can make additions to this list in an asynchronous manner at any time. In the process, I'll demonstrate the use of RMI callbacks by having the server object periodically select a quote from its list in random fashion, and update each of the connected client objects in an asynchronous manner.
Specify the remote-server interface. The remote-server interface serves to specify the functionality implemented by the server object. This functionality is then made accessible to a client via remote methods invocations.
The remote-server interface (see Listing Onelists two methods available to client RMI objects. The addQuote method is used by remote clients to update the list of quotes, and the setQClientInterface method is used by clients to "register" themselves with the server object.
Specify the remote-client interface. Any RMI client applet/application that exports itself as a remote object needs to implement a remote-client interface. This is what enables the server object to invoke the client functionality in an asynchronous manner via callbacks.
The refreshClient method (see Listing Two) defines how each remote-client object can be "notified" or updated by the server object asynchronously.
Develop the remote-server object by implementing the remote-server interface. If you have prior experience with RMI, you'll notice that nothing really new is happening in the implementation of the remote-server object in Listing Three. The server object maintains two updatable Vector objects: one of the quotes, and the other to maintain references to all the connected remote RMI client objects. The remote-server object also demonstrates the use of callbacks in making asynchronous updates to the client object.
Develop the client application or applet by implementing the remote-client interface. The mypackage.rmiexamples package in Listing Four illustrates how you can implement the client application/applet using the remote-client interface in step 2.
Compile the Java source files for the RMI client and server. Since the RMI client applet is sent across the Internet by the web server, it is necessary that the applet and all stub files and interfaces be accessible in some location under the document root directory of the web server; for example, create a directory "rmi" somewhere within the $DOCUMENT_ROOT for your web server. Since the Java classes -- including the RMI client and server -- are part of the package mypackage.rmiexamples, you will have to create the subdirectories $DOCUMENT_ROOT/rmi/mypackage and $DOCUMENT_ROOT/ rmi/mypackage/rmiexamples.
Assuming you have all the Java source files within $DOCUMENT_ROOT/rmi, you can compile them by going to that directory and executing Example 1. This process creates the package mypackage.rmiexamples containing the classes QClientInterface.class, QServerInterface.class, QuoteServer.class, and QuoteApplet.class.
Generate the stubs and skeletons for the RMI client and server objects. From within $DOCUMENT_ROOT/rmi, invoke Example 2, which generates the server skeletons and client stubs for the client and server remote objects within $DOCUMENT_ROOT/rmi/mypackage/rmiexamples.
QuoteServer_Stub.class and QuoteServer_Skel.class are the client stubs and server skeletons for the remote-server object. QuoteApplet_Stub.class and QuoteApplet_Skel.class are the same for the client-remote object.
Start the remote-server object. Start up the remote-server object from within $DOCUMENT_ROOT/rmi using java mypackage.rmiexamples.QuoteServer &.
Run the client applet after creating an HTML file for the RMI client. Since Java applets are embedded within an HTML file, make sure that you create an HTML file (say, rmi.html) to send the QuoteApplet.class to the browser or appletviewer.
Since support for JDK 1.1 is still evolving within the major browsers, test your client applet with the JDK 1.1 appletviewer. You should be able to bring up the RMI client applet with your appletviewer using appletviewer http:// hostname/rmi/rmi.html. Figure 1 shows an RMI client applet executing within the appletviewer.
Summary
Callbacks are not just an elegant computing paradigm -- they are also a prerequisite for enabling peer-to-peer asynchronous communication. Here, I've presented one way you can easily implement callbacks within RMI. Although there may be other mechanisms for implementing RMI callbacks, they all rely upon the basic principle of setting up the RMI client as an exportable remote object, then passing its reference to the remote-server object. Finally, note that RMI applications utilizing callbacks cannot be used across firewalls because RMI makes use of HTTP tunneling; to get through firewalls, remote-client object references cannot use the stateless HTTP protocol for persistence.
DDJ
Listing One
/** Defines the methods available to the client for remote invocation */package mypackage.rmiexamples; public interface QServerInterface extends java.rmi.Remote { public void addQuote(String quote) throws java.rmi.RemoteException; public void setQClientInterface(QClientInterface c) throws java.rmi.RemoteException; }
Listing Two
/** Defines methods available to server for remote invocation as callbacks */package mypackage.rmiexamples; public interface QClientInterface extends java.rmi.Remote { public void refreshClient(String q) throws java.rmi.RemoteException; }
Listing Three
package mypackage.rmiexamples;import java.rmi.*; import java.rmi.server.UnicastRemoteObject; import mypackage.rmiexamples.QClientInterface; import mypackage.rmiexamples.QServerInterface; import java.util.*; import java.rmi.registry.LocateRegistry; </p> public class QuoteServer extends UnicastRemoteObject implements QServerInterface, Runnable { private Vector clientele; //tracks all connected clients private QClientInterface myQClientInterface; private Vector quoteList; //maintains updatable list of quotes private QClientInterface myClientObj; private Thread clientThread=null; private static int counter=0; </p> public QuoteServer () throws java.rmi.RemoteException { super(); // create and initialize the quote list quoteList = new Vector(); quoteList.addElement("Climb mountains to see lowlands"); quoteList.addElement("If you want the rainbow, put up with the rain"); quoteList.addElement("Smooth seas do not make skilful sailors"); clientele = new Vector(); } public void setQClientInterface(QClientInterface c) throws RemoteException { synchronized (clientele) { clientele.addElement(c); } if (clientThread == null) { clientThread = new Thread(this, "clientThread"); clientThread.start(); } } private void doIt() { synchronized (clientele) { Vector backup = (Vector) clientele.clone(); int seed; while ((seed = new Random().nextInt()) <=0) ; seed = seed % quoteList.size(); String data = quoteList.elementAt(seed)+ ": "+ ++counter; for (int i=0; i < clientele.size(); i++) { myClientObj = (QClientInterface) clientele.elementAt(i); try { //update the client asynchronously via callback myClientObj.refreshClient((String) data ); } catch (RemoteException e) { System.out.println("client must have disconnected!"); //get rid of remote reference for disconnected client backup.removeElement(myClientObj); if (backup.size() <= 0) { //no more clients- so stop server thread clientele = (Vector) backup.clone(); Thread dummy = clientThread; clientThread = null; dummy.stop(); } } } clientele = (Vector) backup.clone(); } //end syncronization on clientele } public void run() { while (true) { try { //sleep for a second Thread.currentThread().sleep(1000); } catch (Exception e) { } doIt(); } } public void addQuote(String quote) throws RemoteException { synchronized (quoteList) { //update quote list quoteList.addElement(quote); } } public static void main(String[] args) { System.setSecurityManager(new RMISecurityManager()); try { System.out.println("QuoteServer.main: creating registry"); LocateRegistry.createRegistry(1099); System.out.println("QuoteServer.main: creating server"); QuoteServer myQuoteServer=new QuoteServer(); System.out.println("QuoteServer.main: binding server "); Naming.rebind("/QuoteServer", myQuoteServer); System.out.println("QuoteServer bound in registry"); } catch (Exception e) { System.out.println("Exception on binding QuoteServer"); System.out.println(e.toString()); } } }
Listing Four
package mypackage.rmiexamples; </p> import mypackage.rmiexamples.QServerInterface; import mypackage.rmiexamples.QClientInterface; import java.rmi.*; import java.rmi.server.*; import java.util.Random; import java.awt.*; import java.applet.*; public class QuoteApplet extends Applet implements QClientInterface { public QServerInterface srvQuote=null; </p> //the client uses jdk1.1 event handling mechanism void sendQuote_Clicked(java.awt.event.ActionEvent event) { try { //update the remote server object with a new quote srvQuote.addQuote((String)myQuote.getText()); } catch (Exception e) { System.out.println(e.toString()); } } public void init() { super.init(); try { //by exporting the client object, turn it into a //remote object UnicastRemoteObject.exportObject(this); } catch (Exception e) { System.out.println("Could not export client remote object"); System.out.println(e); } try { //obtain the remote server object's reference by interrogating the //rmi registry srvQuote = (QServerInterface) Naming.lookup("//"+getCodeBase().getHost()+"/QuoteServer"); srvQuote.setQClientInterface(this); } catch (Exception e) { System.out.println("Could not set client interface at server"); System.out.println(e.toString()); } } public void addNotify() { </p> //don't worry about the specifics of the //following code. All it does is create a GUI //for the rmi client super.addNotify(); setLayout(null); resize(1016,565); label1 = new java.awt.Label("Input Quote:"); label1.reshape(21,24,72,21); add(label1); myQuote = new java.awt.TextField(); myQuote.reshape(104,24,242,21); add(myQuote); sendQuote = new java.awt.Button("Send"); sendQuote.reshape(357,24,48,21); add(sendQuote); quoteList = new java.awt.TextArea(); add(quoteList); quoteList.reshape(36,60,371,125); //register listners Action lAction = new Action(); sendQuote.addActionListener(lAction); } java.awt.Label label1; java.awt.TextField myQuote; java.awt.Button sendQuote; java.awt.TextArea quoteList; public void refreshClient(String q) throws java.rmi.RemoteException { //this is the callback method implementation quoteList.append(q+"\n"); } class Action implements java.awt.event.ActionListener { public void actionPerformed(java.awt.event.ActionEvent event) { Object object = event.getSource(); if (object == sendQuote) sendQuote_Clicked(event); } } }
Copyright © 1998, Dr. Dobb's Journal