I’m developing a simple message passing interface in Java using JBoss’s Netty API. During the design phase I considered two options. The first option is quite primitive and was simply a constants file containing public integers that mapped to a particular message type. This way seemed efficient, but may not scale so well and will be annoying to maintain. The second option was to design a base Message class that will be extended by custom message types. This was the option I chose. Here is a small example of how it works.
package com.dunyaonline.communication.messages;
import java.nio.ByteBuffer;
public abstract class Message {
protected int clientId;
protected int characterId;
public abstract int getMessageType();
public abstract ByteBuffer toByteBuffer();
public static final int CLIENT_ID_OFFSET = 1;
public static final int CHAR_ID_OFFSET = 2;
public Message() {
}
public Message(int id) {
this.clientId = id;
}
/**
*
* @param clientId the id of the remote client.
* @param charId the id of the remote clients current selected character
*/
public Message(int clientId, int charId) {
this.clientId = clientId;
this.characterId = clientId;
}
public int getClientId() {
return clientId;
}
public void setClientId(int clientId) {
this.clientId = clientId;
}
public int getCharacterId() {
return characterId;
}
public void setCharacterId(int characterId) {
this.characterId = characterId;
}
}
package com.dunyaonline.communication.messages.requests;
import java.io.Serializable;
import java.nio.ByteBuffer;
import com.dunyaonline.communication.MessageTypes;
import com.dunyaonline.communication.messages.Message;
public class LoadCharacterRequest extends Message implements Serializable {
public static final int TYPE = MessageTypes.CHAR_SELECTION_TYPE;
private static final long serialVersionUID = -1718878638687805906L;
public LoadCharacterRequest(int id) {
super(id);
}
/**
*
* @param clientId the id of the remote client.
* @param charId the id of the remote clients current selected character
*/
public LoadCharacterRequest(int clientId, int charId) {
super(clientId, charId);
}
public LoadCharacterRequest(String[] messageArray) {
clientId = Integer.parseInt(messageArray[CLIENT_ID_OFFSET]);
characterId = Integer.parseInt(messageArray[CHAR_ID_OFFSET]);
}
public int getMessageType() {
return TYPE;
}
/**
* Prepare message for custom serialization
*/
public ByteBuffer toByteBuffer() {
StringBuilder outputString = new StringBuilder();
outputString.append(TYPE + "#" + clientId + "#" + characterId + "\r\n");
ByteBuffer bb = ByteBuffer.wrap(outputString.toString().getBytes());
return bb;
}
}
Once the message is instantiated, it can be sent over an ordinary Channel by invoking the channel.write(Object);. For the sake of brevity I will only demonstrate how the Channel is setup and which pipelines to use (Note: For the sake of simplicity I am using a deprecated Object).
public void initNIOSocketChannel() {
ChannelFactory factory = new NioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ClientBootstrap bootStrap = new ClientBootstrap(factory);
bootStrap.setPipelineFactory(new ClientPipelineFactory());
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("encoder", new ObjectEncoder());
pipeline.addLast("decoder", new ObjectDecoder());
pipeline.addLast("clientchannelhandler", handler);
bootStrap.setPipeline(pipeline);
clientMessageService.setChannel(bootStrap
.connect(new InetSocketAddress("localhost", 7555))
.awaitUninterruptibly().getChannel());
}
The ObjectEncoder and ObjectDecoder are provided by Netty and automatically handle serializing and deserializing the object to and from a ChannelBuffer.
The receiving end of the message will also need an identical pipeline setup. In the case of this example, it is a Client/Server relationship.
private void initNetwork() {
messageService = new MessageService();
ChannelFactory factory = new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ServerBootstrap bootstrap = new ServerBootstrap(factory);
zoneServerHandler = new MessageChannelHandler();
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new ObjectEncoder(),
new ObjectDecoder(), zoneServerHandler);
}
});
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
messageService.addChannel(bootstrap.bind(new InetSocketAddress(PORT)));
messageService.setServerHandler(zoneServerHandler);
Note: I did not include an ObjectEncoder for the Server side, which means that the server will not be writing Objects back out to the client. If you want to add an encoder, simply mimic the client side pipeline setup, or consult Netty’s documentation.
Now that this is complete, I will show you how I handle messages on the server side.
private void processMessages(Queue messageQueue) {
for (int i = 0; i < messageQueue.size(); ++i) {
if (messageQueue.peek() == null)
return;
MessageEvent e = messageQueue.poll();
if (e.getMessage() instanceof LoadCharacterRequest) {
}else if(e.getMessage() instanceof ZoneListRequest)
{
}else if(e.getMessage() instanceof ZoneSelectedRequest)
{
}
}
}
Notice, that once a message has been encoded, sent, received and decoded, the receiving end has a POJO and we can use the instanceof operator to determine the message type and direct program flow accordingly. This method leaves the source code in a much more readable and maintainable state.