Network Packet Serializer

From XNAWiki
Jump to: navigation, search

Introduction

For the ease of many people like myself, I've written up a small class to handle automatic serialization of objects to and from a PacketWriter or PacketReader, respectively. The goal is a simple way for people to send network data without the annoyance of repetitive, mundane code. The class is structured to mimic the way XmlSerializer works so as to be familiar to .NET developers.

Supported Types

The class is able to handle any packet type that contains one or more of the following basic types:

  • Matrix
  • Quaternion
  • Vector2
  • Vector3
  • Vector4
  • Boolean
  • Byte
  • Char
  • Decimal
  • Double
  • Int16 (short)
  • Int32 (int)
  • Int64 (long)
  • SByte
  • Single (float)
  • String
  • UInt16 (ushort)
  • UInt32 (uint)
  • UInt64 (ulong)

Example

As an example type, here's a small structure that could be used as a packet along with a demonstration of how to serialize and deserialize that type:

public struct TestPacket
{
	public int Value;
	public bool Another;
 
	[PacketIgnore]
	public Vector4 Complex; // this field is not serialized into the packet
}
 
// create the serializer along with a PacketWriter and PacketReader
PacketSerializer packetSerializer = new PacketSerializer(typeof(TestPacket));
PacketWriter writer = new PacketWriter();
PacketReader reader = new PacketReader();
 
// serialize and send some test data
TestPacket p = new TestPacket();
p.Another = false;
p.Value = 153;
p.Complex = new Vector4(
	(float)rand.NextDouble(),
	(float)rand.NextDouble(),
	(float)rand.NextDouble(),
	(float)rand.NextDouble());
packetSerializer.Serialize(writer, p);
session.LocalGamers[0].SendData(writer, SendDataOptions.None);
 
// the recipient would then deserialize the packet to get the resulting data. 
// Remember that the Complex field is ignored so it will not get a value.
foreach (LocalNetworkGamer gamer in session.LocalGamers)
{
	while (gamer.IsDataAvailable)
	{
		NetworkGamer sender;
		gamer.ReceiveData(reader, out sender);
		TestPacket p = (TestPacket)packetSerializer.Deserialize(reader);
	}
}

As you can see this provides a nice level of ease when it comes to sending and receiving data by automatically serializing out the basic types. One could extend the NetworkPacketSerializer to include support for custom serialization for more complex types if they so chose.

Source Code

First is the simple code for the PacketIgnoreAttribute we use to ignore certain fields and properties during serialization:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class PacketIgnoreAttribute : Attribute { }

And here is the full code for the serializer. The class uses caching to try and cut back on the cost of reflection. Ideally you would create one instance of the PacketSerializer per type you plan to serialize and keep it around for the duration of your game.

public class PacketSerializer
{
	// the list of types our serializer supports
	private static readonly List<Type> validTypes = new List<Type>
	{
		typeof(Matrix),
		typeof(Quaternion),
		typeof(Vector2),
		typeof(Vector3),
		typeof(Vector4),
		typeof(Boolean),
		typeof(Byte),
		typeof(Char),
		typeof(Decimal),
		typeof(Double),
		typeof(Int16),
		typeof(Int32),
		typeof(Int64),
		typeof(SByte),
		typeof(Single),
		typeof(String),
		typeof(UInt16),
		typeof(UInt32),
		typeof(UInt64),
	};
 
	// cached methods for the PacketWriter's methods (by Type of data being written)
	private static readonly Dictionary<Type, MethodInfo> writerMethods = 
		new Dictionary<Type, MethodInfo>();
 
	// cached methods for the PacketReaders's methods (by Type of data being read)
	private static readonly Dictionary<Type, MethodInfo> readerMethods = 
		new Dictionary<Type, MethodInfo>();
 
	// the type specific fields and properties being serialized
	private readonly FieldInfo[] typeFields;
	private readonly PropertyInfo[] typeProperties;
 
	// the type being serialized
	private readonly Type type;
 
	// a static constructor for generating our PacketWriter/PacketReader caches
	static PacketSerializer()
	{
		// go through each valid type
		foreach (Type t in validTypes)
		{
			// get the PacketWriter.Write method for the type and store it
			MethodInfo method = typeof(PacketWriter).GetMethod("Write", new[] { t });
			writerMethods.Add(t, method);
 
			// get the PacketReader.Read method for the type and store it
			string readerMethodName = "Read" + t.ToString().Substring(t.ToString().LastIndexOf(".") + 1);
			method = typeof(PacketReader).GetMethod(readerMethodName);
			readerMethods.Add(t, method);
		}
	}
 
	public PacketSerializer(Type type)
	{
		// store the type being serialized
		this.type = type;
 
		// get all of the type's fields and properties
		List<FieldInfo> fields = 
			new List<FieldInfo>(type.GetFields(BindingFlags.Instance | BindingFlags.Public));
		List<PropertyInfo> properties = 
			new List<PropertyInfo>(type.GetProperties(BindingFlags.Instance | BindingFlags.Public));
 
		// for each field...
		for (int i = fields.Count - 1; i >= 0; i--)
		{
			// see if the field has a PacketIgnoreAttribute on it
			FieldInfo f = fields[i];
			object[] attrs =
				f.GetCustomAttributes(typeof(PacketIgnoreAttribute), true);
 
			// if so remove that field
			if (attrs != null && attrs.Length > 0)
			{
				fields.RemoveAt(i);
				continue;
			}
 
			// if the field is of an unsupported type, throw an exception
			if (!validTypes.Contains(f.FieldType))
				throw new Exception(string.Format(
					"Cannot serialize type {0} because the field {1} is not supported.", 
					type, 
					f));
		}
 
		// for each property...
		for (int i = properties.Count - 1; i >= 0; i--)
		{
			// see if the property has a PacketIgnoreAttribute on it
			PropertyInfo p = properties[i];
			object[] attrs =
				p.GetCustomAttributes(typeof(PacketIgnoreAttribute), true);
 
			// if so remove that property
			if (attrs != null && attrs.Length > 0)
			{
				properties.RemoveAt(i);
				continue;
			}
 
			// if the property is of an unsupported type, throw an exception
			if (!validTypes.Contains(p.PropertyType))
				throw new Exception(string.Format(
					"Cannot serialize type {0} because the property {1} is not supported.", 
					type,
					p));
		}
 
		// store the final results in our arrays
		typeFields = fields.ToArray();
		typeProperties = properties.ToArray();
	}
 
	public void Serialize(PacketWriter writer, object packet)
	{
		// verify that the packet is the correct type
		if (!packet.GetType().Equals(type))
			throw new Exception(string.Format(
				"Cannot serialize packet of type {0} using a serializer for type {1}", 
				packet.GetType(), 
				type));
 
		// write all the fields and properties using our cached PacketWriter methods
		foreach (FieldInfo f in typeFields)
			writerMethods[f.FieldType].Invoke(writer, new[] { f.GetValue(packet) });
		foreach (PropertyInfo p in typeProperties)
			writerMethods[p.PropertyType].Invoke(writer, new[] { p.GetValue(packet, null) });
	}
 
	public object Deserialize(PacketReader reader)
	{
		// create a new instance of the type
		object packet = Activator.CreateInstance(type);
 
		// read in all the field and properties using our cached PacketReader methods
		foreach (FieldInfo f in typeFields)
			f.SetValue(packet, readerMethods[f.FieldType].Invoke(reader, null));
		foreach (PropertyInfo p in typeProperties)
			p.SetValue(packet, readerMethods[p.PropertyType].Invoke(reader, null), null);
 
		// return the packet
		return packet;
	}
}