I'm building a client and server for a game but wanted a generic messaging system in a shared library that let me focus on application logic and was largely separate form the underlying networking I/O details.
I'm mostly looking for suggestions or possible improvements. Performance on the server is a concern with all the casting. I'm stuck on .Net 3.5 for the client (Unity3D) but if there's decent performance improvements to be found for the server I'd consider splitting the library. I'm also using Protobuf-net for serializing messages being sent across the network. My network protocol uses two 4 byte prefixes, one for the message byte length and the 2nd is an int32 that maps to a dictionary of types so I can deserialize messages to their exact type upon arrival. The typemap is built at startup reflecting through all the message subtypes in the same library.
I'll not post the client/server code because I see those as separate concerns (Network I/O, handshaking, serialization etc can be done a bunch of different ways). Lastly, I've cut out most the threading code to make it easier to read.
My approach breaks down messaging into three simple Message
, Request
and Response
types with the following rules:
- All network messages will derive from the
Message
class. - Any message that derives from
Request
will expect aResponse
in reply. Request
andResponse
are both sub-classes ofMessage
.
Message.cs
public partial class Message
{
public virtual bool IsResponse { get { return false; } }
public int id { get; }
public string description { get; }
static int idCounter = 0;
public Message()
{
id = idCounter++;
}
public Message(string description)
{
id = idCounter++;
this.description = description;
}
}
Response.cs
public class Response : Message
{
public MessageStatus status { get; }
public bool Success { get { return status == MessageStatus.Ok; } }
public override bool IsResponse { get { return true; } }
public Response(int id, string description)
{
this.id = id;
this.status = MessageStatus.Ok;
this.description = description;
}
public Response(int id, MessageStatus status, string description)
{
this.id = id;
this.status = status;
this.description = description;
}
}
Request is not shown, but it's basically Message with an additional type field so the application knows what kind of Response type is expected. Game/Application logic can register to listen for the arrival of specific message subclasses. All the plumbing is taken care of via two singeltons; a MessageRouter and a ResponseRouter (couldn't think of better names). As the server or client receives data and the protocol handler deserializes it into a Message instance it then passes it off to the MessageRouter
via the Dispatch method:
MessageRouter.cs
class MessageRouter
{
private static MessageRouter instance;
protected Dictionary<Type, object> handlers;
public static MessageRouter Instance
{
get
{
if (instance == null) { instance = new MessageRouter(); }
return instance;
}
}
protected MessageRouter()
{
handlers = new Dictionary<Type, object>();
}
public bool AddHandler<T>(Action<T> handler) where T : Message
{
if (handler == null) { return false; }
var messageType = typeof(T);
object outHandler;
if (handlers.TryGetValue(messageType, out outHandler))
{
var typedHandler = outHandler as Action<T>;
handlers[messageType] = typedHandler + handler;
return true;
}
handlers.Add(messageType, handler);
return true;
}
public bool RemoveHandler<T>(Action<T> handler) where T : Message
{
if (handler == null) { return false; }
var messageType = typeof(T);
return handlers.Remove(messageType);
}
public bool Dispatch<T>(T message) where T : Message
{
if (message == null) { return false; }
if (message.IsResponse)
{
if (ResponseRouter.Instance.Dispatch(message)) { return true; }
}
var messageType = message.GetType();
object handler;
if (handlers.TryGetValue(messageType, out handler))
{
if (handler == null) { return false; }
(handler as Action<T>)(message);
}
return false;
}
}
As you may have noticed, if the message has an IsResponse
property of true
it is passed off to the ResponseRouter
instead as I assume no code should be registering to listen for response types via the MessageRouter (I should probably check for that and log a warning). The ResponseRouter
uses a similar pattern but they're more one-shot events, as soon as they're triggered the handler is removed from the response router. Response handlers are passed into the client/server's send method:
public delegate void ResponseCallback<T>(T response) where T : Message;
void Send<TMessage>(TMessage message)
where TMessage : Message;
{ ... }
void Send<TRequest, TResponse>(TRequest request, ResponseCallback<TResponse> responseCallback,)
where TRequest : Request
where TResponse : Response;
{
...
ResponseRouter.Instance.RegisterCallback(request, responseCallback);
...
}
ResponseRouter.cs
public class ResponseRouter
{
private static ResponseRouter instance;
private Dictionary<int, object> responseMap;
public static ResponseRouter Instance
{
get
{
if (instance == null) { instance = new ResponseRouter(); }
return instance;
}
}
protected ResponseRouter()
{
responseMap = new Dictionary<int, object>();
}
public bool RegisterCallback<TRequest,TResponse>(TRequest request, ResponseCallback<TResponse> callback)
where TRequest : Request
where TResponse : Response
{
if (responseMap.ContainsKey(request.id)) { return false; }
responseMap.Add(request.id, callback);
return true;
}
public bool Dispatch<T>(T response) where T : Message
{
if (response == null) { return false; }
object callback = null;
if (responseMap.TryGetValue(response.id, out callback))
{
responseMap.Remove(response.id);
}
if (callback == null) { return false; }
(callback as ResponseCallback<T>)(response);
return true;
}
}
To support a new message type I simply have to subclass Message or Response and write the application code to hook into handling them. The only messaging special case is authentication. Establishing or restoring client sessions (disconnect recover) is all logic that can exist far away from my core server/client code.
Request is not shown, but it's basically
- please add it, even if it is a trivial difference from another class, this helps with the ability for us to help you optimize/follow best practices. \$\endgroup\$