Introduction#

One of the major limitations in Wonderland before 0.5 has been the lack of extensibility in the communications APIs. Adding new message types outside of a cell previously involved modifying several classes in the core of Wonderland, which can only be done by core developers. Further, all clients were represented on the Wonderland server by the same type of session object, so clients have no control over what type or how many messages they receive.

The goal of the Wonderland 0.5 communications architecture is to remove these restrictions, allowing developers to extend how Wonderland communicates in many interesting ways. Sample use cases are:

  • Sending messages to all clients with a given cell in view, as in previous versions of Wonderland
  • Extending Wonderland with a new communication type, such as advanced text chat
  • Connections for custom clients such as the Shared Application Server and server management UI
  • Connection via totally different interfaces such as a C or mobile client

The architecture described below is designed to support these various use cases by building an extensible communications architecture.

Architecture Overview#

The communications architecture is designed in 3 main layers. The lowest layer is the protocol layer, which is used by all clients to select how they communicate with the server. Above the protocol layer is the session layer, which organizes Java-based clients into as an extensible set of connections. Finally, the connection layer is used by the application to send application-specific data. Figure 1 shows these various layers.

Communications Overview

Figure 1: Layers of communications architecture


Specific information about each layer is described below. The communications APIs Javadoc is available here:

Entry Points#

The comms API is designed with two simple entry points. On the server, the CommsManager provides methods for managing protocol, sessions and clients, as described below. On the client, the ServerSessionManager allows client-side plugins to register for events fired during the lifecycle of a session and corresponding connections.

Multiple Protocol Support#

In order to support multiple communications styles and mechanisms, it is important that Wonderland support an extensible set of communications protocols. While most clients will use the default Wonderland protocol described below, the default protocol requires the client to send messages as serialized Java objects. For non-Java or bandwidth-limited clients, it is often preferable to use different types of messages. The protocol selection mechanism allows clients to specify the message type they would like to use, and lets the server specify a SessionListener to handle those messages. The set of protocols and protocol versions is designed to be extensible, so developers can add support for new protocols to the Wonderland server.

When Wonderland clients connect, the first message then send must be a ProtocolSelectionMessage. The ProtocolSelectionMessage specifies the protocol name and protocol version the client wants to use. The server handles this message with a built-in ProtocolSessionListener. Based on the requested protocol, the server then allocates an instance of the protocol-specific SessionListener for that protocol. All future messages sent by the client are handled by this specific SessionListener. Figure 2 documents the sequence of events for a client to select an appropriate protocol.

TODO: Today the ProtocolSelectionMessage is sent as a serialized Java object. This message should be defined in a platform-agnostic way such as binary or XML to enable other client types.

Communications Protocol Selection

Figure 2: Protocol selection

Adding a new protocol#

Communications protocols are defined on the server with instances of the CommunicationsProtocol object. Each communications protocol is uniquely identified by a string protocol name, for example "test_protocol". In addition protocols contain a protocol version object, which can be used to test if the client's version of a protocol is compatible with the server's version.

To register a new protocol, a server plugin calls the CommsManager.registerProtocol(CommunicationsProtocol) method. This method will add the protocol with the given name to the set of protocols managed by the server. Only a single CommunicationsProtocol can be registered for any given protocol name.

When a client connects and sends a ProtocolSelectionMessage, the server finds a CommunicationsProtocol registered with the same name, or sends an error if none can be found. Once the protocol object is found, the server comparese the ProtocolVersion specified by the client with the server's version. If the version's are compatible the server calls the CommunicationsProtocol.createSessionListener() method to generate the actual listener. Note the createSessionListener() takes the ProtocolVersion as an argument, so the protocol can return different listeners to handle different client version, for example.

Each protocol defines its own ClientSessionListener. After the initial ProtocolSelectionMessage, this ClientSessionListener is used to handle all messages from the client, just like a normal Darkstar ClientSessionListener.

Example one below shows a custom server protocol.

Default protocol#

Wonderland contains a default protocol, which performs the connection management described in the next two sections. This protocol uses serialized Java messages for all communications. The protocol is selected automatically by the WonderlandSession. The protocol is defined as follows:

* Protocol Name: "wonderland_client" * Protocol Version: an instance of DefaultProtocolVersion incremented as the core protocol changes.

These details are handled internally by the WonderlandProtocolVersion class.

Session Layer#

