Networking our whiteboard with Java 1.1

Find out how easy it is to network our whiteboard with servers, sockets, and RMI

Our world grows smaller with each passing day. Corporate alliances span the globe, and, as businesses move into increasingly remote locations, a growing number of working stiffs commute via telnet. In the past, coordinating the activities of such globally de-centralized communities was a challenge to say the least, but with the advent of the Internet and the opportunities it's given rise to, all that has changed. With utilities like our whiteboard (fully ramped-up with the networking portion of the app), interaction among users in different cities (or even different countries) is a snap.

We developed the graphical aspects of the whiteboard last month (shown below), so now we're ready to add the networking features to the application. We are going to accomplish this by networking the ObservableList class. As we discussed last month, this class creates a list of the displayed elements of the whiteboard. If you need a refresher, take this opportunity to reread part 1.

The graphical whiteboard

Our whiteboard tools manipulate the whiteboard by altering the list of elements. When changes are made to the list, all interested listeners are notified. Listeners, such as the display area, can then automatically update themselves to reflect these changes.

To enable networking, we will simply replace ObservableList with a distributed ObservableList, which ensures changes are propagated over the Internet. The distributed list presents exactly the same API as the original local list and so requires no changes to (or knowledge of) any parts of the whiteboard.

In the course of this article, we will look at two different approaches to networking -- socket-based and RMI-based -- and examine the pros and cons of each approach.

The new list class

Our first step is to implement a new list class called an

IDList

. This class is essentially a combination of the traditional

Hashtable

and

Vector

classes; it is an ordered list (

Vector

) of identifier-element pairs (

Hashtable

). As objects are added to the

IDList

, they are automatically assigned an associated identifier. The list can then be manipulated by addressing the elements by their identifiers (instead of the traditional mode of addressing the elements by their index number, which can change if any element is removed or inserted).

The IDList class

When we network the IDList class, the element identifiers will be distributed to all connected whiteboards. The whiteboards can then communicate coherently about elements of the list through the use of their identifiers.

Class IDList

The interface that class IDList presents is fairly rudimentary; however, it is sufficient to serve our purposes. Let's quickly review the methods available to this class:

  • public Object addElement (Object element)

    This method adds an element to the list, assigns it a VM-unique identifier (based on the VM hashcode of the element) and returns this identifier.

    The effects of replaceElement()
  • public Object replaceElement (Object oldID, Object element)

    This method replaces the old element identified by oldID with the new element element. The new element is added to the end of the list and the identifier that it is allocated is returned. If the specified old element was not present in the list, this method returns null without adding the new element.

  • public void addElementWithID (Object id, Object element)

    This method adds an element to the list, assigning it the specified identifier id. In a networked whiteboard, the central server will allocate identifiers and so the clients will use this method to add elements to their local lists.

  • public boolean replaceElementWithID (Object oldID, Object id, Object element)

    This method replaces the old element identified by oldID with the new element element and a pre-assigned identifier id. The new element is added to the end of the list. The method returns true if successful and false otherwise.

  • public Object getIDOfElement (Object element)

    This method returns the identifier associated with the specified element, or null if the element is not present.

  • public Enumeration elements ()

    This method returns an Enumeration of the elements in this list.

  • public Object clone ()

    This method returns a clone of the list.

  • public int getUpdateCount ()

    This method returns a count of the number of modifications that have been made to this list.

I won't dissect the code; it is relatively straightforward. The only interesting piece is our use of System.identityHashCode(Object) to allocate VM-unique identifiers for each element (based on the object's internal VM handle). We could have simply used an incrementing counter to allocate identifiers; however, this approach is far more interesting.

Networking the whiteboard with sockets

Our first approach to networking the whiteboard is based entirely on TCP/IP

Socket

s and

ServerSocket

s. For a primer, look back to my article "

Stepping through a site navigator applet

," which explored writing a networked chat system. The main change from that implementation is the use of object streams (

ObjectOutputStream

and

ObjectInputStream

) to provide message encapsulation rather than the simple UTF-encoded string encapsulation employed by the chat system.

