Galactic Dust Messages

Nels Pearson, 8jan08
rev: 24jan08

References

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

Overview

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.

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

IsAlive?: A new message called IgnoreMe is used to test the connection by either the client or the server. If the TCP/IP fails to send the message or fails to get an ACK back, the java socket code will throw an exception to indicate that the connection has been lost and that the streams and socket should be closed by the application.  

Send Process

1. Instantiate a message object and pass it 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. Call sendMessage to encode the message and send it.
(This is where the message is actually built up.)

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.
(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

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

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.
 
 

Examples

Sending a Message

Example: Firing a bullet. Done after all checks pass for being able to fire.
FireMessage msg = new FireMessage(x, y);
sendMessage(msg);
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;
    }
  }
}