In a federated Wonderland environment, a client will often need to connect simultaneously to multiple Wonderland servers. The session layer is designed to facilitate these connections. A Wonderland client can create a number of sessions to different servers, and enable different capabilities, in the form of connections, on each of these sessions. A client may connect multiple sessions to same server, and may also connect to different servers. Figure 3 shows a single client connected to multiple servers with multiple sessions and connections.

Client with Multiple Sessions

Figure 3: Client with multiple sessions

A session -- represented on the client by an implementation of the WonderlandSession interface -- handles login to the Wonderland server and implements the protocol selection mechanism described above. Once a session is connected to a server, connections may be added to it. All messages between the client and server are then sent over one of these connections. Connections and messages are described in more detail below.

Session lifecycle#

A WonderlandSession has a well-defined lifecycle. Sessions move between the following states: DISCONNECTED, CONNECTING, CONNECTED, as shown in figure 4.

Session states

Figure 4: Session states

A session initially starts as DISCONNECTED. When the client calls WonderlandSession.login(), the session will change status to CONNECTING, and attempt to connect to the server. If the connection fails, the session will return to the DISCONNECTED state. If the connection succeeds, it goes into the CONNECTED state. Once a session is connected, it will remain that way until it returns to the DISCONNECTED state when the client calls WonderlandSession.logout() or the server disconnects. Code on the client can register a SessionStatusListener with any WonderlandSession. The listener will be notified whenever the session's status changes between these states.

In addition, plugin code on the client can register a SessionLifecycleListener that will be notified when a new session is created. While many custom clients will extend the default WonderlandSessionImpl as described below, plugins can add connections to any or all existing sessions by registering a SessionLifecycleListener with the WonderlandSessionManager singleton. This listener will be notified when a new connection is created, allowing the plugin to register a SessionStatusListener on any connection that is created.

Extending the default WonderlandSessionImpl#

The Wonderland client provides a default implementation of WonderlandSession called WonderlandSessionImpl. This session implements all the methods of WonderlandSession. Typically, different clients will extend this default WonderlandSessionImpl to attach and make available certain connections by default. The code snippet below shows an example of an extension of WonderlandSessionImpl that connects default clients on login:

public class CellClientSession extends WonderlandSessionImpl {
    // Override the login message to connect clients after the login
    @Override
    public void login(LoginParameters loginParams) 
            throws LoginFailureException 
    {
        // this will wait for login to succeed
        super.login(loginParams);
        
        // if login succeeds, connect the various clients
        try {
            cellChannelConnection.connect(this);
            cellCacheConnection.connect(this);
        } catch (ConnectionFailureException afe) {
            // a client failed to connect -- logout
            logout();
            
            // throw a login exception
            throw new LoginFailureException("Failed to attach client" , afe);
        }
    }
 }

The complete example is shown in Example Two.

Managing sessions on the server#

On the server, sessions are managed by the WonderlandSessionListener. The WonderlandSessionListener supports the basic operations of attaching and detaching connections. This class is internal, and not designed to be extended. The intention is that extension will primarily involve adding new connection types. If a lower-level change is needed, you can define a custom protocol as defined above.

Connection Layer#

Connections are the main way of communicating between the Wonderland client and the Wonderland server. A Wonderland client establishes any number of connections to a Wonderland server by connecting them to a session, as described above. Once a connection is connected, messages may be passed from the client to the server and vice-versa. Messages can also be broadcast by the server, so a message from a single client can be sent by the server to different groups of clients. Figure 5 below shows some common message patterns.

Message Patterns

Figure 5: Message patterns

As shown in figure 5, clients can only send messages to the server. The server can send response messages to a single client, or can broadcast a response to a number of clients. While not shown in figure 5, the server can also originate messages, sending messages that are not in response to a client message or request.

Each connection in Wonderland has a unique type for sending different types of data. For example, a client may use one connection for sending cell data, and another for sending voice communications data. Clients may use as many or as few connections as are necessary for their interaction with the server. Thus a system manager application may only use a connection for server status information, and not cell or voice connections. The only limitation is that a client may only have a single connection of a given type connected to a given session. Clients may use multiple sessions to get multiple copies of a single connection type, as shown in figure 3 above.

Establishing connections#

A connection in Wonderland consists of three main classes:

  • A ConnectionType identifies the type of connection. This is a string name that must be unique for each class of connection. Only one connection of a given type can be connected to a given session.
  • The ClientConnection interface defines the client side of a connection. This interface primarily defines the callbacks that will be used to notify the connection when it is connected, disconnected or a message is received.
  • The ClientConnectionHandler interface defines the server side of a connection. The interface primarily defines the callbacks that will be used to notify the handler when a new connection is received.

