1
\$\begingroup\$

Here is a packet serialization system.

It takes the class of the message its sending, uses reflection to fetch the property's and auto maps it to byte stream equivalent as if we had written it ourselves. It just provides a much cleaner way to serialize outgoing data especially when using collections.

First, its important to understand the old way we did things. I've shown the most minimal writers for this example for readability purposes, but some are more complex where the benefit of the transition is greater.

Old example:

public class CatalogModeWriter : NetworkPacketWriter
{
    public CatalogModeWriter(int mode)
    {
        WriteShort(ServerPacketId.CatalogMode);
        WriteInteger(mode);
    }
}

New example:

[PacketId(ServerPacketId.CatalogMode)]
public class CatalogModeWriter : AbstractPacketWriter
{
    public int Mode { get; set; }
}

Each writer now uses AbstractPacketWriter, which just provides the ability to override an OnSerialize method.

public abstract class AbstractPacketWriter : NetworkPacketWriter
{
    public Dictionary<PropertyInfo, Action<NetworkPacketWriter>> RulesForSerialize { get; } = new();
    
    public virtual void OnSerialize()
    {
        
    }

    protected void AddRule(PropertyInfo propertyInfo, Action<NetworkPacketWriter> function)
    {
        RulesForSerialize.Add(propertyInfo, function);
    }
}

It provides writers to implement serialization rules for custom formatting like so:

public override void OnSerialize()
{
    var property = PropertyHelper<CatalogModeWriter>.GetProperty(x => x.Mode);
    
    AddRule(property, writer =>
    {
        writer.WriteInteger(Mode + 5);
    });
}

Here is the serializer class, its job is to take the object and convert it to a byte array using reflection.

public class NetworkPacketSerializer
{
    private static void InvokeSerializeRuleInitialization(object packet)
    {
        var serializeOverrideMethod = packet.GetType().GetMethod("OnSerialize");
        serializeOverrideMethod?.Invoke(packet, []);
    }

    private static void AddObjectToWriter(object packet, NetworkPacketWriter writer)
    {
        var properties = packet.GetType().GetProperties();

        var identifierAttribute = packet.GetType().GetCustomAttribute<PacketIdAttribute>();

        if (identifierAttribute == null)
        {
            throw new InvalidOperationException($"Missing packet identifier attribute for packet type {packet.GetType()}");
        }
        
        writer.WriteShort(identifierAttribute.Id);

        var ruleMap = (Dictionary<PropertyInfo, Func<PropertyInfo, object>>)
            packet
                .GetType()
                .BaseType
                .GetProperty("Rules")
                .GetValue(packet);

        foreach (var property in properties)
        {
            if (ruleMap.ContainsKey(property))
            {
                ruleMap[property].Invoke(property);
            }
            else
            {
                WriteProperty(property, writer, packet);
            }
        }
    }
    
    public static IEnumerable<byte> SerializeToBytes(object packet)
    {
        InvokeSerializeRuleInitialization(packet);
        
        var writer = new NetworkPacketWriter();
        AddObjectToWriter(packet, writer);
        return writer.GetAllBytes();
    }

    private static void WriteStringCollectionPropertyToWriter(List<string> list, NetworkPacketWriter writer)
    {
        writer.WriteInteger(list.Count);
            
        foreach (var item in list)
        {
            writer.WriteString(item);
        }
    }

    private static void WriteArbitraryCollectionPropertyToWriter(PropertyInfo propertyInfo, NetworkPacketWriter writer, object packet)
    {
        var collection = (List<object>)propertyInfo.GetValue(packet)!;

        writer.WriteInteger(collection.Count);
        
        foreach(var element in collection)
        {
            foreach (var elementProperty in element.GetType().GetProperties().Where(p => Attribute.IsDefined(p, typeof(PacketDataAttribute))))
            {
                WriteProperty(elementProperty, writer, packet);
            }
        }
    }
    
    private static void WriteProperty(PropertyInfo property, NetworkPacketWriter writer, object packet)
    {
        var dataTypeSet = Attribute.IsDefined(property, typeof(PacketDataTypeAttribute));
        var dataType = property.GetCustomAttribute<PacketDataTypeAttribute>();

        var type = !dataTypeSet
            ? property.PropertyType
            : property.GetCustomAttribute<PacketDataTypeAttribute>()!.Type;
        
        if (type == typeof(string))
        {
            writer.WriteString((property.GetValue(packet) as string)!);
        }
        else if (type == typeof(int))
        {
            var i = dataTypeSet && dataType.Type == typeof(bool)
                ? (bool)property.GetValue(packet)! ? 1 : 0
                : (int)property.GetValue(packet)!;
            
            writer.WriteInteger(i);
        }
        else if (type == typeof(bool))
        {
            writer.WriteBool((bool) property.GetValue(packet)!);
        }
        else if (type == typeof(List<string>))
        {
            var collection = (List<string>)property.GetValue(packet)!;
            WriteStringCollectionPropertyToWriter(collection, writer);
        }
        else if (type == typeof(List<>))
        {
            WriteArbitraryCollectionPropertyToWriter(property, writer, packet);
        }
    }
}

It can then be used like so:

var writer = new CatalogModeWriter
{
    Mode = 1
};

var bytes = 

NetworkPacketSerializer.SerializeToBytes(writer);

Anything below here is just in case the reviewer wants to know how further areas of the code base work and isn't part of the main code under review here

NetworkPacketWriter

public class NetworkPacketWriter
{
    private readonly ArrayBufferWriter<byte> _packet = new();

    public void WriteString(string data)
    {
        WriteShort((short) data.Length);
        WriteBytes(Encoding.Default.GetBytes(data));
    }

    private void WriteBytes(byte[] data, bool reverse = false)
    {
        _packet.Write(reverse ? data.Reverse().ToArray() : data);
    }

    public void WriteShort(short data)
    {
        WriteBytes(BitConverter.GetBytes(data), true);
    }

    public void WriteInteger(int data)
    {
        WriteBytes(BitConverter.GetBytes(data), true);
    }

    protected void WriteLong(long data)
    {
        WriteBytes(BitConverter.GetBytes((int)data), true);
    }

    public void WriteBool(bool boolean)
    {
        WriteBytes(new[] {(byte) (boolean ? 1 : 0)});
    }

    public byte[] GetAllBytes()
    {
        var bytes = new List<byte>();
            
        bytes.AddRange(BitConverter.GetBytes(_packet.WrittenCount));
        bytes.Reverse();
        bytes.AddRange(_packet.WrittenSpan.ToArray());

        return bytes.ToArray();
    }
}

Attribute classes:

internal class PacketIdAttribute(short i) : Attribute
{
    public short Id { get; } = i;
}
\$\endgroup\$
1
  • \$\begingroup\$ The order of the properties returned by reflection is not guaranteed so your packet format is unknown. How do you deserialize these arrays of bytes on the other side? I'd suggest you choose an off-the-shelf solution for this, e.g. protobuf-net \$\endgroup\$
    – RobH
    Commented Apr 29 at 12:43

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.