Book Home Java Distributed Computing Search this book

6.6. Message Passing with Java Events

During the course of this chapter, we've built up our own message-handling framework from scratch, relying on basic sockets, I/O streams, and object serialization to implement a protocol for sending, receiving, and handling messages. In this section, we'll look at merging a message-passing framework with the Java event model that's used in the AWT package for handling GUI events. The advantage of using events is the possibility of integrating your distributed system with other systems based on the Java event model, including AWT-based applications or applets.

6.6.1. Event Model Overview

The event model included in the Java API (version 1.1 and later) is generic enough to build event-handling protocols for general applications, not just GUI-related ones. The Java event model is based on EventObjects that are created by various event sources and handled by classes that implement an EventListener interface.

Different types of events are defined by creating subclasses of the EventObject class. The EventObject class only contains a source Object. Subclasses of EventObject can add additional data to represent event specifics. For example, the AWT package defines a KeyEvent subclass that represents keyboard events. The KeyEvent class contains data fields that specify which key was pressed to generate the event.

The Java event model is called a delegation model; events are generated by a source of some kind, and EventListeners register themselves with the event source to handle specific types of events. When an event arrives, the event source delegates the handling of the event to the EventListeners registered with it. The Java AWT package uses this event model by defining various GUI components that are sources of user events. User events are modeled as subclasses of EventObject. Various types of EventListeners are registered with the GUI components to receive specific user event types, like mouse events, keyboard events, etc.

6.6.2. Distributed Events

In our case, we want to send and receive events over a network between distributed agents. For each node in our distributed system, we'll need to receive messages in the form of events from one or many remote agents, and we'll need to send messages to other agents. One model to use in building this event-based message-passing system is to follow the lead of our previous message-handler examples, and have event " transceivers" at each node in the system. For the local node, these event transceivers would act as both sources of events and as event handlers or listeners, passing local events to remote agents.

Along these lines, we have the EventTransceiver class shown in Example 6-13. This class connects itself to a single remote agent to exchange events with it. This limited form of an event transceiver will be sufficient to implement an event-based version of our chess-playing example. We'll leave it to the reader to extend this class to handle multiple remote agents.

Example 6-13. An Event Transceiver

package dcj.util.message;

import java.util.*;
import java.net.*;
import java.io.*;

public class EventTransceiver implements EventHandler extends Thread {
  // A hashtable of handlers for specific events
  private Hashtable handlers = new Hashtable();
  // A list of handlers that want all events
  private Vector globalHandlers = new Vector();

  // Our connection to a remote agent
  InputStream evIn = null;
  OutputStream evOut = null;

  public EventTransceiver(String host, int port) {
    try {
      InetAddress a = InetAddress.getByName(host);
      connect(a, port);
    }
    catch (Exception e) {}
  }

  public EventTransceiver(InetAddress a, int port) {
    connect(a, port);
  }

  void connect(InetAddress a, int port) {
    try {
      Socket s = new Socket(a, port);
      evIn = s.getInputStream();
      evOut = s.getOutputStream();
    }
    catch (Exception e) {
      evIn = null;
      evOut = null;
    }
  }
  
  public EventTransceiver(InputStream in, OutputStream out) {
    setStreams(in, out);
  }

  void setStreams(InputStream in, OutputStream out) {
    evIn = in;
    evOut = out;
  }

  public void sendEvent(EventObject ev) throws IOException {
    ObjectOutputStream oout = new ObjectOutputStream(evOut);
    oout.writeObject(ev);
  }

  EventObject receiveEvent() throws IOException {
    ObjectInputStream oin = new ObjectInputStream(evIn);
    EventObject ev = null;
    try {
      ev = (EventObject)oin.readObject();
    }
    catch (ClassCastException e) {
      System.out.println("Non-event object sent to EventTransceiver");
    }
    catch (ClassNotFoundException e2) {
      System.out.println(
        "Unresolvable object type sent to EventTransceiver");
    }

    return ev;
  }

