ZMUohMy_MessageFactory.html
06dec09 CmdrZin
17dec09
 

References

1. Kevin Glass's sgs-tank (org.newdawn) code used with the Darkstar Java Game Server.

Overview

The Message Factory concept is a general purpose message formatting and handling system.
It supports different types of messages identified by the first byte in the message. The rest of the message is formatted and parsed based on this message type ID byte. In this way, not every thing has to be String text based. The basic message transport is still the ByteBuffer object.
The base class is Message and all messages extend this and add their own formatting and parsing methods.

Messages are data structures passed between the Client and the Server.
They are stored in a ByteBuffer object. This object maintains an index pointer that is incremented on each get() call.
On receive, the MessageFactory pulls the first byte off to see what type of message it is. It then instantiates a new object of this message type and calls the decode() method for that object. It then returns the message object to the caller. This seems like a fairly clean way to handle messages.

Unified message system
Send and Receive use the same Message Class for a common code base.
Send and Receive use the same extended Message Class objects for encoding and decoding message body data and inserting message type codes.

Send Process

1. Instantiate a message object and pass its body data.
(The message is not built up at this time. This allows a single common msg.encode call to be made at the time of sending rather than every time a message is instantiated.)
2. Pass a large enought ByteBuffer to buildMessage to encode the message into.
(This is where the message is actually built up.)
3. Pass the trimmed down ByteBuffer to sendToUser() for output.

Receive Process

1. Upon detecting a full data packet in the stream buffer, call dataArrived to pull the first byte out and use it to determine the message type. This is done by MessageFactory.getMessage().
The ByteBuffer object being passed around remembers when it is accessed and keep this status in its ponters.
(The appropriate message object is instantiated and the message is converted from a byte array into the elements of the new message object.)
2. The new message object is then sent to a handler for game processing.

Organization

sun.com.sgs.darkmud.messages (package)
Message.java  // Common abstract message class
MessageFactory.java // Central distributor for incoming messages
UniqueMessage1.java // message object using the abstract Message class
 ...
UniqueMessageN.java // message object using the abstract Message class

Enhancement Ideas

Messages to the server that are game related could be sent to a queue based on the LinkedList class.
System related messages, such as LogOff, would be handled immediately.

Encryption: Using byte buffers for messages allows easy implementation of encryption. Message body can be padded out to 16 or 64 byte boundaries with random numbers for block encryption. The header (type) could be expanded to two bytes and given error correction encoding.

Sending a Text Message to the Client

MudUser.sendToUser() is the primary way to send a message to the Client. It currently sends a text string through use of a ByteBuffer. This protocol will be changed to use a ByteBuffer as the input parameter.
Sending messages will now be a two step process:
1. Build the Message object into a ByteBuffer with xxx.encode (xxx is a class that extends the Message class).
2. Send the ByteBuffer to sendToUser the way text is now sent.
First step is to change over all of the text message generators to use this protocol and then change the Client to be compatable.

TextMessage class

Add TextMessage.java that extends the Message class to sun.com.sgs.darkmud.messages.
This class just encodes and decodes a text string.

Changes to MudUser.java

Add a sendMsgToUser( Message message ) method that allocates a general purpose ByteBuffer to be used for sending a Message object. (see code)
NOTE: Also added some test code to decode an encoded String to test the MessageFactory.

Add another sendToUser() method that has a ByteBuffer as the input parameter.

Changes to MudMain.java

Changed loggedIn() to use a TextMessage object to send text to Client.
    // We send the welcome message to the client
//OLD     user.sendToUser(welcome);
    TextMessage msg = new TextMessage(welcome); //NEW
    user.sendMsgToUser(msg);                    //NEW

DEBUG:
Works with test code in sendMsgToUser(), so on to modifying Client.

Changes to MudClient.java