To create a custom connection type, developers must define a custom ConnectionType, and then implement the client-side ClientConnection interface and server-side ClientConnectionHandler interface. Defining custom connections is described in detail below. Example three shows a complete connection, including the messages, connection, and connection handler.

Once a connection is defined, it can be used on the client by connecting it to a WonderlandSession, using the WonderlandSession.connect() method. The connect() method optionally takes a set of properties, which are passed to the server to modify how the connection is handled. The connect() method can only be called on a WonderlandSession that is in the CONNECTED state. As shown above, this is often done in custom extensions of WonderlandSessionImpl. In addition, plugins can add new connections to existing WonderlandSessions by registering lifecycle listeners, as defined above.

Messages#

Connections on the client and connection handlers on the server communicate by sending messages. All messages must extend the Message base class, which defines a simple serializable object with a unique id. Messages must be serializable. In addition to Message, a few standard message base classes are defined:

  • ResponseMessage represents a response to a user's request. The MessageID of the response must match the id of the original request. Clients will frequently wait for a response to a request they send to the server. Note that currently, clients typically send only requests, while server send either requests or responses.
  • OKMessage and ErrorMessage are specific responses that indicate either success or failure of the given request.
  • MessageList allows a number of messages to be sent in a single message object.

With the exception of OKMessages and ErrorMessages, connections will typically define their own custom messages. These messages extend the Message base classes to carry specific information. The example below shows a simple message which carries text data:

public class TextMessage extends Message {
    private String text;
    
    public TextMessage(String text) {
        this.text = text;
    }
    
    public String getText() {
        return text;
    }
}

Message ids are automatically generated in the default Message class, using an instance of MessageIDGenerator. The specific generator to use can be set using the static Message.setMessageIDGenerator() method. Message ids do not have to be globally unique, but must be unique within a given session and connection type, so clients can wait without worrying about duplicate ids.

Creating the ClientConnection#

Developers can add new connection types to the Wonderland client by creating custom connection classes, which implement the ClientConnection interface. To make this process easier, the BaseConnection helper class implements the basic building blocks needed by most connections. The BaseConnection class contains methods to easily attach the connection to a client, and also methods for sending and handling messages.

For sending messages, BaseConnection provides three methods:

In order to use the second two methods, the server must cooperate to guarantee a response will be sent to the given request. If no response is sent, the client will either have a memory leak due to listeners not being cleaned up or the thread will block indefinitely. TODO: consider adding timeout versions of the listener and sendAndWait methods.

Note that the methods to send messages in BaseConnection are protected. This is because extensions of Connection on the client will not usually expose methods to send messages. Instead, a custom Connection class will provide an interface that other parts of the client code can call, and that interface will create and send the relevant messages. Similarly, most custom Connection classes will provide listener interfaces that will be notified of an event when a message is received. This type of abstraction shields the client code from the mechanics of the underlying messages.

To handle received messages, override the abstract handleMessage(Message) method. Do not replace the messageReceived() method, as this will prevent sendAndWait() from working.

Creating the ClientConnectionHandler#

On the server, each connection is handled by a class implementing ClientConnectionHandler. The connection handler is responsible for handling all messages sent over the connection by the client, and also for sending messages back to the client.

Before a connection handler can be used, it must be registered with the CommsManager, using the registerClientHandler() method. This registration must happen before any clients connect, typically when a plugin is installed in the server.

Unlike the client side, there is no helper class for the ClientConnectionHandler, since all method of the handler deal directly with the lifecycle of the handler and attached clients. The connection handler is notified of the following events:

  • The ConnectionHandler is registered with the CommsManager, via the registered() method.
  • A new client connects a ClientConnection of the type this handler manages, via the clientConnected() method. The properties passed to this method are the properties the client used when it connected.
  • A message is received from a client's ClientConnection for this type, via the messageReceived() method. * A client disconnection a ClientConnection via the clientDisconnected() method.

All of these lifecycle methods take as a first argument a WonderlandClientSender. A WonderlandClientSender is the only way to send messages to a client and have them properly handled by the client's ConnectionHandler. The WonderlandClientSender is passed in to the ClientConnectionHandler in many places, including the registered() method, and it is serializable so it can be cached. In addition, the CommsManager.getSender(ConnectionType) method can be used to get a sender for any connection type. These mechanisms make it possible for the server to asynchronously send messages to the client as needed.