  void distributeEvent(EventObject ev) {
    // Send event to all "global" handlers
    Enumeration e = globalHandlers.elements();
    while (e.hasMoreElements()){
      EventHandler h = (EventHandler)e.nextElement();
      h.handleEvent(ev);
    }

    // Send event to handlers targeting the event's class
    Class evClass = ev.getClass();
    Vector evHandlers = (Vector)handlers.get(evClass);
    e = evHandlers.elements();
    while (e.hasMoreElements()) {
      EventHandler h = (EventHandler)e.nextElement();
      h.handleEvent(ev);
    }
  }

  // No default behavior for handling events...
  public void handleEvent(EventObject e) {}

  // Register a handler that wants all events.
  public void addHandler(EventHandler eh) {
    if (!globalHandlers.contains(eh)) {
      globalHandlers.addElement(eh);
    }
  }

  // Register a handler for a specific type of event
  public void addHandler(EventHandler eh, EventObject e) {
    Class eClass = e.getClass();
    addHandler(eh, eClass);
  }

  public void addHandler(EventHandler eh, Class ec) {
    Vector evHandlers = (Vector)handlers.get(ec);
    if (evHandlers == null) {
      evHandlers = new Vector();
      handlers.put(ec, evHandlers);
    }
    if (!evHandlers.contains(eh)) {
      evHandlers.addElement(eh);
    }
  }

  // Remove a handler from all lists
  public void removeHandler(EventHandler eh) {
    globalHandlers.removeElement(eh);
    Enumeration ecList = handlers.keys();
    while (ecList.hasMoreElements()) {
      Vector evHandlers =
        (Vector)handlers.get(ecList.nextElement());
      if (evHandlers != null) {
        evHandlers.removeElement(eh);
      }
    }
  }

  // Remove a handler for a specific event type
  public void removeHandler(EventHandler eh, EventObject e) {
    removeHandler(eh, e.getClass());
  }

  public void removeHandler(EventHandler eh, Class ec) {
    Vector evHandlers = (Vector)handlers.get(ec);
    if (evHandlers != null) {
      evHandlers.removeElement(eh);
    }
  }

  // If run as an independent thread, just loop listening
  // for events from the remote agent, and distribute them
  // to registered handlers
  public void run() {
    try {
      while (true) {
        EventObject e = receiveEvent();
        if (e != null)
          distributeEvent(e);
      }
    }
    // Treat an IOException as termination of the event
    // input stream, and let this handler thread die
    catch (IOException e) {}
  }
}

The EventTransceiver class extends an EventHandler interface, which is an extension of the java.util.EventListener interface that adds a handleEvent() method (see Example 6-14). The EventTransceiver maintains its connection to a remote agent as an I/O stream pair. It has two constructors that take arguments specifying a remote agent to which to connect: one uses a hostname and port number, the other uses an InetAddress and port number. These two constructors use the host and port information to open a socket to the remote agent and get the InputStream and OutputStream from the socket. A third constructor accepts an InputStream and OutputStream that are preconnected to a source and destination for events, respectively.

Example 6-14. EventHandler Interface

package dcj.util.message;

import java.util.EventListener;
import java.util.EventObject;

public interface EventHandler extends EventListener {
  public void handleEvent(EventObject e);
}

The EventTransceiver interface includes addHandler() methods that let you attach EventHandlers to this event source. You can register a handler for any events, or you can register a handler for a specific type of event by providing a second argument that's either an instance of an EventObject subclass, or the Class object for the subclass itself. The EventTransceiver keeps the registered handlers in a hashtable of Vectors that hold the EventHandlers; the sets of handlers are hashed by the EventObject subclass under which they were registered. Handlers can be removed from the transceiver using the corresponding removeHandler() methods. The distributeEvent() method takes an EventObject instance and passes it to the registered handlers. First it calls the handleEvent() method on any handlers registered to receive all events. Then it looks up the handlers registered for the specific type of event by getting the Class of the EventObject, and getting the Vector of EventHandlers from its table of handlers. It passes the EventObject to the handleEvent() method of any handlers it finds.

