This is an iterative review request of this other question I posted.
As a follow up to that previous question, I changed the pattern completely and I went to an events-based approach, which might be more relevant.
Pre-text and background story
To address some of the points in that question (to hopefully avoid repeats):
- The
DataMessageType
enum:
I really don't like this enum and "type" member.
public enum DataMessageType { Login, } public interface IDataMessage { ILogger Logger { get; } DataMessageType Type { get; } void WriteNetworkMessage(NetOutgoingMessage message); void ReadNetworkMessage(NetIncomingMessage message); }
Unfortunately, that enum is what is sent "over-the-wire" between the clients and the server to determine what packet was actually sent. It's hard to remove because I need a way to determine what packet type it was. That said, I'll get to the way I reworked it further down this message.
- Using
DataMessageType
on the server:
This here is where things get not so good.
private void ProcessDataMessage(NetIncomingMessage message) { _Logger.LogInformation("Data recieved from: {0}, payload size: {1} bytes", message.SenderConnection.RemoteEndPoint, message.LengthBytes); IDataMessage messageResult = _DataMessageHandler.HandleMessage(message); switch (messageResult.Type) { case DataMessageType.Login: // Do login-ey things string username = ((LoginMessage)messageResult).Username; byte[] password = ((LoginMessage)messageResult).Password; break; } }
This was actually a very nice suggestion, and the basis for beginning my rewrite. Instead of using that case statement I went to an events approach, so that the main NetworkServer
is not responsible for directly knowing which message type it is.
- Interfaces for
Net*
classes:
If we check the
NetworkServer
class for what could cause itself to change, we can identify
- NetServer
- NetConnection
- DataMessageHandler
- NetIncomingMessage
because you use these as classes but not as interfaces. If you change the name of the class, or its implementation by removing or adding methods etc, you will also need to change the
NetworkServer
class.I would like to suggest using interfaces here. Instead of injecting the
NetPeerConfiguration
into the constructor, you should just inject anINetServer
interface.
Unfortunately, these classes come from a separate project, and I can't won't change them due to the fact that the aforementioned "other" project is source I do not own. (I have it, but I don't own it.) To maintain compatibility (if that source is ever updated and I don't want to merge changes) I can't really use this suggestion. (I would love to, but it's no conceivable.)
- Adapter Pattern (this is where it gets interesting):
Based on the comments and the chatting the problem is like that, that the
NetServer
and the related classes are out of your control.This screams for the adapter pattern which you should place between your
NetworkServer
class and theNetServer
class. The purpose here is not to help your application to understand the 3rd party server, but to hide deep down the code of the maybe ugly convertation of theNetIncomingMessage
.
This is something I'm up for discussing. With the changes I made, the adapter pattern is looking more and more relevant.
On to the code!
So, in lieu of a better way to do things, I switched to an events pattern (which could easily be combined with the adapter pattern):
We'll start with the DataMessageHandler
, since it was the most rewrite:
public class DataMessageHandler
{
private readonly ILogger _Logger;
public ILogger Logger { get { return _Logger; } }
public DataMessageHandler(ILogger logger)
{
_Logger = logger;
}
public IDataMessage HandleMessage(NetIncomingMessage message)
{
DataMessageType type = (DataMessageType)message.ReadInt32();
IDataMessage dataMessage = null;
switch (type)
{
case DataMessageType.Login:
dataMessage = new LoginMessage(_Logger);
dataMessage.ReadNetworkMessage(message);
OnLoginMessageReceived(new LoginMessageEventArgs((LoginMessage)dataMessage));
break;
case DataMessageType.ChatMessage:
dataMessage = new ChatMessage(_Logger);
dataMessage.ReadNetworkMessage(message);
OnChatMessageReceived(new ChatMessageEventArgs((ChatMessage)dataMessage));
break;
default:
_Logger.LogWarning("Unhandled DataMessageType: {0}", type);
OnUnhandledMessageReceived(new MessageEventArgs(dataMessage));
break;
}
return dataMessage;
}
protected void OnLoginMessageReceived(LoginMessageEventArgs e)
{
if (LoginMessageReceived != null)
LoginMessageReceived(this, e);
}
protected void OnChatMessageReceived(ChatMessageEventArgs e)
{
if (ChatMessageReceived != null)
ChatMessageReceived(this, e);
}
protected void OnUnhandledMessageReceived(MessageEventArgs e)
{
if (UnhandledMessageReceived != null)
UnhandledMessageReceived(this, e);
}
public event LoginMessageReceivedEventHandler LoginMessageReceived;
public event ChatMessageReceivedEventHandler ChatMessageReceived;
public event UnhandledMessageReceivedEventHandler UnhandledMessageReceived;
}
Essentially, I added two events (mostly for context), and instead of just returning an IDataMessage
, it also throws an event related to the message it got.
The delegates
and their awfully long names:
public delegate void LoginMessageReceivedEventHandler(Object sender, LoginMessageEventArgs e);
public delegate void ChatMessageReceivedEventHandler(Object sender, ChatMessageEventArgs e);
public delegate void UnhandledMessageReceivedEventHandler(Object sender, MessageEventArgs e);
The EventArgs
classes:
public class MessageEventArgs : EventArgs
{
private IDataMessage _Message;
public IDataMessage Message { get { return _Message; } }
public MessageEventArgs(IDataMessage message)
{
_Message = message;
}
protected internal MessageEventArgs()
{
// So that child classes within this assembly don't need to worry about the `Message` parameter, which they hide anyway. (So that they can strongly type it.)
}
}
public class LoginMessageEventArgs : MessageEventArgs
{
private LoginMessage _Message;
public new LoginMessage Message { get { return _Message; } }
public LoginMessageEventArgs(LoginMessage message)
{
_Message = message;
}
}
public class ChatMessageEventArgs : MessageEventArgs
{
private ChatMessage _Message;
public new ChatMessage Message { get { return _Message; } }
public ChatMessageEventArgs(ChatMessage message)
{
_Message = message;
}
}
So, this makes sure that all the events can have the appropriate information when handled on the server.
That last part is a bit I really want some advice on (especially that protected internal
and all the new
bits). I'm not sure if I'm going about things correctly here or not, and I would greatly appreciate some advice.
Next, the NetworkServer
:
public class NetworkServer
{
private readonly NetServer _MainNetServer;
private readonly HashSet<NetConnection> _ConnectedClients = new HashSet<NetConnection>();
private readonly ILogger _Logger;
private readonly DataMessageHandler _DataMessageHandler;
/// <summary>
/// Creates a new instance of the <see cref="NetworkServer"/> from the <c>NetPeerConfiguration</c> and <see cref="ILogger"/>.
/// </summary>
/// <param name="serverConfiguration">The intial configuration of the <see cref="NetServer"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> that the server should log to.</param>
public NetworkServer(NetPeerConfiguration serverConfiguration, ILogger logger)
{
_Logger = logger;
_Logger.LogInformation("Initializing server...");
_MainNetServer = new NetServer(serverConfiguration);
_DataMessageHandler = new DataMessageHandler(_Logger);
_DataMessageHandler.LoginMessageReceived += _DataMessageHandler_LoginMessageReceived;
_DataMessageHandler.ChatMessageReceived += _DataMessageHandler_ChatMessageReceived;
}
/// <summary>
/// This will run the server main loop. This should generally be waited on the other side (<c>RunServer().Wait()</c> or something).
/// </summary>
/// <returns>A <c>Task</c> that contains the work for the <see cref="NetworkServer"/> to do.</returns>
/// <remarks>
/// Why am I documenting this? It's only ever used once.
///
/// Because it's a publicly facing method, damnit!
/// </remarks>
public Task RunServer()
{
return Task.Run(() =>
{
_MainNetServer.Start();
_Logger.LogImportant("Server starting on socket {0}...", _MainNetServer.Socket.LocalEndPoint);
DateTime started = DateTime.UtcNow;
//_Logger.LogImportant(_Logger.FormatMessage("The server was started on {0} at {1}.", started.ToString("dd-MM-yyyy"), started.ToString("HH:mm:ss.fffffff")));
_Logger.LogImportant("The server was started at: {0}", started.ToString("O"));
while (true)
{
NetIncomingMessage msg;
while ((msg = _MainNetServer.ReadMessage()) != null)
{
switch (msg.MessageType)
{
case NetIncomingMessageType.VerboseDebugMessage:
case NetIncomingMessageType.DebugMessage:
case NetIncomingMessageType.WarningMessage:
case NetIncomingMessageType.ErrorMessage:
ProcessErrorMessage(msg);
break;
case NetIncomingMessageType.StatusChanged:
ProcessStatusChangeMessage(msg.SenderConnection);
break;
case NetIncomingMessageType.Data:
ProcessDataMessage(msg);
break;
default:
_Logger.LogWarning("Unhandled type: {0}", msg.MessageType);
break;
}
_MainNetServer.Recycle(msg);
}
System.Threading.Thread.Sleep(1);
}
});
}
private void ProcessDataMessage(NetIncomingMessage message)
{
_Logger.LogInformation("Data recieved from: {0}, payload size: {1} bytes", message.SenderConnection.RemoteEndPoint, message.LengthBytes);
IDataMessage messageResult = _DataMessageHandler.HandleMessage(message);
}
private void ProcessErrorMessage(NetIncomingMessage message)
{
_Logger.LogError("Network error: ", message.ReadString());
}
private void AddClient(NetConnection client)
{
if (_ConnectedClients.Add(client))
_Logger.LogInformation("New client discovered: {0}", client.RemoteEndPoint);
else
_Logger.LogError("Duplicate client discovered: {0}", client.RemoteEndPoint);
}
private void RemoveClient(NetConnection client)
{
if (_ConnectedClients.Contains(client))
_ConnectedClients.Remove(client);
_Logger.LogInformation("Client disconnected: {0}", client.RemoteEndPoint);
}
private void ProcessStatusChangeMessage(NetConnection connection)
{
switch (connection.Status)
{
case NetConnectionStatus.Disconnected:
RemoveClient(connection);
break;
case NetConnectionStatus.Connected:
AddClient(connection);
break;
default:
_Logger.LogWarning("Unhandled StatusChanged: {0} now {1}", connection.RemoteEndPoint, connection.Status);
break;
}
}
private void _DataMessageHandler_LoginMessageReceived(object sender, LoginMessageEventArgs e)
{
NetConnection client = e.Message.Client;
string username = e.Message.Username;
byte[] password = e.Message.Password;
_Logger.LogInformation("Client ({0}) attempted login: {1}", client.RemoteEndPoint, username);
}
private void _DataMessageHandler_ChatMessageReceived(object sender, ChatMessageEventArgs e)
{
NetConnection client = e.Message.Client;
string message = e.Message.Message;
_Logger.LogInformation("Client ({0}) attempted chat: {1}", client.RemoteEndPoint, message);
}
}
The new ChatMessage
:
public class ChatMessage : IDataMessage
{
private readonly ILogger _Logger;
public ILogger Logger { get { return _Logger; } }
private NetConnection _Client;
public NetConnection Client { get { return _Client; } }
public DataMessageType Type { get { return DataMessageType.ChatMessage; } }
private string _Message;
public string Message { get { return _Message; } }
public ChatMessage(ILogger logger)
{
_Logger = logger;
}
public ChatMessage(ILogger logger, string message)
: this(logger)
{
_Message = message;
}
public void WriteNetworkMessage(NetOutgoingMessage message)
{
message.Write((int)Type);
message.Write(_Message);
}
public void ReadNetworkMessage(NetIncomingMessage message)
{
_Client = message.SenderConnection;
_Message = message.ReadString();
}
}
And lastly, the enum:
public enum DataMessageType
{
Login,
ChatMessage,
}
As you can see, there are still a few changes that need made for each message I add, though they are changes I am "OK" with:
- I need to add an event and event methods (Easy).
- I need to add event args maybe (Not always).
- I need to subscribe to the new event (Easy).
- I need to throw the new event (Easy).
This makes it easier to track what changes need made (makes sense, we're throwing an event for each message!).
The problem: the number of events could get out of hand. (How out of hand? I'm not sure yet!)
Any and all suggestions are welcome. I have stated my main concerns, I would appreciate if the brunt of the focus were on them.