The WonderlandClientSender provides several ways to send to different collections of connected clients. The methods allow you to send to:

Note that the sending methods other than send(Message) don't guarantee that all clients will have connected the a ClientConnection of the given connection type. Care should be taken, for example, when managing channels to make sure all clients have the a ClientConnection of the right type available. A good way to do this is by managing Channel joins and leaves in the clientConnected() lifecycle method.

Code Examples#

Example 1. Custom communications protocol#

The protocol name and version are defined by an instance of ProtocolVersion:

// protocol version used for testing
public class TestProtocolVersion extends DefaultProtocolVersion {
    public static final String PROTOCOL_NAME = "test_protocol";
    public static final TestProtocolVersion VERSION = new TestProtocolVersion();
    
    private TestProtocolVersion() {
        super (1, 0, 0);
    }
}

On the server, a plugin registers a new handler for TestProtocol:

// plugin to support the TestProtocol
public class TestProtocolPlugin implements ServerPlugin {

    // the initialize method of the plugin registers the new protocol
    public void initialize() {
        CommsManager cm = WonderlandContext.getCommsManager();
        cm.registerProtocol(new TestProtocol());    
    }

    // handle the test protocol    
    public static class TestProtocol 
            implements CommunicationsProtocol, Serializable
    {
        private ProtocolVersion version = TestProtocolVersion.VERSION;
        
        public String getName() {
            return TestProtocolVersion.PROTOCOL_NAME;
        }

        public ProtocolVersion getVersion() {
            return version;
        }

        public ClientSessionListener createSessionListener(ClientSession session, 
                                           ProtocolVersion version) 
        {
            return new TestProtocolSessionListener(session);
        }
    }
    
    // listener that will be notified when a message comes from the client
    public static class TestProtocolSessionListener
            implements ClientSessionListener, Serializable
    {
        private ManagedReference<ClientSession> sessionRef;

        private TestProtocolSessionListener(ClientSession session) {
            logger.info("New session for " + session.getName());
            
            DataManager dm = AppContext.getDataManager();
            sessionRef = dm.createReference(session);
        }
            
        public void receivedMessage(ByteBuffer data) {
            System.out.println("Received " + data.remaining() + " bytes from " +
                                    getSession().getName() + ".");
        }

        public void disconnected(boolean forced) {
            System.out.println("Session " + getSession().getName() + " disconnected.");
        } 
        
        private ClientSession getSession() {
            return sessionRef.get();
        }
    }
}

Finally, a client that can be used to connect to a client with the given protocol: TODO: this example client shouldn't use WonderlandSessionImp

public class ClientMain {
    // the server info
    private WonderlandServerInfo serverInfo;
    
    public ClientMain(WonderlandServerInfo serverInfo) {
        this.serverInfo = serverInfo;
    }

    public void connect() throws LoginFailureException {
        // read the username and properties from files
        String username = System.getProperty("sgs.user", "sample");
        String password = System.getProperty("sgs.password", "sample");

        // create the client & login
        TestSession session = new TestSession(serverInfo);
        session.login(new LoginParameters(username, password.toCharArray()));

        // if we get here, login succeeded
        session.logout();
    }
   
    // session that connects with test protocol
    class TestSession extends WonderlandSessionImpl {

        public TestSession(WonderlandServerInfo serverInfo) {
            super (serverInfo);
        }
        
        @Override
        protected String getProtocolName() {
            return TestProtocolVersion.PROTOCOL_NAME;
        }

        @Override
        protected ProtocolVersion getProtocolVersion() {
            return TestProtocolVersion.VERSION;
        }
    }
    
