Doblet Interfaces

Mark Hooijkaas, $Revision: 1.1.1.1 $, $Date: 2000/10/12 02:15:29 $

Introduction

This document describes the core interfaces for using with doblets. These core interfaces only deal with object creation, connection setup, basic message handling, and cleanup. It explicitely does not deal with user interface or administration issues.

It is a design target to provide a environment for doblet clients and servers to be written in many different languages. However, for the short term, we will focus on java based servers and clients, for simplicity, and getting a better understanding of the problem and the do's and dont's.

The interfaces and this document can be split up in two separate parts:

This design has been given much thought and has been rewritten several times, but finally seems to be very satisfying. The special feature here is that the messaging interfaces do not form a complete messaging system. It leaves out all details of creating objects and connecting them. This is thought to be of a lower level, and might have several different implementations.

Messaging Interfaces

The messaging interfaces are fairly generic and straightforward. They should not be too difficult to understand from the raw code as presented below:
package org.doblet.message

public interface SendableMessage {
  String getId();
  void   send();
}

public interface ReceivedMessage {
  String           getId();
  Serializable     getBody;
  Channel          getReceiveChannel();
  SendableMessage  createReply(Serializable body);
}

public interface Channel {
  SendableMessage createMessage(Serializable body);
  void destroy();
}

public interface MessageReceiver {
  void handleMessage(ReceivedMessage msg);
}

public interface ThreadSafeReceiver extends MessageReceiver {
}
The general idea is that two "objects" (it is not yet defined what kind of objects), communicate with each other over a dedicated Channel.
  1. To send a message, one creates a SendableMessage using a Channel, and invokes the send method.
  2. This message then arrives at the other end of the Channel as a ReceivedMessage and is passed to the MessageReceiver object on that other side, using the handleMessage method.
  3. This MessageReceiver can examine the message body, but also it's origin, by identifying which channel the message was received from.
  4. If it wants to send a reply to the sender of the message, it could use this Channel to create a new message. However there is a special method createReply to create a reply to a specific ReceivedMessage. Using this special method gives the underlying implementation a possibility to correlate the "reply" to the original "request". This feature has not yet been worked out any further, but might allow a more convenient request.reply mechanism to be used in the future.
Normally, it is assumed that a MessageReceiver will receive messages one at a time, i.e. the handleMessage will not be called until the previous handleMessage has finished and returned. This makes it easy for programmers, because they don't have to worry about thread safety. A more advanced programmer might try to make his code thread safe anyway, and mark it as such by implementing the special marker interface ThreadSafeReceiver. If a MessageReceiver also implemtents that interface the message dispatcher is allowed (but not obliged) to pass multiple messages to the receiver in separate threads. This might give rise to improved performance, but with properly designed asynchronous code it should not be necessary. Note that even a normal MessageReceiver might have a risk of multiple threads, because a GUI in general has a separate event loop thread. So even if it will have to handle only one message at a time, there is a possibility that it will receive a GUI event during the handling of the message.

When the Channel is broken (by either side, or if a network problem is detected), both sides of the Channel will receive a special system message.

public interface SystemMessage extends ReceivedMessage {
}

public interface ChannelDestroyedMessage extends SystemMessage {
}
This message will always be the last message to be received on a Channel. A SystemMessage is special since it is not sent by the other side of the Channel but by the system/middleware itself. A SystemMessage will not have an Id (i.e. it will return a null String as id). You will not be able to create a reply to a ChannelDestroyMessage, since the channel is already destroyed.

Doblet Interfaces