Essentially, a central multithreaded TCP/IP server will maintain a master IDList, which contains the contents of the whiteboard. When a client first connects, he or she is sent a copy of this central display list. Thereafter, all changes to the whiteboard display list are relayed through the central server. So, when a whiteboard tool manipulates the whiteboard, its modifications are sent to the central server, which validates the changes and then broadcasts the result to all connected clients.

The socket-based distributed list

Class DListClient

The DListClient class implements the client-side of our distributed list. It is basically an extension of the ObservableList class, which opens a Socket to a central server and translates all client-side calls to manipulate the display list into messages that are sent to the server. A separate thread receives messages from the central server, updating the local display list accordingly. As expected, as messages arrive from the server, the registered update listeners are notified of any changes.

The implementation of this class is comparatively simple: After setting up the sockets and streams, client-side calls to update the list all essentially follow this form:

// ObjectOutputStream objectOut;

public synchronized void addElement (Object element) { try { objectOut.writeObject (new AddElementMsg (element)); objectOut.flush (); } catch (IOException ignored) { } }

Here, the client wishes to add an element to the list. We transmit an appropriate message to the server, flush the output stream, and return. For simplicity, we simply ignore any exceptions that may occur.

The listening thread is similarly simple:

// ObjectInputStream objectIn;

void processMsgs () throws IOException, ClassNotFoundException { while (true) { DListMsg msg = (DListMsg) objectIn.readObject (); if (msg instanceof ElementAddedMsg) { Object id = ((ElementAddedMsg) msg).getID (); Object element = ((ElementAddedMsg) msg).getElement (); list.addElementWithID (id, element); fireUpdate (); } ...

We loop, receiving messages from the server, which we then decode and execute. In this case, we receive a message indicating that someone (possibly us) has added an element to the central list. We add the element with the server-assigned identifier to our local copy of the display list and call fireUpdate() to notify listeners of the change.

Class DListServer

The server side of the socket-distributed list, class DListServer, follows the form of a typical multithreaded server. A main thread opens a ServerSocket, which then waits for client connections. For every connection that arrives, we create a new Thread which independently processes the connection. Each thread receives messages from the remote client, applies these to the central display list, and then broadcasts appropriate modification messages to all associated clients. So, the main loop has roughly the following form:

// ObjectInputStream objectIn; // IDList list;

void processMsgs (ObjectInputStream objectIn) throws IOException, ClassNotFoundException { while (true) { DListMsg msg = (DListMsg) objectIn.readObject (); ... if (msg instanceof ReplaceElementMsg) { Object oldID = ((ReplaceElementMsg) msg).getID (); Object element = ((ReplaceElementMsg) msg).getElement (); Object id = list.replaceElement (oldID, element); if (id != null) broadcastMsg (new ElementReplacedMsg (oldID, id, element)); } else ...

Here, we receive a request to replace an element on the list. We attempt to apply this change to the central list; if we succeed, we broadcast a message to all clients indicating the change they should perform and the identifier of the new element. Two clients attempting to replace an object at the same time is one obvious point of failure. In this case, the first message recieved by the server is executed and broadcast, the second is ignored.

Note that we don't expend any effort in reusing threads or minimizing the critical sections of our code. We don't expect a great number of people to be connected to the whiteboard at one time, so it is not necessary that our server be bulletproof. In a critical production environment where we had more breadth on the space-time continuum, we might look into those issues a bit more.

The messages

The messages that we send are all encapsulated as Objects that can be easily transmitted by the object streams. The following classes comprise the messages:

  • DListMsg

    DListMsg, which extends Serializable and declares no methods, is the superinterface of all messages. All messages must implement this interface and, thus, by inheritance must also implement Serializable, which means that they can be transmitted by the object streams.

  • InitMsg

    InitMsg is the first message sent from the server to a client. It contains the current display list.

  • AddElementMsg

    A client sends this message to the server when it wishes to add an element to the display list. The message contains the new element to be added.

  • ElementAddedMsg

    The server sends this message to all clients in response to an AddElementMsg. The message contains the new element, as well as its unique identifier. Upon receipt of this message, clients can add the new element to their display lists.

  • ReplaceElementMsg

    A client sends this message to the server when it wishes to replace an element of the display list. The message contains the new element, as well as the old element's identifier.

  • ElementReplacedMsg

    The server sends this message to all clients in response to a successful ReplaceElementMsg. The message contains the identifier to be removed, the new element, and its identifier.

  • QuitMsg

    The client sends this message to the server before it quits, allowing the server to perform a more graceful shutdown of the network connection.

Using the socket-distributed list

To apply this distributed list to our whiteboard, we simply modify the WB class to create an instance of DListClient in place of the original ObservableList. This client connects to DListServer, which must be running on the machine from which our applet originates.

For convenience, I include such a modified whiteboard class, called SocketWB, in the source distribution. Before you can put to use what you've learned thus far, you must first compile all of the source files. Remember, this uses lots of JDK 1.1 features, so you must compile with JDK 1.1. Here's what you need to do:

  1. Download the complete source, which is located in the Resources section of the article, and unzip (or untar, depending on which configuration you're using) the file.

  2. Change to the step directory.

  3. Compile the code using the following command:

    javac org\merlin\step\dec\*.java org\merlin\step\dec\socket\*.java

(If you're using Unix, be sure to use the forward slash and not the backslash in this command sequence.)

Next, you must start the server running on the machine that serves your Web pages. You must have a Web server, and you must serve the whiteboard as an applet from here. That's not 100 percent true; if you know what you're doing you can explore other options, but this is the easy route.

  1. Change to the step directory.

  2. Enter the following command:

    java org.merlin.step.dec.socket.DListServer

Finally, view the applet. You can perform this step from anywhere on the Internet that can create a direct socket connection to the server. Simply enter the following command:

appletviewer http://server/step/socket.html

If all goes well, you should be able to have several clients merrily drawing away on a whiteboard, all courtesy of our distributed list class.

Note: This applet will run only in the HotJava browser or Windows 95/NT implementation of Internet Explorer 4.0. If you are using either of these browsers, click on the link below to launch the applet.

http://www.javaworld.com/jw-12-1997/step/socket.html

If you are not using either of the above browsers, you can download the source from the Resources section and launch the applet locally using appletviewer.

In addition, because the applet attempts to create a socket connection with your machine, it may not succeed through some firewalls.

Now, let us do it all again....

Networking the whiteboard with RMI

Our RMI networking implementation is, in fact, quite similar to the socket-based implementation. The central server is replaced by a central list (a remote object) upon which we make remote method calls. When clients are first launched, they make a remote method call on the central list to retrieve the current display list. All subsequent manipulations of the client-side display list are also made as remote method calls on the central list.

Unfortunately, it is impractical for the central list to make remote method calls back to all clients in response to these changes. Instead, clients regularly poll the central list to determine whether any changes have been made. I'll discuss this approach more in my conclusion. For now, all you need to know is that a client thread polls the central list every ten seconds, updating its local IDList if the central list has been modified.

In essence, our resulting implementation consists of an interface (RList) that describes the remote interface to our central list, an actual implementation of this interface (RListImpl) that contains an IDList and resides on the server, and a client-side ObservableList subclass (RMIList) that proxies client-side changes into remote method calls on the central list.

The RMI-based distributed list

Interface RList

The remote interface to the central list, RList looks like a slimmed-down version of the actual IDList interface:

  • public Object addElement (Object element) throws RemoteException

    This method adds the specified element to the central list, returning the identifier that it was assigned.

  • public Object replaceElement (Object oldOD, Object element) throws RemoteException

    This method removes an element from the central list with the identifier oldID and adds the specified new element to the end of the list. The method returns the identifier assigned to the new element, or null if the original object was no longer present.

  • public IDList getUpdate (int updateCount) throws RemoteException

    This method returns a copy of the new central display list if it has been modified more recently than updateCount. If the list is unchanged, the method returns null. This is how the client polls for updates to the central list.

The polling interface is extremely simplistic. A better (but more complex) implementation would return a list of updates (rather than the complete list) to the central list since the specified modification time. The client could then apply each update in sequence to its own list. Obviously, if such a modification list were larger than the central list itself, a new copy could simply be returned, as is the case here. For didactic purposes, this simple interface is more manageable.

Class RMIList

The RMIList is our client-side ObservableList, which proxies any updates into remote method calls on the central list.

// RList rList; // IDList list;

public RMIList (String host) throws RemoteException, NotBoundException { Registry remoteRegistry = LocateRegistry.getRegistry (host); rList = (RList) remoteRegistry.lookup (RList.REGISTRY_NAME); list = new IDList (); update (); ...

When the client RMIList is created, we immediately obtain a remote reference to the central list. We first locate the registry running on the central server and then look up the distributed list that is registered there. We can then call our update() method to obtain the current display list.

  protected synchronized void update () throws RemoteException {
    IDList newList = rList.getUpdate (list.getUpdateCount ());
    if (newList != null) {
      list = newList;
      fireUpdate ();
    }
  }

Our update() method calls getUpdate() on the central list to determine if the display list has changed. If it has, we replace our old internal list and fire an update message to all registered listeners.

Elsewhere in this code, a separate thread takes care of calling this update() method every few seconds to pick up changes to the central list.

This next snippet shows an example of how we proxy local updates into calls on the central server.

  public synchronized void addElement (Object element) {
    try {
      Object id = rList.addElement (element);
      list.addElementWithID (id, element);
    } catch (RemoteException ignored) {
    }
  }

Here we remotely call the addElement() method on the central server. This process returns the identifier assigned to the element in the central list. We can then add the element to our local list with the appropriate identifier. The separate update thread will take care of updating the local list with any other changes that are made to the central list.

Class RListImpl

The RListImpl class, a remote object, is our central implementation of the RList interface. On the whole, the implementation is trivial; we simply maintain a central IDList against which client updates are applied. All that is required then is for us to register this RListImpl, so that clients can obtain a remote reference through which they can make their remote method calls.

We'll begin with our implementation of the addElement() remote method:

// IDList list;

public Object addElement (Object element) { return list.addElement (element); }

Here we simply call the corresponding method on our internal IDList.

Next, we implement the polling method:

  public IDList getUpdate (int updateCount) {
    return (list.getUpdateCount () == updateCount) ? null :
        (IDList) list.clone ();
  }

Here we return null if the client has an up-to-date version of the list (that is to say, the client and server lists have the same update count); otherwise, we return a copy of the central list. We clone the list for threadsafety.

Next, is the main() method:

  public static void main (String[] args) throws RemoteException {
    RListImpl rList = new RListImpl ();
    Registry localRegistry = LocateRegistry.createRegistry (Registry.REGISTRY_PORT);
    localRegistry.rebind (REGISTRY_NAME, rList);
  }

The main() method takes care of creating the central RListImpl and registering it with the local registry. We first use the LocateRegistry class to create an RMI registry running locally on the default RMI port. We then call rebind() on the resulting Registry to register our central list.

Using the RMI-distributed list

The RMI-based distributed list requires another trivial change to the WB class, which I have provided as the class RMIWB. The change essentially involves creating an RMIList with the address of the central list instead of just an ObservableList.

To compile this and get it up and running, follow these simple steps (remember, we're using lots of JDK 1.1 features, so you must compile with JDK 1.1):

  1. Again, download the complete source and unzip (or untar, depending on which configuration you're using) the file.

  2. Change to the step directory.

  3. Compile the code using the following command:

    javac org\merlin\step\dec\*.java org\merlin\step\dec\rmi\*.java

(If you're using Unix, be sure to use the forward slash and not the backslash in this command sequence.)

Next, we need to create the RMI stub and skeleton files. Here's how:

  1. Change to the step directory.
  2. Enter the following command:

    rmic org.merlin.step.dec.rmi.RListImpl

This process will create two files, RListImpl_Stub.class and RListImpl_Skel.class. If rmic deposits these files in your current directory (as opposed to the org\merlin\step\dec\rmi directory), then you must manually move the class files to the correct location. The rmic utility does not always cope with packages as it should.

Next, you must create and register the remotely accessible list on your Web server. Again, you must have a Webserver, and you must serve the whiteboard as an applet from here:

  1. Change to the step directory.
  2. Enter the following command:

    java org.merlin.step.dec.rmi.RListImpl

Finally, you can view the RMI-based collaborative whiteboard using the HotJava browser, IE 4 Windows 95/NT implementation platform from anywhere on the network. Simply enter the following command on your client machine:

appletviewer http://server/step/rmi.html

Comparing the different networking approaches We've reviewed two different ways to network the whiteboard. Now that you're familiar with both techniques, I'll briefly summarize some of the strengths and weaknesses of each approach.

Socket-based

The socket-based implementation enforces strict lock-step among the clients by relaying all updates to the central display through the server. The advantage of this technique is that updates to the list are displayed in real-time by the clients. The disadvantage is that all clients require a direct TCP/IP connection to the server, which may not be possible in an environment with firewalls and other constraints.

Our use of object streams to encapsulate messages provides us with a much cleaner interface than using raw bits and bytes; however, there is still some effort required to implement a multithreaded server that can handle network failures, among other potential disasters.

RMI-based

The RMI-based environment, on the other hand, allows clients to proceed on their merry way independent of the central list. If a client makes an update to the list, the server will inform it whether or not the update was approved; however, the server will not tell the client what other changes have been made (although it could). Instead, it is up to each client to regularly poll the server -- either manually or automatically -- for updates from other clients.

RMI usually requires a direct socket connection to the central server, but it also has support for HTTP proxying of requests should that be necessary. As a result, our RMI-networked whiteboard can operate behind a firewall where HTTP traffic is permitted or proxied.

For this particular application, the socket-based solution is, in general, superior. RMI, however, has the advantage of being fairly simple, and in an environment where real-time update is not a necessity, an RMI-based distributed list is far more elegant than the socket-based approach. Note that our RMI list has no knowledge of when clients connect or disconnect, of threads, or of network failures; it must simply respond to method calls that are made by the RMI substructure. For real-time updates, however, the overhead of clients constantly setting up network connections to poll the server is most undesirable.

An alternative to RMI with polling would be to implement a thoroughly RMI-based ObservableList. Updates from clients would be made through RMI calls to the server, and these updates would then result in the server making RMI calls back to all the registered listeners -- no matter where on the network they are. Although interesting in concept, this approach has a few failings: Unlike our current RListImpl, the RMI-based server would have to maintain a list of all active clients, making the solution less elegant. Furthermore, and this is what makes this approach useless to our applet-based whiteboard, the clients would then have to expose themselves as remote objects that could be accessed by the server. This goes against the security model imposed by most browsers, and, thus, would require that the clients be digitally signed and trusted by the users -- an undesirable burden.

Conclusion

Hopefully you have gained some useful insight not only into servers, sockets, and RMI, but also into design methods. By keeping our whiteboard loosely coupled, we were able to do some interesting things without major repercussions: We networked the system without changing any of the original whiteboard code, and we can further extend the whiteboard without having to go back and make major modifications.

Another interesting aspect of our implementation is that we didn't use a completely dedicated networking layer for the whiteboard. Instead, we used a fairly generic data structure: a distributed, observable list, which can be easily reused in other projects. We've kept our class structure uncoupled, so the list knows as little about the whiteboard as the whiteboard knows about the list, and each can be used and altered in an independent and useful manner. :END_BODY

Merlin Hughes watches television. America watches television. We all watch television.

Learn more about this topic

  • Download this article and the complete source code a gzipped tar file http://www.javaworld.com/javaworld/jw-12-1997/step/jw-12-step.tar.gz
  • Download this article and the complete source code a zip file http://www.javaworld.com/javaworld/jw-12-1997/step/jw-12-step.zip
  • Read my article "Stepping through a site navigator applet" (JavaWorld - January 1997), which provides a primer on sockets http://www.javaworld.com/javaworld/jw-01-1997/jw-01-chat.html
  • Object serialization specification http://www.javasoft.com/products/jdk/rmi/doc/serial-spec/serialTOC.doc.html
  • Learn more about RMI and Java distributed computing http://www.javasoft.com/features/1997/nov/rmi.html
  • JavaSoft's RMI Web page http://www.javasoft.com/products/jdk/rmi/
  • Previous Step by Step articles

Join the discussion
Be the first to comment on this article. Our Commenting Policies