Now we'll look at a collaborative application that's not so simple--a shared whiteboard. There are more things to worry about in this application because, while we still need to keep track of agent identities and communications, the data shared between agents is a bit more complicated and can lead to performance problems pretty quickly.
First let's build a simple shared whiteboard system based on our RMI collaborative classes. Like the chat example, our first version will use the standard mediator class, RMIMediatorImpl, because this version will only need the mediator to do the default routing of messages to the other agents. This initial whiteboard example will also need a new subclass of the RMICollaborator that acts as a whiteboard user. Actions that the user performs in the local whiteboard are broadcast to all other users so that their displays are updated properly.
Example 10-2 shows a WhiteboardUser class that subclasses RMICollaboratorImpl. It has a single constructor with four arguments: a name that the whiteboard user goes by, a color that is used when the user draws on the whiteboard, a host name for the mediator, and the mediator's name. The user name, host, and mediator name are passed to the RMICollaboratorImpl constructor to establish the connection to the mediator, and to initialize the collaborator's Identity. Once this is done, the color is added to the whiteboard user's Identity. We include the user's color in the Identity so other users of the shared whiteboard will know what color to use to draw our scribblings. When they receive the Identity in the remote call of their notify() methods, they can extract the Color object from the Identity and use it to draw the remote user's inputs. Since the java.awt.Color class implements the Serializable interface, we know that we can safely send the Color object through a remote method call via RMI.
package dcj.examples.Collaborative; import dcj.util.Collaborative.*; import java.awt.event.*; import java.awt.*; import java.util.Hashtable; import java.util.Properties; import java.io.IOException; import java.rmi.RemoteException; public class WhiteboardUser extends RMICollaboratorImpl implements MouseListener, MouseMotionListener { protected Hashtable lastPts = new Hashtable(); protected Component whiteboard; protected Image buffer; public WhiteboardUser(String name, Color color, String host, String mname) throws RemoteException { super(name); Properties p = new Properties(); p.put("host", host); p.put("mediatorName", mname); connect(p); getIdentity().setProperty("color", color); System.out.println("color = " + color.getRed() + " " + color.getGreen() + " " + color.getBlue()); buildUI(); } protected void buildUI() { Frame f = new Frame(); GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); f.setLayout(gridbag); f.addNotify(); c.fill = GridBagConstraints.BOTH; c.gridwidth = GridBagConstraints.REMAINDER; Canvas canvas1 = new java.awt.Canvas(); canvas1.setSize(240,180); canvas1.setBackground(Color.white); gridbag.setConstraints(canvas1, c); f.add(canvas1); String name = null; try { name = getIdentity().getName(); } catch (Exception e) { name = "unknown"; } Label label1 = new java.awt.Label("Your name: " + name); label1.setSize(100,30); gridbag.setConstraints(label1, c); f.add(label1); f.setSize(240,210); f.show(); whiteboard = canvas1; whiteboard.addMouseListener(this); whiteboard.addMouseMotionListener(this); buffer = whiteboard.createImage(f.getSize().width, f.getSize().height); } public void mousePressed(MouseEvent ev) { Point evPt = ev.getPoint(); try { broadcast("start", evPt); } catch (Exception e) {} } public void mouseReleased(MouseEvent ev) { Point evPt = ev.getPoint(); try { broadcast("end", evPt); } catch (Exception e) {} } public void mouseDragged(MouseEvent ev) { Point evPt = ev.getPoint(); try { broadcast("drag", evPt); } catch (Exception e) { } } public void mouseExited(MouseEvent ev) {} public void mouseMoved(MouseEvent ev) {} public void mouseClicked(MouseEvent ev) {} public void mouseEntered(MouseEvent ev) {} public boolean notify(String tag, Object data, Identity src) throws IOException, RemoteException { Color origColor = null; Color agentColor = null; Graphics gr = buffer.getGraphics(); try { agentColor = (Color)src.getProperty("color"); if (agentColor != null) { gr.setColor(agentColor); } else { System.out.println("No agent color available."); } } catch (Exception exc) { System.out.println("Exception while switching colors."); exc.printStackTrace(); } if (tag.compareTo("start") == 0) { lastPts.put(src.getName(), data); } else if (tag.compareTo("drag") == 0) { Point lastPt = (Point)lastPts.get(src.getName()); Point currPt = (Point)data; gr.drawLine(lastPt.x, lastPt.y, currPt.x, currPt.y); lastPts.put(src.getName(), data); } else if (tag.compareTo("end") == 0) { Point lastPt = (Point)lastPts.get(src.getName()); Point currPt = (Point)data; gr.drawLine(lastPt.x, lastPt.y, currPt.x, currPt.y); lastPts.remove(src.getName()); } whiteboard.getGraphics().drawImage(buffer, 0, 0, whiteboard); return true; } }
The last thing that the WhiteboardUser constructor does is build the user interface by calling the buildUI() method. This method assembles the AWT elements that make up the whiteboard interface for the user. We won't delve into the details of the interface, except to say that the main part of the whiteboard interface is a simple Canvas, to which we attach the WhiteboardUser itself as a MouseListener and a MouseMotionListener (notice that the WhiteboardUser class implements both of these AWT interfaces). This is done near the end of the buildUI() method.
All drawing operations are done using the nextLine() method. This method draws a line from the last point on the user's drawing path, which is stored in a Hashtable, to the next point; it's passed in as an argument. The color of the line to be drawn is passed in as a Color argument. The line is drawn first on an Image buffer, which was initialized in the buildUI() method, then the buffer image is copied to the Canvas. We do this so that we can restore the whiteboard display if the window becomes obscured by another window and becomes visible again; all of the scribblings are stored in the Image buffer and can be recopied to the Canvas when needed.
Since we've attached the WhiteboardUser to the Canvas as a MouseListener and a MouseMotionListener, it will get mouse click and motion events from the Canvas, passed to it through calls to its mouseXXX() methods. When the user presses a mouse button while the cursor is in the Canvas, the WhiteboardUser.mousePressed() method is called. If the user drags the mouse with the button pressed, the WhiteboardUser.mouseDragged() method is called repeatedly, recording each new position of the cursor. When the user releases the mouse, the WhiteboardUser.mouseReleased() method is called. Each of these methods is passed a MouseEvent object as an argument, which includes information about the event that triggered the method call. This information includes the position of the mouse within the Canvas.
To let the rest of the shared whiteboard users know what the local user has done (so they can update their displays), the mouse event-handling methods in WhiteboardUser broadcast a message to them all using the broadcast() method inherited from the RMICollaboratorImpl parent class. In mousePressed(), a "start" message is sent along with the coordinates of the mouse press (passed as the body of the message). This tells the other whiteboard users that this user has started drawing something at those coordinates. In mouseDragged(), a "drag" message is sent, with the coordinates of the mouse. mouseReleased() sends an "end" message with the coordinates of the mouse.
This gets the drawing actions of each whiteboard user to all of the other users; now we need to do something with this information. Remember that when the mediator calls the collaborator's notify() method, it passes in the message tag, the body of the message, and the Identity of the sender. The WhiteboardUser.notify() method first gets the drawing color of the remote agent from its Identity--remember that each agent adds its preferred color to its Identity in its constructor. The notify() method sets the drawing color of the buffer image by getting its Graphics object and calling its setColor() method with the remote agent's color. Next, it checks the message tag. If this is a "start" message, then it just stores the mouse location in a Hashtable, so that it will know where to draw a line when the next mouse position comes as the user moves the mouse. When a "drag" message comes in, the last mouse position from this user is retrieved from the Hashtable, and a line is drawn from that point to the new point, using the agent's color. Then the last point is set to the current point, for the next drag message. If an "end" message is received, then a line is drawn from the last point to the new point, and the last point is removed from the Hashtable. Figure 10-2 shows the shared whiteboard in action, with two users sharing a whiteboard on remote machines.
Although this first attempt works, it's not very useful. The event-handling methods on the WhiteboardUser don't do any drawing on the Canvas; all of the drawing is done in the notify() method. So even the local user's scribbles on the whiteboard are not shown in the local display until a message has been broadcast through the mediator, and received back through notify(). Even with a pretty fast network connection, users will see a noticeable (and annoying) delay between their mouse movements and the update of the Canvas.
This problem is simple to remedy. We need to draw the user's scribblings on the local whiteboard immediately, broadcast the event through the mediator, and ignore any incoming notifications from the mediator that have our Identity on them (to avoid drawing the local stuff twice). We can do this by calling nextLine() right in the mouseDragged() and mouseReleased() methods, before we broadcast the event to the other users:
public void mouseReleased(MouseEvent ev) { Point evPt = ev.getPoint(); try { nextLine(getIdentity().getName(), evPt, (Color)getIdentity().getProperty("color")); lastPts.remove(getIdentity().getName()); broadcast("end", evPt); } catch (Exception e) {} } public void mouseDragged(MouseEvent ev) { Point evPt = ev.getPoint(); try { nextLine(getIdentity().getName(), evPt, (Color)getIdentity().getProperty("color")); lastPts.put(getIdentity().getName(), evPt); broadcast("drag", evPt); } catch (Exception e) {} }
Then we modify our notify() method to compare the identity of the source to our local identity. If they are the same, we just ignore the message, since we've already drawn our own scribblings locally:
public boolean notify(String tag, Object data, Identity src) throws IOException, RemoteException { if (src.getName().compareTo(getIdentity().getName()) == 0) { return true; } ...
A more subtle problem with this whiteboard client is the choppy drawing that results as the user drags the mouse across the Canvas. Instead of a smooth, curved line being drawn exactly where the mouse goes, I get a choppy line connecting points along the path I take. This happens because of the way we're handling mouse events in the WhiteboardUser class. Each event is passed into a call to one of the mouseXXX() methods. These methods broadcast the event to the other whiteboard users by calling the broadcast() method from RMICollaboratorImpl, which in turn remotely calls the mediator's broadcast() method. While the WhiteboardUser waits for the remote method call to complete, the user continues to move the mouse. In most AWT implementations, the event is passed into the event handler in the same thread that is polling for user events. This is done to ensure that events are handled in the order that they are received. If the AWT internals simply spawned off an independent thread to handle each incoming event, there would be no guarantee of the order in which the threads will run--it's up to the thread-scheduling process of the local virtual machine. In our case, if we blindly start a new thread to handle each draw event from the user, then some drag events may be handled out of sequence if their threads happen to get some CPU time before the earlier events. We'll end up drawing lines between disconnected points along the path of the mouse, which will result in a confusing mess. But with each event being handled in the same thread as the event poller, a lengthy or blocked event-handling thread can cause lost user events, which seems to be our problem here. Some of the mouse drag events are being lost while the event handling thread waits on the remote broadcast() call.
Fixing this problem is a bit more involved. We need to split the event handling part of our agent and the collaborative broadcasting part into separate threads, but we still need to be sure that the events are processed in the order in which they come in from the user. The easiest part for us to isolate into a new thread is the remote method calls, so that's what we'll do. The new thread simply broadcasts local events to the other users by calling the WhiteboardUser's broadcast() method. The mouse-handling methods pass the events on to this thread as they come in. To ensure that the events get broadcast through the mediator in the right order, we'll put the data for each event onto an event queue, and the event broadcasting thread will poll this event queue and send out events in the order in which they appear in the queue (first-in, first-out).
Our event broadcasting thread is implemented using the Msg and CommHelper classes shown in Example 10-3. The Msg class is simply a container that holds the data for each event. This data is just the tag that will be sent in the remote broadcast() method call, and the Object that is the body of the message (in our case, a Point object). The CommHelper class extends Thread, and has a reference to the collaborator that it's helping, and a Vector of Msgs. The run() method just polls the message list, sending them out as they come by calling the collaborator's broadcast() method.
class Msg { public Object data; public String tag; public Msg(String t, Object o) { data = o; tag = t; } } class CommHelper extends Thread { RMICollaborator collaborator; Vector msgs = new Vector(); public CommHelper(RMICollaborator c) { collaborator = c; } public static void addMsg(String t, Object o) { synchronized (msgs) { msgs.addElement(new Msg(t, o)); } } public void run() { while (true) { try { Msg m = null; synchronized (msgs) { m = (Msg)msgs.elementAt(0); msgs.removeElementAt(0); } collaborator.broadcast(m.tag, m.data); } catch (Exception e) {} } } }
We just need to update our WhiteboardUser class to use this new thread to broadcast user events rather than calling broadcast() directly from the event-handling methods. The updated WhiteboardUser class is shown in Example 10-4 as the ThreadedWhiteboardUser . This updated class also includes the changes described previously to avoid the local drawing delay. The changes are pretty minor: the ThreadedWhiteboardUser has a CommHelper reference, which it initializes in its constructor, passing a reference to itself as the collaborator; the mouseDragged() and mouseReleased() methods have also been updated to send the message tag and event location to the CommHelper, where the event will be queued for broadcast through the mediator.
package dcj.examples.Collaborative; import dcj.util.Collaborative.*; import java.awt.event.*; import java.awt.*; import java.util.Hashtable; import java.util.Properties; import java.io.IOException; import java.rmi.RemoteException; import java.util.Vector; public class ThreadedWhiteboardUser extends RMICollaboratorImpl implements java.awt.event.MouseListener, java.awt.event.MouseMotionListener { protected Hashtable lastPts = new Hashtable(); protected Component whiteboard; protected Image buffer; protected CommHelper helper; public ThreadedWhiteboardUser(String name, Color color, String host, String mname) throws RemoteException { super(name, host, mname); getIdentity().setProperty("color", color); buildUI(); helper = new CommHelper(this); helper.start(); } protected void buildUI() { Frame f = new Frame(); GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); f.setLayout(gridbag); f.addNotify(); c.fill = GridBagConstraints.BOTH; c.gridwidth = GridBagConstraints.REMAINDER; Canvas canvas1 = new java.awt.Canvas(); canvas1.setSize(240,180); canvas1.setBackground(Color.white); gridbag.setConstraints(canvas1, c); f.add(canvas1); String name = null; try { name = getIdentity().getName(); } catch (Exception e) { name = "unknown"; } Label label1 = new java.awt.Label("Your name: " + name); label1.setSize(100,30); gridbag.setConstraints(label1, c); f.add(label1); f.setSize(240,210); f.show(); whiteboard = canvas1; whiteboard.addMouseListener(this); whiteboard.addMouseMotionListener(this); buffer = whiteboard.createImage(f.getSize().width, f.getSize().height); } protected void nextLine(String agent, Point pt, Color c) { Graphics g = buffer.getGraphics(); g.setColor(c); Point lastPt = (Point)lastPts.get(agent); g.drawLine(lastPt.x, lastPt.y, pt.x, pt.y); whiteboard.getGraphics().drawImage(buffer, 0, 0, whiteboard); } public void mousePressed(MouseEvent ev) { Point evPt = ev.getPoint(); try { lastPts.put(getIdentity().getName(), evPt); CommHelper.addMsg("start", evPt); } catch (Exception e) {} } public void mouseReleased(MouseEvent ev) { Point evPt = ev.getPoint(); try { nextLine(getIdentity().getName(), evPt, (Color)getIdentity().getProperty("color")); lastPts.remove(getIdentity().getName()); helper.addMsg("end", evPt); } catch (Exception e) {} } public void mouseDragged(MouseEvent ev) { Point evPt = ev.getPoint(); try { nextLine(getIdentity().getName(), evPt, (Color)getIdentity().getProperty("color")); lastPts.put(getIdentity().getName(), evPt); helper.addMsg("drag", evPt); } catch (Exception e) {} } public void mouseExited(MouseEvent ev) {} public void mouseMoved(MouseEvent ev) {} public void mouseClicked(MouseEvent ev) {} public void mouseEntered(MouseEvent ev) {} public boolean notify(String tag, Object data, Identity src) throws IOException, RemoteException { // If this is our own event, ignore it since it's already been handled. if (src.getName().compareTo(getIdentity().getName()) == 0) { return true; } Color agentColor = null; try { agentColor = (Color)src.getProperty("color"); } catch (Exception exc) { System.out.println("Exception while getting color."); exc.printStackTrace(); } if (tag.compareTo("start") == 0) { // First point along a path, save it and continue lastPts.put(src.getName(), data); } else if (tag.compareTo("drag") == 0) { // Next point in a path, draw a line from the last // point to here, and save this point as the last point. nextLine(src.getName(), (Point)data, agentColor); lastPts.put(src.getName(), data); } else if (tag.compareTo("end") == 0) { // Last point in a path, so draw the line and remove // the last point. nextLine(src.getName(), (Point)data, agentColor); lastPts.remove(src.getName()); } return true; } }
This updated shared whiteboard system is still pretty simple, but useful. Each user can have her own color to distinguish herself from the other users, local drawing is done right away so there's no annoying delay as we drag the mouse over the whiteboard, and we've isolated the remote method calls from the event-handling thread so that none (or few) of the user's mouse events are lost while we block on the remote broadcast() call. But there are a few additional improvements that we could make so that this distributed application is more pleasant to use.
When a user joins a shared whiteboard session, it would be nice to see who is currently using the whiteboard, what color they are using, etc. We could add this to our distributed application by defining a specialized mediator--a WhiteboardMediator--that sends a notification to each new agent with a list of all of the identities of the current users. The WhiteboardMediator would also send a notification to every existing user when a new user joins. The WhiteboardUsers could then update their local displays of remote users, using the name and color properties from the Identity list. To do this, we would just have to write a new implementation of the register() method on our WhiteboardMediator:
public boolean register(Identity i, RMICollaborator c) throws RemoteException { super.register(i, c); send(i, getIdentity(), "userlist", getMembers()); }
We'd also have to update the notify() method on the WhiteboardUser so that it could store the list of users and update its local display.
The most glaring flaw in our whiteboard is that we don't store the board's state on the mediator. This means that any new agents joining an existing whiteboard session won't be able to see what's already been drawn, just what's drawn from the time they join forward. It's easy to fix this problem with a specialized WhiteboardMediator. The mediator would just keep a history of all the scribbles that each user has made. New users joining the whiteboard receive a notification that includes all of the current scribbles that are on the whiteboard, so that they can draw them on their local display. The mediator could save the whiteboard state either as a table of point sets indexed by user Identity, or as an Image buffer. The table of point sets is more complicated to maintain, but opens up the possibility of removing individual user's actions (an "undo" feature), letting users change their personal color, etc. The Image buffer is easy to maintain and to send to new users, but doesn't allow us to pick out scribblings from particular users, since their actions have all been jumbled together into a single Image.
We should be able to speed up the broadcasting and processing of drawing events by the other users. With the current whiteboard system, each mouse event from each whiteboard user is broadcast individually to the group through the mediator. The result is a delay from the time that one whiteboard user moves the mouse to the time that the line is drawn on another user's whiteboard. One way to speed things up is to batch the broadcasting of user events across the system, so that instead of broadcasting each event individually, we're broadcasting sets of events in a single remote broadcast() call. We can either batch the events at the WhiteboardUser, or on the WhiteboardMediator. If we batch them on the mediator, then we're still causing a remote method call for every mouse event the user makes; so it seems we would get the best improvement by doing the batching on the WhiteboardUser itself. We could do this by adding some code to the run() method of the CommHelper, so that local user events are sent a group at a time. We would also have to update the notify() method on the WhiteboardUser, so that it would recognize batch notifications and handle them appropriately.
Copyright © 2001 O'Reilly & Associates. All rights reserved.