Network Packet Serializer
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;
}
}