UDP Server update: Datagram processing

Published March 12, 2016
Advertisement

So the server has started taking some form over the last couple of days. I've got the datagram message parsing put together and have a test client app sending data to the server for consumption without issues. There is a lot of work still to do, like handle packet loss, deal with packet ordering and then deal with multi-packet messages.

There are a few things that need to happen when the datagram first arrives to the server.


  • Parse the contents of the message
  • Determine the channel that the message belongs to
  • Store the datagram if it belongs to a multi-part message, until all of the messages have come through.


The parsing of the datagram was straightforward and was covered a little in my previous post. I've since done a bit of refactoring and cleaned up the way that the datagrams handle the binary data reading and writing. Now, the server creates the BinaryReader when a Datagram arrives. This allows the server to read the datagram header and route the message accordingly to the correct channel. Upon arriving at the channel, the channel can resume reading the binary stream from the position that the server left off at while reading the header. This allows me to not read the header contents twice, by sharing the same reader. I don't have channel routing in place yet, so I'm using a factory to handle instantiating the datagram message object and letting the server interact with it directly.private void ReceiveClientData(IAsyncResult result){ // Pull our the connection state and it's data PacketState state = (PacketState)result.AsyncState; Socket socket = state.UdpSocket; EndPoint endPoint = state.Destination; int receivedData = socket.EndReceiveFrom(result, ref endPoint); if (receivedData == 0) { this.ListenForData(socket); return; } // Create a binary reader so we can deserialize the bytes delivered into an IMessage implementation var reader = new DatagramReader(state.Buffer); // Read the header in from the buffer first so we know what kind of message and how to route. var header = new ClientHeader(); header.GetMessageFromBytes(state.Buffer, reader); if (!header.IsMessageValid() && header.IsRequiredMessage) { // Handle sending a request back to the client. // this.SendMessage(new ResendDatagramMessage(header)); } // Now we have the header, ask the factory to create the datagram message for us. // In this example, 'datagram' will represent a Handshake class. IDatagram datagram = this.datagramFactory.CreateDatagramFromHeader(header); if (datagram == null) { // TODO: handle null } // Tell the datagram to pull the data out of the buffer and populate it's values. datagram.GetMessageFromBytes(state.Buffer, reader); // Clean up and grab the next socket buffer if available. reader.Dispose(); this.ListenForData(socket);}
This little bit of code is the current heart of my socket listener. There is a lot of work to be done here, such as caching the IPEndPoint for sending data back, creating a User object for future referencing and more. What I want to show here though is that the server doesn't need to do any switch statements based off of the datagram message arriving from the client. Instead, a factory is used to figure it out. Once the factory gives back a datagram, the server tells it to read the data from the packet so we can react to it.


The factory itself is pretty simple. It is given a collection of every IDatagram implementation available for caching. It runs through the collection and maps the protocol attribute (shown further down) to the Type representing an IDatagram implementation. When the factory is asked to create an instance of the IDatagram that is defined in the header, it just pulls the correct Type from cache, based off of the Protocol Attribute key. A new instance of the IDatagram implementation is then returned to the server.public sealed class DatagramFactory{ private static readonly Dictionary datagrams = new Dictionary(); public DatagramFactory(IEnumerable datagramTypes) { foreach (Type datagram in datagramTypes) { if (datagram.IsAbstract || !datagram.IsClass) { continue; } ProtocolVersionAttribute protocolVersion = AttributeCache.GetAttribute(datagram); DatagramFactory.datagrams.Add(protocolVersion.DatagramName, datagram); } } public IDatagram CreateDatagramFromHeader(ClientHeader header) { Type datagramType = null; if (!DatagramFactory.datagrams.TryGetValue(header.MessageName, out datagramType)) { return null; } IDatagram datagram = (IDatagram)Activator.CreateInstance(datagramType); // TODO: Figure out how to assign the header without using reflection; it needs to be on the interface in some manor. datagram.GetType().GetProperty("Header")?.SetValue(datagram, header); return datagram; }}
I needed a way to map the packet message to a version of the messaging protocol i'm using, along with define what channel it belongs to. I did that using attributes, and a caching strategy. During the server startup, I cache all of the IDatagram message implementations and their associated Attribute. This way, when messages come in, I don't have to use any reflection. I will pull the attribute and the Type information out of cache and instantiate it.[AttributeUsage(validOn: AttributeTargets.Class, Inherited = false)]public class ProtocolVersionAttribute : Attribute{ public ProtocolVersionAttribute(int targetVersion, string datagramName, DatagramChannels category) { this.TargetProtocolVersion = targetVersion; this.Category = category; this.DatagramName = datagramName; } public int TargetProtocolVersion { get; } public string DatagramName { get; } public DatagramChannels Category { get; }}
This is what the Attribute looks like. It holds the protocol version, the name of the datagram and the category. This allows my factory to look at the message name passed in from the datagram packet, and find a matching IDatagram implementation.


The following is an example IDatagram implementation. The handshake message would originate from the game client, and the server would consume it and turn it into a Handshake object.[ProtocolVersion(1, DatagramNames.Account.Handshake, DatagramChannels.Account)]public class Handshake : ClientDatagramBase{ public string WelcomeMessage { get; private set; } = "Hello"; public bool IsFreshStart { get; set; } protected override void WriteData(BinaryWriter serializer) { serializer.Write(this.WelcomeMessage); serializer.Write(this.IsFreshStart); } protected override void ReadData(BinaryReader deserializer) { this.WelcomeMessage = deserializer.ReadString(); this.IsFreshStart = deserializer.ReadBoolean(); } public override bool IsMessageValid() { return string.IsNullOrEmpty(this.WelcomeMessage); }}

This works out pretty well thus far. Nice and clean, which lets me quickly add new datagram packet messages. It's not shown here, but the base class has a ClientHeader property, which represents the header we parsed out up above in the server code.


So that solves for parsing the datagram packet content. What about determining what channel each message belongs to? Well I've used the attribute to define that but I've not done anything with it. I'm exploring multi-casting so that I can optionally route different channels to different IPEndPoints if needed. Something like a chat EndPoint that I could put on an independent box to handle the verbose chat traffic, and grouping non-verbose channels together on a single server box. I'm still working through the design on that one. The channel needs to be flexible in the event that I want to filter chat, or locations based on OS, Protocol Version and the contents of the message.

The last item, storing of the datagram for multi-part messages, will be worked on over the weekend. I need to get the user connection class created and wired up with the server before i start dealing with multi-part messages though.

1 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement
Advertisement