The EventTransceiver has a sendEvent() method for sending an Event-Object directly to its remote agent, and a receiveEvent() method that does a blocking read on the InputStream (using an ObjectInputStream wrapper) for an EventObject from the remote agent. The EventTransceiver also extends Thread, and in its run() method it performs an infinite loop, reading events from its InputStream using its receiveEvent() method and distributing them by calling its own distributeEvent() method.

Using the EventTransceiver class, implementing our chess-playing server is just a matter of subclassing a ChessEventServer class, as shown in Example 6-15. The ChessEventServer maintains its own ChessPlayer object, and it mediates a game between its player and a remote player, much like the message-passing ChessServer in Example 6-5. In its constructors, the ChessEventServer registers with itself to receive ChessMoveEvent and ChessConcedeEvent events.

Example 6-15. An Event-Based Chess Server

package dcj.examples.message;

import dcj.util.message.*;
import java.util.*;
import java.net.*;
import java.io.IOException;

public class ChessEventServer extends EventTransceiver {
  ChessPlayer player = new ChessPlayer();

  public ChessEventServer(String host, int port) {
    super(host, port);
    register();
  }

  public ChessEventServer(InetAddress host, int port) {
    super(host, port);
    register();
  }

  void register() {
    // Add ourselves to this handler's list for
    // chess-related events
    try {
      addHandler(this, Class.forName("ChessMoveEvent"));
      addHandler(this, Class.forName("ChessConcedeEvent"));
    }
    catch (ClassNotFoundException nfe) {}
  }

  public void handleEvent(EventObject e) {
    try {
      if (e instanceof ChessMoveEvent) {
        ChessMoveEvent cm = (ChessMoveEvent)e;
        ChessMove m = cm.getMove();
        switch (cm.getType()) {
          case ChessMoveEvent.SUBMIT:
            if (player.acceptMove(m)) {
              ChessMoveEvent conf = new ChessMoveEvent(m, player);
              conf.setConfirm();
              sendEvent(conf);

              ChessMove next = player.nextMove();
              if (next != null) {
                ChessMoveEvent submit = new ChessMoveEvent(next, player);
                sendEvent(submit);
              }
              else {
                sendEvent(new ChessConcedeEvent(player));
              }
            }
            else {
              ChessMoveEvent reject = new ChessMoveEvent(m, player);
              reject.setReject();
              sendEvent(reject);
            }
          break;

          case ChessMoveEvent.REJECT:
            ChessMove next = player.nextMove();
            if (next != null) {
              sendEvent(new ChessMoveEvent(next, player));
            }
            else {
              sendEvent(new ChessConcedeEvent(player));
            }
          break;

          case ChessMoveEvent.CONFIRM:
            player.moveAccepted(m);
          break;
        }
      }
      // If we get a concede message, the other player has
      // given up and we win...
      else if (e instanceof ChessConcedeEvent) {
        player.conceded();
      }
    }
    catch (IOException ioe) {
      System.out.println("IO error while handling event.");
      ioe.printStackTrace();
    }
  }
}

Both of these EventObject subclasses are shown in Example 6-16. Everything else is done in the ChessEventServer.handleEvent() method. When it receives a ChessMoveEvent from the remote agent, it checks the type of move event (SUBMIT, CONFIRM, or REJECT), and calls the appropriate method on the local player. If necessary, it takes output from the local player and sends it off to the remote player as a ChessMoveEvent or ChessConcedeEvent, calling its sendEvent() method to transmit the event to the remote agent. If a ChessConcedeEvent comes in, then it tells the local player that it won the game by calling its conceded() method.

Example 6-16. Chess-Specific EventObjects

package dcj.examples.message;

import java.util.EventObject;

