Photon Server Serializable Data Contracts Using Reflection
I’ve recently started playing with Photon Server, working on porting some server side game logic from a Node.js server. The main appeal of Photon to me is the promise of structured, strongly typed messages and general neatness of typed code, and the ability to share definitions between the server and the client when working with a Unity3D based client. So far I’m having lots of fun, and Photon seems to deliver on it’s promise. But I’ll write about that in another post.
Photon handles message serialization using byte-object dictionaries, which results in easily deserializable messages since all fields have a fixed identifier. The Photon SDK even has a neat base class called DataContract which provides an out of the box class to dictionary serialization method.
So far is all sounds great, right? The only problem is that the DataContract class is available only for the server SDK and not for the client SDK. Reading up on their forums, the reasons for that seem to vary from not being a highly requested feature to the performance overhead in client code (namely Unity and it’s aged Mono runtime). What this means is that it’s impossible to share message definitions between the server and the client with the provided out of the box solution. Bummer!
But wait! what if I implement this feature myself? Well, turns out it’s pretty straight forward!
First of all, we need to define our own custom attribute that will be used to mark serialized fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; namespace Common.Serialization { [AttributeUsage( AttributeTargets.Field, AllowMultiple = false, Inherited = true ) ] public class DataFieldAttribute : Attribute { public byte code; public bool optional; public DataFieldAttribute() { code = 0; optional = true; } } } |
Next, we’ll define a serializable to byte-object dictionary base class.
The class will feature 2 constructors – the first, a default constructor, and second a constructor that takes a byte-object dictionary and uses it to create a class instance. Additionally, we’ll add a ToParameters() method which serializes the class to a byte-object dictionary.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using System; using System.Collections.Generic; namespace Common.Serialization { // Defines a base class that handles byte,object dictionary serialization for unified server/client operation and event definitions public class SerializableMapBase { public SerializableMapBase() { } public SerializableMapBase( Dictionary< byte, object > param ) { ReflectiveMapSerializer.Deserialize( this, param ); } public Dictionary< byte, object > ToParameters() { return ReflectiveMapSerializer.Serialize( this ); } } } |
And the last piece of the puzzle, as you might have guessed from the definition above, is the reflective serializer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
using System; using System.Reflection; using System.Collections.Generic; namespace Common.Serialization { class ReflectiveMapSerializer { private class SerializedType { public Type type; public SerializedField[] fields; public SerializedType( Type t ) { type = t; FieldInfo[] allFields = t.GetFields(); List< SerializedField > serializedFields = new List< SerializedField >(); for ( int i = 0; i < allFields.Length; i++ ) { DataFieldAttribute attr = ( DataFieldAttribute )Attribute.GetCustomAttribute( allFields[ i ], typeof( DataFieldAttribute ) ); if ( attr != null ) { serializedFields.Add( new SerializedField( allFields[ i ], attr ) ); } } fields = serializedFields.ToArray(); } } private class SerializedField { public DataFieldAttribute attribute; public FieldInfo fieldInfo; public SerializedField( FieldInfo field, DataFieldAttribute attr ) { fieldInfo = field; attribute = attr; } } // Lazy serializer instantiation // Serializer is thread static, which in a worst case scenario can create per thread redundancy of serialized types definitions, // But does not require locking for type lookup. // Need to profile and see if this approach is better than locking during lookup private static ReflectiveMapSerializer instance { get { if ( _instance == null ) { _instance = new ReflectiveMapSerializer(); } return _instance; } } [ThreadStatic] private static ReflectiveMapSerializer _instance; public static Dictionary< byte, object > Serialize< T >( T obj ) { return instance.DoSerialize( obj ); } public static void Deserialize< T >( T obj, Dictionary< byte, object > param ) { instance.DoDeserialize( obj, param ); } // Instance implementation private Dictionary< Type, SerializedType > m_serializedTypeMap; public ReflectiveMapSerializer() { m_serializedTypeMap = new Dictionary<Type, SerializedType>(); } // Resolve runtime type and update cache if necessary private SerializedType GetSerializedType< T >( T obj ) { Type type = obj.GetType(); if ( !m_serializedTypeMap.ContainsKey( type ) ) { SerializedType serializedType = new SerializedType( type ); m_serializedTypeMap.Add( type, serializedType ); } return m_serializedTypeMap[ type ]; } private Dictionary< byte, object > DoSerialize< T >( T obj ) { SerializedType type = GetSerializedType( obj ); Dictionary< byte, object > map = new Dictionary<byte, object>(); for ( int i = 0; i < type.fields.Length; i++ ) { object val = type.fields[ i ].fieldInfo.GetValue( obj ); byte code = type.fields[ i ].attribute.code; bool opt = type.fields[ i ].attribute.optional; if ( val == null && !opt ) { throw new ArgumentNullException( type.fields[ i ].fieldInfo.Name ); } else { map.Add( code, val ); } } return map; } private void DoDeserialize< T >( T obj, Dictionary< byte, object > param ) { SerializedType type = GetSerializedType( obj ); for ( int i = 0; i < type.fields.Length; i++ ) { byte code = type.fields[ i ].attribute.code; bool opt = type.fields[ i ].attribute.optional; if ( !param.ContainsKey( code ) && !opt ) { throw new ArgumentNullException( type.fields[ i ].fieldInfo.Name ); } else { type.fields[ i ].fieldInfo.SetValue( obj, param[ code ] ); } } } } } |
That’s the largest piece of code in the entire setup. To sum it up in one sentence, it looks up the runtime type of the object and using reflection finds all the fields marked with the DataField attribute, and simply dumps the data in a dictionary with the key being the code specified in the attribute.
And just for completeness, here’s a sample of a serializable class implementing SerializableMapBase looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class OperationCreateUser : SerializableMapBase { [DataField( code=1, optional=false )] public string userId; [DataField( code=2, optional=false )] public string password; [DataField( code=3, optional=false )] public string displayName; public OperationCreateUser( string user, string pass, string name ) : base() { userId = user; password = pass; displayName = name; } public OperationCreateUser( Dictionary< byte, object > param ) : base( param ) { } } |
Verdict Time – Does it Blend?
Being able to implement a feature only helps us as long as it actually works – but does this approach work?
The short answer is both yes and no and it really depends.
Performance wise, using reflection has marginal impact on performance, calling message.ToParameters() 100,000 times per frame in a tight loop times at ~< 0.9msec on my AMD A10, however, this generates LOTS of garbage (~37MB for the above scenario) - this in turns degrades performance greatly. However, there are several things to keep in mind - first of all, especially with regards to network serialization, you'll probably never end up serializing 100,000 messages per frame (that's 6,000,000 messages per second), second - some of the memory overhead can be optimized by creating a non-allocating serialization method and reusing the same object, however, there is still the implicit box/unboxing involved with casting value types to object which cannot be helped. To summarize - this method works for cases where the data rate is low but definitely not for tight loops and time critical segments. Think authentication, chat, stats synchronization - all of those are cases where this approach would work.