    public static void main(String[] args) {
        // read server and port from properties
        String server = System.getProperty("sgs.server", "locahost");
        int port = Integer.parseInt(System.getProperty("sgs.port", "1139"));

        // create a login information object
        WonderlandServerInfo serverInfo = new WonderlandServerInfo(server, port);

        // the main client
        ClientMain cm = new ClientMain(serverInfo);

        try {
            cm.connect();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Example 2. Extending WonderlandSessionImpl#

This example shows an extension to WonderlandSessionImpl for adding connecting well-known connections every time the session connects.
/**
 * An extension of WonderlandSession that attaches all the relevant
 * handlers needed for a client using the Cell system
 */
@InternalAPI
public class CellClientSession extends WonderlandSessionImpl {
    
    /** the cell client */
    private CellCacheConnection cellCacheConnection;
    private CellChannelConnection cellChannelConnection;
    
    public CellClientSession(WonderlandServerInfo serverInfo) {
        super (serverInfo);
        
        // create connections       
        cellCacheConnection = new CellCacheConnection();        
        cellChannelConnection = new CellChannelConnection();
    }
    
    /**
     * Get the cell cache connection
     * @return the cell cache connection
     */
    public CellCacheConnection getCellCacheConnection() {
        return cellCacheConnection;
    }
    
    /**
     * Get the cell channel connection
     * @return the cell channel connection
     */
    public CellChannelConnection getCellChannelConnection() {
        return cellChannelConnection;
    }
    
    /**
     * Override the login message to connect clients after the login
     * succeeds.  If a client fails to connect, the login will be aborted and
     * a LoginFailureException will be thrown
     * @param loginParameters the parameters to login with
     * @throws LoginFailureException if the login fails or any of the clients
     * fail to connect
     */
    @Override
    public void login(LoginParameters loginParams) 
            throws LoginFailureException 
    {
        // this will wait for login to succeed
        super.login(loginParams);
        
        // if login succeeds, connect the various clients
        try {
            // first connect the cell channel connection, so we can receive
            // cell messages.  We need to do this before attaching the
            // cell cache connection, since the cell cache connection
            // will create a view and immediately start joining us to cells
            cellChannelConnection.connect(this);

            // Now connect to the cellCache. The view will be determined via the
            // localAvatar object.
            cellCacheConnection.connect(this);
        } catch (ConnectionFailureException afe) {
            // a client failed to connect -- logout
            logout();
            
            // throw a login exception
            throw new LoginFailureException("Failed to attach client" , afe);
        }
    }
}

Example 3. Custom connection type#

First, our custom connection type:

public class TextConnectionType extends ConnectionType {
    public static final TextConnectionType TEXT_TYPE = new TextConnectionType("__TextClient"):
    
    private TextConnectionType(String typeName) {
        super (typeName);
    }
}

Next, the message we will send:

public class TextMessage extends Message {
    private String text;
    
    public TextMessage(String text) {
        this.text = text;
    }
    
    public String getText() {
        return text;
    }
}

Then, the connection itself, for use on the client side:

class TextConnection extends BaseConnection {

        public ConnectionType getConnectionType() {
            return TextConnectionType.TEXT_TYPE;
        }

        // send a message to the server.  Note this method
        // creates the message internally
        public void send(String message) {
            super.send(new TextMessage(message)));
        }
        
        // handle a message we receive
        public void handleMessage(Message message) {
              if (message instanceof TextMessage) {
                  System.out.println(((TextMessage) message).getText());
              }
        }
}

Finally, the connection handler on the server:


static class TestClientHandler implements ClientConnectionHandler {

    public ConnectionType getConnectionType() { 
        return TextConnectionType.TEXT_TYPE;
    }
        
    // handle a message from the client
    public void messageReceived(WonderlandClientSender sender,
                                                    ClientSession session,
                                                    Message message)
    {
        // echo message to all clients
        TextMessage tm = (TextMessage) message;
        sender.send(new TextMessage(session.getName() + ": " + message.getText());
    }   
    
    // notification that this plugin is registered
    public void registered(WonderlandClientSender sender) {
        System.out.println("Registered");
    }
    
    // notification that a new client has connected    
    public void clientConnected(WonderlandClientSender sender,
                                                ClientSession session,
                                                Properties properties) 
    {
        System.out.println("Client attached: " + session);
    }
    
    // notification that a client has disconnected
    public void clientDisconnected(WonderlandClientSender sender,
                                                     ClientSession session) 
    {
        System.out.println("Client detached: " + session);
    }
}

Add new attachment

Only authorized users are allowed to upload new attachments.

List of attachments

Kind Attachment Name Size Version Date Modified Author Change note
png
comms_overview.png 52.0 kB 1 29-Jul-2011 23:00 Josmas Flores
png
comms_protocol_selection.png 8.1 kB 1 30-Jul-2011 12:07 Josmas Flores
png
message_patterns.png 52.0 kB 1 30-Jul-2011 13:29 Josmas Flores
png
multi_session.png 44.7 kB 1 30-Jul-2011 13:14 Josmas Flores
png
session_states.png 5.0 kB 1 30-Jul-2011 13:17 Josmas Flores
« This page (revision-20) was last changed on 21-Sep-2015 12:35 by Abhishek Upadhyay