Changed receivedMessage() to use the test code from sendMsgToUser() to decode the message back into a String. Once this works, the support for other Message types can be added.
ChangeOver step 1: Test the first byte of the ByteBuffer to see if its a TextMessage.ID. If is is, send it to the MessageFactory for decoding and handle as a Message object. If not, then pass on as a String as before.
Added code (see code)
    if (message[0] == TextMessage.ID) {
        try {
            m.rewind();
            Message bmsg = MessageFactory.getMessage(buffer);
            // Need to check the message type then cast to proper object.
            TextMessage tmsg = (TextMessage) bmsg;
            msg = tmsg.getMessage();
        } catch (IOException e) {
        }
    } else {
        msg = new String(message);
This and the old type testing code will be replace with a new method called handleMessage().
Ok..text messages work. Back to the Server to change over all othe text outputs to Client.
Changed FlightSpell.cancelSpell()
//OLD  ((MudUser)getContainer()).sendToUser("Your blue glow fades.\n");
    TextMessage msg = new TextMessage("Your blue glow fades.\n"); //NEW
    ((MudUser)getContainer()).sendMsgToUser(msg);                 //NEW
Changed Zombie.doSomething()
//OLD   mu[0].sendToUser(sOut);
    TextMessage msg = new TextMessage(sOut); //NEW
    mu[0].sendMsgToUser(msg);                //NEW
Changed MudUser.buildExperienceMessage()
//OLD   sendToUser("@01"+experience);
    TextMessage msg = new TextMessage("@01"+experience); //NEW
    sendMsgToUser(msg);                                  //NEW
Other modules changed in the same way.
Ok...all other text output to Client is changed over. Now to remove the test code in the Client so that
it only uses the new handleMessage method for the new protocol...
Added method handleMessage() (see code). Might as well go all out and add InfoMessage and QuestMessage types rather than hack more of the recieveMessage() method.

Add InfoMessage.java to handle old '@' messages. Leave as Strings for now. Need to figure out the best way to format the data. (see code)
Add QuestMessage.jave to handle old '[' messages. Leave as Strings for now.  Need to figure out the best way to format the data. (see code)
DEBUG: Client receiveMessage() uses handleMessage() and only Message objects..oops, forgot to add new message types to MessageFactory switch case list...doh..and not using in Server to encode messages..arrrgh..Lots of other sendToUser() calls in server. Mostly for command handling..so just cast these all into TextMesssage objects for now..
Changes to sendToUser
Add
        TextMessage msg = new TextMessage(text);
        sendMsgToUser(msg);
Delete the rest.
DEBUG: rats..forgot to disable test code in sendMsgToUser..hmm..type 0 being sent..try default these to Strings..maybe need to rewind before testing..rewind before sending??..YES..ok..a little clean up..and forgot to add break for each case in handleMessage()..ok..everything seems to work again..
TODO: Still need to figure out the command output and how it gets to sendToUser().
Next steps are to do the same thing for Client to Server messages and to use the encoding features to format the data instead of string token coding and parsing.
Going to do the encoding first..
14dec09
ok..on with the encoding..The first area to use it is in the Info lists that are sent to the Client. These contain stats, skills, item, and quest data. These are name:value pair right now so encoding them should be a piece of cake.

InfoMessage.java

Need four lists of name:value pairs: Stats, Skills, Inventory, and Quests and four for the Strings.
The InfoMessage class will do the name extraction from the database now and send the name strings in the message. Otherwise, the Client would have to have a copy of the database also.
For now, the InfoMessage constructor will be sent all four lists. Later, a single list can be sent for updating with very little change to the code. This is where the Message Factory concept provides flexablility.
(see code)

MudClient.java

More changes here. Need to change the way the display lists are loaded.
Added method ClientInfo.updateInfo() to put list data into display lists. (see code)
This is a lot cleaner than all the text parsing.
DEBUG: Haven't done buildExperienceMessage() yet..so disable..hmm...maybe buffer too small??..
BUG: Wrong order of encoding of Info..didn't get key size..WOO HOO...all back to normal.
Now the Client to Server needs to be done, but this is mostly just text right now so should be easy.
17dec09 Update the ASK command and the EP message.

ExpMesssage.java

This message is used to update the User's experience points text display.
Its sent as an int instead of a String just to annoye packet sniffers. (see code).
Add this message type to MessageFactory, MudUser, and MudClient.handleMessage.
Test exp message..works like its suppose too...make an AskMessage that brings up NPC dialog box.

AskMessage.java

This message will bring up the general purpose NPC dialog box. For now, it displays the text that is static on the Client side. It can easily be expanded to output text sent from the server.
hmm...stuck in commit..don't really need to have an AskTask. If used, need to get invoker..ok now
Trying to use AskTask, but need invoker, but MudCommand is not seralizable..so kernel fails..
Going to do it without Task for now..tried by holding the ManagedReference for the user..works..so will use AskTask to send message..another example if nothing else. (see QuestNPC.java code)

All working. Now to come up with a way to send user input back to this NPC without using the command protocol..maybe an ID for each generated QuestNPC or an association table..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
   

TODO: Continue with exapmle. This is all old stuff.
Example: Sending a text message

// text comes from the user input text box as before.
UserCommandMessage msg = new UserCommandMessage(text);
sendMessage(msg);
The first messgae type will be TEXT to replace the general text command messages and outputs that are used by the Client and Server.
 
 
Details:
ID is a static byte defined in the Class for each message type. These must be different for each message type.
This constructor is called to instantiate a Class which Extends a common Message Class
The constructor calls the original (super) Message Class constructor to set the message type code.
/**
 * Create a new fire message
 */
public FireMessage(float x, float y) {
Call the constructor of the super class (Message) to set the message type.
  super(ID);
Save the parameters for constructing the message body later.
  this.x = x;
  this.y = y;
}

This is the constructor for Message. It is evoked by the super( ) call.
The parameter code is the message class ID from the super( ) call.
Message.class code
/**
  * Create a new message based on its unique identifier
  */
public Message(byte code) {
  this.code = code;
}

This code calls the message encoder to construct a data array for the message body the then sends the message.
SendBuffer is a global 2k ByteBuffer object used as a holding buffer for output.
/**
 * Send a message to the channel.
 */
private synchronized void sendMessage(Message msg) {
  try {
    // encode the message into a byte buffer so SGS
    // client libs can send it
    msg.encode(sendBuffer);
This must be the SGS send support call. This would be replaced with the socket send method call.
    channel.sendBroadcastData(sendBuffer, true);
  } catch (IOException e) {
    Log.log(e);
  } catch (Throwable e) {
    Log.log(e);
  }
}

Build up the message body in a temporary buffer.
Clear the holding buffer then put in the type and then add the message body.
The buffer can now be sent to the channel for sending.
Message.class code
/**
 * Encode this message into the byte buffer provided
 */
public void encode(ByteBuffer buffer) throws IOException {
  ByteArrayOutputStream bout = new ByteArrayOutputStream();
  DataOutputStream dout = new DataOutputStream(bout);

  // I like data output streams, they're easier to work with than buffers
  encodeImpl(dout);

  buffer.clear();
  buffer.put(code);
  buffer.put(bout.toByteArray());
}

This was an abstract method of Message that is overwritten by the new Class.
It writes the message body data into the stream provided.
(See ChatLobby for string use examples.)
/**
 * @see org.newdawn.tank.messages.Message#encodeImpl(java.io.DataOutputStream)
 */
public void encodeImpl(DataOutputStream dout) throws IOException {
  dout.writeFloat(x);
  dout.writeFloat(y);
}

Receiving a Message

Example: receiving a message and process it
Message message = MessageFactory.getMessage(data);
handleMessage(message);
Code from GameView.java puts the handler inside the try loop.
This code from LobbyDialog.java is a bit cleaner with them separate.
Details:
Pass the buffer to the MessageFactory to try to generate the right type on message object.
This code is assumed to be called by the SGS CLient.
Must be from implementing ClientChannelListener interface.
/**
 * @see com.sun.gi.comm.users.client.ClientChannelListener#dataArrived(byte[], java.nio.ByteBuffer, boolean)
 */
public void dataArrived(byte[] userID, ByteBuffer data, boolean reliable) {
  try {
    Message message = MessageFactory.getMessage(data);

    handleMessage(message);
  } catch (IOException e) {
    Log.log(e);
  }
}

The MessageFactory pulls the first byte off to determine the message type.
It then instantiates a message object of that type or throws an error back.
If the type is valid, it calls the message's decode method to format the data into elements associated to the specific message type and returns the message object for processing.
/**
 * A simple factory to take a ByteBuffer recieved from SGS and decode
 * it into a well formed message object.
 * @author Kevin Glass
 */
public class MessageFactory {
 /**
  * Decode the byte buffer into a message object. A runtime exception
  * is thrown to indicate an unrecognised message type.
  *
  * @param buffer The buffer to decode
  * @return The message object decoded
  * @throws IOException Indicates that data could not be read from the
  * buffer.
  */
  public static Message getMessage(ByteBuffer buffer) throws IOException {
    byte code = buffer.get();
    Message message;
    switch (code) {
      case ChatMessage.ID:
      {
        message = new ChatMessage();
        break;
      }
      case ArenaListMessage.ID:
      {
        message = new ArenaListMessage();
        break;
      }
      case FireMessage.ID:
      {
        message = new FireMessage();
        break;
      }
      default:
      {
        Log.log("Unrecognised message code: "+code);
        throw new RuntimeException("Unrecognised message code: "+code);
      }
    }
    message.decode(buffer);
    return message;
  }
}

Assuming a FireMessage was sent, then message.decode would call FireMessage.decode which is a Message.decode method that FireMessage inherited.
/**
 * Decode the message from a byte buffer
 */
public void decode(ByteBuffer buffer) throws IOException {
  byte[] array = new byte[buffer.remaining()];
  buffer.get(array);

  ByteArrayInputStream bin = new ByteArrayInputStream(array);
  DataInputStream din = new DataInputStream(bin);

  decodeImpl(din);
}

The decodeImpl method is abstract and supplied by FireMessage.
The method is customized for the FireMessage class and extracts the data into the appropriate elements.
/**
 * @see org.newdawn.tank.messages.Message#decodeImpl(java.io.DataInputStream)
 */
public void decodeImpl(DataInputStream din) throws IOException {
  x = din.readFloat();
  y = din.readFloat();
}

The data has now been formatted into the FireMessage object and can be handled by processes unique to that type of message.
Up to this point, NO game actions has been taken based on the contents of the message.
It has only been unravelled back into its original form or data structure that was sent by the sender.
The Handler can now decide what game action(s) should take place based on the message content.
Since this code snippet handler came from the chat room code JobbyDialog.java, a FireMessage would just be ignored. The GameView.jave handler code shows how this message type would be processed.
/**
 * Handle a message recieved form the server
 *
 * @param message The message recieved from the server
 */
public void handleMessage(Message message) {
  switch (message.getID()) {
    case ChatMessage.ID:
    {
      ChatMessage msg = (ChatMessage) message;
      chat.append(msg.getMessage()+"\n");
      int index = chat.getText().length();
      chat.select(index,index);
      break;
    }
    case ArenaListMessage.ID:
    {
      ArenaListMessage msg = (ArenaListMessage) message;
      arenaList.clear();
      for (int i=0;i<msg.getArenaCount();i++) {
        arenaList.addElement(msg.getArenaName(i));
     }
     break;
    }
  }
}