The Doblet interfaces are small, but may be complicated to understand. They are mainly used to create and connect doblet servers and clients. A doblet client is created with a doblet server with a Channel as defined in the previous interfaces. Once connected, they can exchange messages as described above. Each client has exactly one Channel (connecting it to it's server), while a server may have zero, one or more Channel's, depending on the number of clients.

There are four main objects to be identified:

package org.doblet

public interface DobClient extends MessageReceiver {
  void init(DobletBrowser, Channel, Logger);
}

public interface Doblet extends MessageReceiver {
  void init(DobletServer, Logger);
}

public interface DobletBrowser {
  Request connect(String Url);
}

public interface DobletServer {
  Request createDoblet(DobletClass class);
}

public interface NewClientMessage extends SystemMessage {}
public interface WelcomeMessage   extends SystemMessage {}
Note: For a definition and an explanation of the Request interface see the "Asynchronous Object Requests" document.

A DobletServer and a DobletBrowser are typically quite large object or applications, with all kind of administration and runtime functionality and a GUI to use it. The interfaces presented here only show the core methods to be implemented by each corresponding object. More specifically, the interfaces basically show how a DobletServer and a Doblet cooperate, and how a DobletBrowser and a DobletClient cooperate.

Doblet and DobletServer cooperation

Creating a Doblet

The creation of a new Doblet is triggered by an external event. Such an event can be a user using the administrator GUI of a DobletServer, but also another (container) Doblet requesting the creation of a (contained) Doblet, or even a network event. The chain of events is roughly as follows:
  1. The caller calls DobletServer::createDoblet with the DobletClass of which an instance is to be created.
  2. The DobletServer, locates the correct code, and creates an instance of this class.
  3. The DobletServer initializes the newly created Doblet with a call to Doblet::init.
  4. The caller is notified using the asynchronous ResultListener mechanism, and receives a reference to the newly created and initialized Doblet as the Result value.
It is left implementation dependent if step 2 and 3 are part of the synchronous createDoblet call, or are performed outside of this method. Step 2 should be performed asynchronously if retrieving the Doblet code is done over a network. Step 3 should in general always be performed asynchronously, because a Doblet might use asynchronous initialization (e.g. connecting to a remote resource, such as a database server).

Handling a new client request

A newly created Doblet is kind of useless when there are no clients connected to it. When a DobletServer receives a request from a DobletBrowser (more about this later), for a Doblet url, the following happens:
  1. The DobletServer receives the request from a DobletBrowser.
  2. The DobletServer creates a Channel Object.
  3. The DobletServer calls handleMessage on the requested Doblet with a NewClientMessage. The channel in this message is the newly created channel.
  4. The Doblet object receives the message and can take any appropriate actions to decide if he wants to accept or decline this new client.
Step 4, might be done immediatly (synchronously) in the handleMessage call. Alternatively it might take a long time, and should be done asynchronously outside of the handleMessage call. A typical case of the latter would be, where the Doblet sends messages to other clients (e.g. all existing clients, or the client with "operator" status.), who manually have to accept or decline the new client.

The middleware should guard objects from sending messages on a channel that is not accepted yet (or that has been destroyed), by throwing an exception in such cases. This exception should be thrown by the send method, and not by the createMessage or createReply method. The reason for this is that someone may create a message while the channel is still active, but when he "finally" tries to send the message, the Channel has been destroyed.

Disconnecting a client

When a client disconnects, the Doblet will receive a ChannelDestroyedMessage. This will happen in all cases where a client disconnects:

DobletClient and DobletBrowser cooperation

Creating a DobletClient

Creating a DobletClient is better known as "connecting to a Doblet". The creation of a new Doblet is triggered by an external event, just as creating a new Doblet. Such an event can be a user typing in the url of a Doblet in his DobletBrowser (or using a bookmark), but also another (container) DobletClient requesting the creation of a (contained) DobletClient. This is probably one of the most complicated actions, but it's main flow will be something as follows:
  1. The caller calls DobletBrowser::connect with the url of the Doblet it wants to connect to.
  2. The DobletBrowser creates a channel to the Doblet.
  3. The DobletBrowser asks the DobServer the exact type for the client for that specific Doblet.
  4. The DobBrowser locates the correct code, and creates an instance of this class.
  5. The DobletBrowser initializes the newly created DobletClient with a call to DobletClient::init.
  6. The caller is notified using the asynchronous ResultListener mechanism, and receives a reference to the newly created and initialized Doblet as the Result value.
  7. The Channel is ready for use when the WelcomeMessage has been received.
The difficult part is that the order of steps 2, 3, 4, 5, 6 and 7 is not rigidly defined. Some variations include: The last case is the most general. For this it is important to know which dependencies exist between steps. Some of these dependencies are: Note: Locating the client code in step 4 may be asynchronous code. One of the key features is that this client code can be dynamically loaded from the DobletServer.

Disconnecting a DobletClient

When a DobletClient is disconnected, the DobletClient will receive a ChannelDestroyedMessage. This will happen in all cases where a client is disconnected: After this event the DobletClient has ceased to exist from a DobletBrowser perspective, and the client should clean up neatly.

Open Issues

Appendix: Convenience classes

The following classes may already be defined in the package to prevent each implementation to define it again:
public class SystemMessageImpl implements SystemMessage {
  private Channel channel;
  public SystemMessageImpl(Channel chan)  { channel = chan; }
  public String       getId()             { return null; }
  public Serializable getBody()           { return null; }
  public Channel      getReceiveChannel() { return channel; }
  public SendableMessage createReply(Serializable body) 
  { return null; }
}

public class ChannelDestroyedMessageImpl 
  extends    SystemMessageImpl
  implements ChannelDestroyedMessage
{
}