Because of the asynchronous nature of the system, two problems arise. The first, naturally, is the set of difficulties that normally accompany any multithreaded program: the need for synchronization, the possibility of non-deterministic behavior, and so on. The messaging system attempts to avoid these common threading problems as much as possible. The second problem is inherent in the nature of the program itself: because of its asynchronousness, it is difficult to establish a "protocol," or a set order of communication. For example, in a game of chess there is the protocol in which one player makes a move, and then the other player makes the next, and so on. A simple networked game of two player chess would have one player send a move, and the second player "reads" the move from the stream and then sends a move, and so on. However, with the messaging system, there is no way to prevent one player from sending ten moves in a row before the other player has sent anything. To solve such a problem, the implementor must develop a "state machine" on both ends of the communication which determines when to send what data. In the example of the chess game, a flag indicating whose move is to be made next would solve the problem. An example of a state machine establishing a protocol within the messaging system is the FileTransferManager.
However, these problems of creating protocol are greatly overshadowed by the benefits found in being able to send and receive information independent of each other. After all, this is how telephones, people, and most modern operating systems work.
Outline
DataReceiver
The DataReceiver class forms the core of the Messaging architecture. It provides for the "asynchronous" receipt of objects through some provided stream, and contains a mechanism for acting upon the immediate receipt of those objects.
DataListener and DataEvent
The DataReceiver invokes notification of received data through DataListener
objects which have registered themselves with the DataReceiver. When an object
is received, the DataReceiver creates a DataEvent object containing the received
object, and fires it to all DataListeners through the dataReceived
method.
Additionally, when the stream through which the DataReceiver is receiving
objects is closed, it fires the streamClosed method on all of its
DataListeners. The object which is fired with this method should be ignored.
Object Serialization
The DataReceiver's constructor requires an InputStream. This InputStream should
be unused and unwrapped (it may be buffered). The DataReceiver will use it to
construct an ObjectInputStream. This means that the output end of the stream
must be an ObjectOutputStream; otherwise the constructor will block until the
stream header is flushed.
For more information, see the Javadoc for ObjectInputStream on Sun's web site.
Multithreading
In order to provide the immediate-response data receiving system of the
DataReceiver, an external thread is used. This thread continuously reads objects
from the stream and fires events, until there is an error in the stream
(indicating generally that the stream has closed).
This means that the DataListener's actions must take into consideration that
they will not be invoked on the main thread, so that any resources which they
access should be protected through synchronization. Specifically, if those
actions manipulate a GUI, then the SwingUtilities.invokeLater
method must be used to perform such manipulations.
Packets
Although DataReceiver is capable of handling any type of Serializable object,
the messaging system requires a more strict hierarchy of possible objects to be
read. This hierarchy starts from the class:
edu.harvard.cduan.messaging.RoutingPacketThis object has three fields, two Users (a sender and a receiver) and a serializable object being sent. This information is immutable. Objects should always be sent wrapped in some type of packet. For even greater type security (and the possibility of including additional fields) the class may be extended. Newly introduced fields should also be immutable.
Users
Rather than using simply strings to represent user names, all user names are
contained in immutable objects of type:
edu.harvard.cduan.messaging.UserIn addition to providing type safety, this allows for users to be compared without case sensitivity. Future implementations may be able to ignore whitespace as well.
Messenger
The Messenger class provides the interface for the messaging system; all users
should perform their message sending/receiving through this class. In its
constructor, the Messenger requires an input and an output stream; the streams
will be wrapped appropriately in object streams. The Messenger class also
provides support for reconnection.
The Messenger class contains a user name. The object itself never actually uses that name; it is provided primarily for the implementor's convenience, since each Messenger is associated usually with only one user.
The class processes received packets through its method processObject, which is abstract. Any object received by the messenger will cause this method to be invoked. In alternate versions, a listener mechanism like that of DataReceiver may be chosen over the overridden-method model, particularly since many implementations of processObject would be better-suited to a listener-type environment.
Objects are sent through the sendObject method. Despite its name, only RoutingPackets may be passed to this method. It does not check the name of the sender of the packet.
Since the Messenger operates on the basis of a multithreaded environment, it provides start and stop methods, to start and stop the listening mechanism. It is often useful--and sometimes necessary--to override these methods. However, there is the danger that stop may be invoked multiple times. As an alternative, the Messenger may take ChangeListeners, which are notified when either of the two methods are invoked.