Plugable serializers for ISFSObjects

Post here your suggestions for new possible features in SmartFoxServer 2X.

Moderators: Lapo, Bax

genar
Posts: 137
Joined: 13 Jul 2017, 11:49

Plugable serializers for ISFSObjects

Postby genar » 12 Mar 2021, 23:49

So it would be really, really great if there would be any mechanic to convert Pojos and complex game-classes to ISFSObjects via reflection and CUSTOM serializers. Especially the custom serializers are very important here. Almost every single network library that i know has some sort of serializer interface that is used to stream or convert game classes and its actually pretty important. This could look like the following...

Code: Select all


public class Transform{
   public Vector2 position;
   public Quaternion rotation;
   public Vector2 scale;
}

var serializer = new SFSObjectSerializer();
serializer.addSerializer(Transform.class, new ISerializer(){
 
   void serialize(Transform){ // Custom Logic  }
   Transform deserialize(){ //Custom logic }
});

var packet = serializer.from(new Transform());  // By custom serializer
var nextPacket = serializer.from(new SuperCoolClass()); // Reflection, if attribute has its own serializer, then use that one instead of reflection


This would be very, very usefull... atleast i suffer from the current mechanic. I simply cant extend some of my third party classes to add my own custom toSFSObject(); methods. So i would love to plugin my own serializer instead of using OOP.

I actually came up with my own solution in an few hours, which is able to parse simple and complex class structures into ISFSObject hierarchys for sending them. And i also managed to implement custom serialization by using interfaces. My approach looks like this, there probably many bugs left or unsupported data types. But this is already working for some of my cases ( Note, deserialization not finished yet ) ...

Code: Select all


import com.smartfoxserver.v2.entities.data.*;
import org.apache.commons.lang3.ArrayUtils;

import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

/**
 * A serializer that is able to serialize and deserialize almost every object by reflection or custom serializers.
 * It makes use of {@link ISFSObject} for this process.
 */
public class NetworkSerializer {

    /** Used to custom serialize certain classes and deserialize them back to entities **/
    public interface ISerializer<T>{
        ISFSObject serialize(NetworkSerializer serializer, T obj, Object meta);
        T deserialize(NetworkSerializer serializer, ISFSObject packet, Object meta);
    }

    /** Stores all available serializers **/
    private Map<Class, ISerializer> serializers = new HashMap<>();

    /**
     * Adds a new serializer.
     * @param clazz The class we wanna serialize
     * @param serializer The used serializer
     * @param <T> The type
     */
    public <T> void addSerializer(Class<T> clazz, ISerializer<T> serializer){ this.serializers.put(clazz, serializer); }

    /**
     * Removes an existing serializer.
     * @param clazz The class we wanna delete the serializer for
     * @param <T> The type
     */
    public <T> void removeSerializer(Class<T> clazz){ this.serializers.remove(clazz); }

    /**
     * Returns an existing serializer or null.
     * @param clazz The class we wanna get the serializer for
     * @param <T> The type
     * @return The existing serializer or null
     */
    public <T> ISerializer getSerializer(Class<T> clazz){ return serializers.get(clazz); }

    /**
     * Serializes a passed object into a {@link ISFSObject}
     * @param obj The object to serialize
     * @return A packet which contains the serialized entity
     */
    public ISFSObject toPacket(final Object obj) {

        // Create packet and add ID/Classname
        var clazz = obj.getClass();
        var packet = new SFSObject();
        packet.putUtfString("c", clazz.getCanonicalName());
 
        // When theres a serialier use that one, otherwhise use reflection
        var serializer = getSerializer(clazz);
        if (serializer != null) {

            // Serialize and merge the created object into the existing one
            var entityPacket = serializer.serialize(this, obj, null);
            merge(packet, entityPacket);
        } else {

            // Convert all attributes
            var fields = clazz.getDeclaredFields();
            for (var index = fields.length - 1; index >= 0; index--) {

                var field = fields[index];
                var modifier = field.getModifiers();
                var isTransistent = Modifier.isTransient(modifier);
                var isStatic = Modifier.isStatic(modifier);

                // If not transistent, convert
                if (!isTransistent && !isStatic) {

                    try {

                        field.setAccessible(true);
                        var fieldName = field.getName();
                        var fieldValue = field.get(obj);
                        var type = toType(fieldValue);

                        // Skip when theres a null value
                        if (type == SFSDataType.NULL) continue;

                        // Convert attribute either using reflection or using a custom serializer
                        var wrapped = wrap(type, fieldValue);
                        packet.put(fieldName, wrapped);
                    } catch (Exception e) { e.printStackTrace(); }
                }
            }
        }

        return packet;
    }

    // TODO : Deserialization of packets
    public <T> T fromPacket(final ISFSObject packet){ return null; }

    /**
     * Checks the type of an object and returns the sfs-type. That one determines how it will be presented inside the packet
     * @param value The object
     * @return Its packet type
     */
    public static SFSDataType toType(Object value) {

        if (value == null) return SFSDataType.NULL;
        else {

            SFSDataType wrapper = null;
            if (value instanceof Boolean) {
                wrapper = SFSDataType.BOOL;
            } else if (value instanceof Byte) {
                wrapper = SFSDataType.BYTE;
            } else if (value instanceof Short) {
                wrapper = SFSDataType.SHORT;
            } else if (value instanceof Integer) {
                wrapper = SFSDataType.INT;
            } else if (value instanceof Long) {
                wrapper = SFSDataType.LONG;
            } else if (value instanceof Float) {
                wrapper = SFSDataType.FLOAT;
            } else if (value instanceof Double) {
                wrapper = SFSDataType.DOUBLE;
            } else if (value instanceof String) {
                wrapper = SFSDataType.UTF_STRING;
            } else if(value.getClass().isArray()){
                wrapper = SFSDataType.SFS_ARRAY;
            } else {
                wrapper = SFSDataType.SFS_OBJECT;
            }
            return wrapper;
        }
    }

    /**
     * Converts an array if its primitive to its boxed form.
     * @param array The primitive/Object array to convert
     * @return The converted, boxed array version
     */
    public Object[] convertArray(Object array){

        if(array instanceof boolean[]){
            var boolArray = (boolean[])array;
            return ArrayUtils.toObject(boolArray);
        } else if(array instanceof byte[]){
            var byteArray = (byte[])array;
            return ArrayUtils.toObject(byteArray);
        } else if(array instanceof short[]){
            var shortArray = (short[])array;
            return ArrayUtils.toObject(shortArray);
        } else if(array instanceof int[]){
            var intArray = (int[])array;
            return ArrayUtils.toObject(intArray);
        } else if(array instanceof long[]){
            var longArray = (long[])array;
            return ArrayUtils.toObject(longArray);
        } else if(array instanceof float[]){
            var floatArray = (float[])array;
            return ArrayUtils.toObject(floatArray);
        } else if(array instanceof double[]){
            var floatArray = (double[])array;
            return ArrayUtils.toObject(floatArray);
        } else return (Object[]) array;
    }

    /**
     * Wraps a object to a {@link SFSDataWrapper} to put it straight into {@link ISFSObject}'s.
     * Smartfoxserver then takes care of the rest.
     * @param value The object to wrap
     * @return A wrapper for that object.
     */
    public SFSDataWrapper wrap(Object value){

        var type = toType(value);
        return wrap(type, value);
    }

    /**
     * Wraps a object to a {@link SFSDataWrapper} to put it straight into {@link ISFSObject}'s.
     * Smartfoxserver then takes care of the rest.
     * @param type The type
     * @param value The object to wrap
     * @return A wrapper for that object.
     */
    public SFSDataWrapper wrap(SFSDataType type, Object value){

        if(type == SFSDataType.NULL || value == null) return new SFSDataWrapper(SFSDataType.NULL, value);
        switch (type){

            case SFS_OBJECT:

                // If theres a unknown type, create a packet and place it in the wrapper
                var packet = toPacket(value);
                return new SFSDataWrapper(type, packet);

            case SFS_ARRAY:

                // If theres an array, wrap its items one by one
                var array = convertArray(value);
                var arrayPacket = new SFSArray();
                for(var index = 0; index < array.length; ++index) {

                    var item = array[index];
                    var itemType = toType(item);

                    var wrapped = wrap(itemType, item);
                    arrayPacket.add(wrapped);
                }

                return new SFSDataWrapper(type, arrayPacket);
            default: return new SFSDataWrapper(type, value);
        }
    }

    /**
     * Merges two {@link ISFSObject}'s... overwrides possible keys.
     * @param mergeIn The one that should be merged into
     * @param toMerge The one that gets merged into the first one
     */
    public static void merge(final ISFSObject mergeIn, final ISFSObject toMerge){

        var iterator = toMerge.iterator();
        while (iterator.hasNext()) {
            var item = iterator.next();
            mergeIn.put(item.getKey(), item.getValue());
        }
    }
}



Heres a little code example...

Code: Select all


   public class CollectionSerializer implements NetworkSerializer.ISerializer<Collection> {

    @Override
    public ISFSObject serialize(NetworkSerializer serializer, Collection collection, Object meta) {

        var collectionPacket = new SFSObject();
        var itemsPacket = new SFSArray();
        collectionPacket.putSFSArray("items", itemsPacket);

        // Loop over all items, convert them using reflection/serializers and return them
        for(var item : collection){
            var wrapped = serializer.wrap(item);
            itemsPacket.add(wrapped);
        }

        return collectionPacket;
    }

    @Override
    public Collection deserialize(NetworkSerializer serializer, ISFSObject packet, Object meta) {
        return null;
    }
}

 var test = new NetworkSerializer();
 test.addSerializer(Collection.class, new CollectionSerializer());
 var tesst = test.toPacket(new Transform()); // The tranform used above
 
 Produces a ISFSObject looking like this :
 
 ISFSObject
    c : classPath
    position : ISFSObject
       c: Vector2 classpath
       x
       y
    rotation : ...


Would be great if you could implement such custom serializers as some sort of plugins instead of using inheritance and OOP.
Feel free to use my code and i hope this helps someone.
User avatar
Lapo
Site Admin
Posts: 22157
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Plugable serializers for ISFSObjects

Postby Lapo » 16 Mar 2021, 16:00

Hi,
it's an interesting concept and we have thought about it before, but there are also several issues to take in consideration.
One is compatibility with all client platforms which can limit what can be done, or exclude some of the client platforms (such as JS) which is to be expected.
Another problematic area is accessing private fields, which is kind of dangerous and can lead to all kinds of issues if classes aren't designed for this kind of "deep reflection" approach.

Then there's the issue that Java 9 (and higher) has changed the approach to reflection and it's more restrictive. For now you just get a warning and the pre-Java 9 reflection code still works, but I am not sure this is a long term strategy. Otherwise we'd need to rethink all of the reflective code for Java 9 and higher, which is out of scope for SFS2X.

There's also a final consideration: we don't encourage Class serialization that much because it's always less efficient, as it adds significant overhead to the network data and overall performance. So, making it super easy to send any serialized class would encourage a sub-optimal use of the SFS2X protocol, which is a pity. This is not to say that Class serialization should never be used. There are good use cases and bad ones.

I think we'll look into this proposal more in depth in the next iteration of SmartFoxServer.

Meanwhile you can still implement your custom serializer by adding the toSFSObject and fromSFSObject methods, as we usually suggest. In case this is inconvenient or not possible with your classes you can still use a separate Utility class to do that, so you don't have to "pollute" your game objects with serialization methods.

Cheers
Lapo
--
gotoAndPlay()
...addicted to flash games
genar
Posts: 137
Joined: 13 Jul 2017, 11:49

Re: Plugable serializers for ISFSObjects

Postby genar » 16 Mar 2021, 19:58

Lapo wrote:Hi,
it's an interesting concept and we have thought about it before, but there are also several issues to take in consideration.
One is compatibility with all client platforms which can limit what can be done, or exclude some of the client platforms (such as JS) which is to be expected.
Another problematic area is accessing private fields, which is kind of dangerous and can lead to all kinds of issues if classes aren't designed for this kind of "deep reflection" approach.

Then there's the issue that Java 9 (and higher) has changed the approach to reflection and it's more restrictive. For now you just get a warning and the pre-Java 9 reflection code still works, but I am not sure this is a long term strategy. Otherwise we'd need to rethink all of the reflective code for Java 9 and higher, which is out of scope for SFS2X.

There's also a final consideration: we don't encourage Class serialization that much because it's always less efficient, as it adds significant overhead to the network data and overall performance. So, making it super easy to send any serialized class would encourage a sub-optimal use of the SFS2X protocol, which is a pity. This is not to say that Class serialization should never be used. There are good use cases and bad ones.

I think we'll look into this proposal more in depth in the next iteration of SmartFoxServer.

Meanwhile you can still implement your custom serializer by adding the toSFSObject and fromSFSObject methods, as we usually suggest. In case this is inconvenient or not possible with your classes you can still use a separate Utility class to do that, so you don't have to "pollute" your game objects with serialization methods.

Cheers


Alright, thanks for taking that in consideration !

The deep reflection approach is not useable for all classes, in my case it works fine because im using a data oriented approach. So my classes are pretty flat. Those plugable custom serializers could become quite handy for improving the performance in comparison to the deep reflection one... kinda like "toPacket(); -> Is there a custom external pluged serializer ? -> Use this one, else check if the object implements a serialization interface -> Use the local interface, else deep reflect object".

This would improve the performance a lot and we could easily add more performant serializers. For objects which arent occuring that often, the deep serialization could be used.

No one will be forced to use this, but i think its quite handy ^^ Also quite usefull for rapid prototyping. The same concept is used by many Json converters.
Luke64
Posts: 16
Joined: 08 Nov 2020, 23:15

Re: Plugable serializers for ISFSObjects

Postby Luke64 » 07 Jul 2021, 10:09

Just to jump in on the subject with an example because we internally use Google's Protocol Buffers (PB) for communication between server and client: with a little overhead per message (like ~10 bytes I'd think) we just wrapped serialized PBs (byte[]) into an SFSObject under key "p". That way, we can use the normal transmission mechanisms of SFS2X server and client while using the more efficient and strictly defined Protocol Buffers.

(Note: in order to know which PB message is in the byte array, we introduced a container message (named "Message" ;)) which knows about all the messages that can be sent by either side, server or client:

Code: Select all

message Message {
  oneof payload {
    commands.Ping ping = 1;
    commands.Pong pong = 2;
    ...
  }
}

This approach worked fine and didn't introduce any problems so far.)
User avatar
Lapo
Site Admin
Posts: 22157
Joined: 21 Mar 2005, 09:50
Location: Italy

Re: Plugable serializers for ISFSObjects

Postby Lapo » 07 Jul 2021, 13:41

@Luke64
Thanks. Sounds like a more approachable system to implement custom serialization (and one we've recommended in several occasions too).
Pack whatever you want to send into a byte[] (using your custom serializer) and wrap it in an SFSObject.

Cheers
Lapo

--

gotoAndPlay()

...addicted to flash games
Luke64
Posts: 16
Joined: 08 Nov 2020, 23:15

Re: Plugable serializers for ISFSObjects

Postby Luke64 » 07 Jul 2021, 14:28

Lapo wrote:@Luke64
(and one we've recommended in several occasions too).
Pack whatever you want to send into a byte[] (using your custom serializer) and wrap it in an SFSObject.

I think in fact you recommended it to me. ;) Works fine :D

Return to “2X Features Wish List”

Who is online

Users browsing this forum: No registered users and 1 guest