public class ChessMoveEvent extends EventObject {
  ChessMove move;
  int type;

  public final static int SUBMIT = 0;
  public final static int CONFIRM = 1;
  public final static int REJECT = 2;

  public ChessMoveEvent(ChessMove subject, ChessPlayer src) {
    super(src);
    move = subject;
    type = SUBMIT;
  }

  public int getType() { return type; }

  // Set the type of the move event
  public void setConfirm() { type = CONFIRM; }
  public void setReject() { type = REJECT; }
  public void setSubmit() { type = SUBMIT; }

  // Get and set the move
  public ChessMove getMove() { return move; }
  public void setMove(ChessMove m) { move = m; }
}

public class ChessConcedeEvent extends EventObject {
  // Just a placeholder class, no data or methods needed
  public ChessConcedeEvent(ChessPlayer src) {
    super(src);
  }
}

6.6.3. Pros and Cons

In terms of the overall utility of the system, this event-based message-passing system is about equivalent to the framework we developed earlier in the chapter. What it offers, however, is the possibility of integrating our distributed event-handling system with AWT-based applications to create distributed user interfaces of a sort. Since our EventTransceiver is written to handle events in the form of Java EventObject, there's no reason it can't send and receive AWT events from user interfaces between agents on the network, as long as they are Serializable. Figure 6-2, for example, demonstrates an AWT button with a listener (the SurrogateActionListener) attached that sends action events from the button through the EventTransceiver to a remote agent, where the action is processed. The listener would need to implement the java.awt.ActionListener interface, as well as our EventHandler interface, as shown in Example 6-17.

figure

Figure 6-2. Distributing AWT events to remote handlers

Example 6-17. A Surrogate AWT ActionListener

import java.awt.event.*;
import dcj.util.message;

public class SurrogateActionListener implements ActionListener, 
                                                EventHandler {

  private EventTransceiver xceiver;

  public SurrogateActionListener(EventTransceiver t) {
    xceiver = t;
    // Register as handler for global events
    xceiver.addHandler(this);
  }

  public void actionPerformed(ActionEvent e) {
    // A local action event has been generated - send it
    // to the remote agent
    xceiver.sendEvent(e);
  }

  public void handleEvent(EventObject) {
    // Do something with events from the transceiver...
  }
}

We could attach this listener to a local AWT interface element like a Button, and when the Button generates an action event, it will be passed to the sendEvent() method on the EventTransceiver to a remote agent. The remote agent may then do something in reaction to the button event, and perhaps generate an event in response. The event will be received by the local EventTransceiver and, since the SurrogateActionListener registers itself with its EventTransceiver as a handler for any events, the transceiver will call the SurrogateActionListener's handleEvent() method, which can act on the event from the remote agent (change the button's color, put some text on the screen, etc.).

Using the SurrogateActionListener, we would only need to create an EventTransceiver as we did before, then construct a SurrogateActionListener, passing the EventTransceiver into its constructor. We would then register the listener as an ActionListener on the AWT Button by calling the Button's addActionListener() method. This would establish the event-passing link from the button, through the surrogate listener to the transceiver, and finally to the remote event handler.

Although this approach is interesting and potentially useful, you have to be careful what kinds of events you're trying to route over the network. The Event-Object is going to be serialized by the EventTransceiver and sent over the network to the remote transceiver. If the EventObject subclass keeps a non-transient reference to other local objects, like one of the AWT components, then these objects and any objects they reference, etc., down the whole reference tree, will be serialized and transmitted, too. This would result in an IOException if any of the objects in the tree aren't Serializable. If serialization succeeds, you might end up sending an unexpectedly large amount of data over the wire.

Be aware that the only state information that the remote event handler has to interpret the event is contained in the EventObject itself. Since the remote handler doesn't have direct access to the actual interface components (unless they were serialized within the EventObject), it can't directly query them for their state. So a little more care has to be taken in ensuring that each Event-Object contains all of the information the handler needs to deal with the event.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.