SFS2X Docs / AdvancedTopics / class-serialization
» SFSObject/SFSArray: Class serialization
In this article we are going to explore an advanced feature provided by the SFSObject and SFSArray classes. The feature allows to exchange custom classes (POJOs) between client and server in a completely transparent fashion. Please note that this feature is not supported by our JavaScript, Objective-C and C++ APIs.
As we have seen in the introduction to SFSObject/SFSArray, you might find yourself copying data to and from your model classes into SFSObject(s) quite often in your Extension code. With Class serialization you will be able to directly send and receive your model classes without any manual conversion.
NOTE
This article touches advanced topics and requires a solid knowledge of OOP (in both Java and the client side language of choice) and familiarity with the concepts of Type Reflection and Class loading mechanism in the JVM.
» The RPG game example
In order to demonstrate the features of Class serialization we will use a real-life use case and provide the source files so you can review the code and experiment with it. The client side code for this example is provided both in C# and ActionScript 3.
For this study we are working on an RPG game. We will create several classes for the game model and we will start off with the characters, using the following properties:
- name: the character name (String)
- type: like knight, magician, priest, etc (String)
- skin: an object describing the graphics (custom class)
- properties: a dictionary with properties and related values such as strength, endurance, agility, etc (Map)
- inventory: a dictionary with physical objects owned by the character (Map)
- spells: a dictionary with spells mastered by the character (Map)
In order to model our RpgCharacter class both for server (Java) and client (C#, ActionScript 3) side, we will have to comply to a series of conventions for the serialization:
- Implement the SerializableSFSType interface. This interface has no methods, and it just works as a "marker" for serializable classes.
- Use the same exact package and class name on both sides of the application. In other words the RpgCharacter class must reside in the same package both on the server and client side.
- Provide an empty constructor (which is used by the system to instantiate the class dynamically).
- Provide public access to all serializable fields: this simply means that all properties that you want to be transported must be accessible either directly or via a public getter/setter (for C#, only direct access is currently supported). Protected, private, package-private fields cannot be serialized. Also static fields are ignored.
- Mark as transient (Java) or [NonSerialized()] (C# - excluding UWP, not yet supported) any public field that should not be serialized. Since in ActionScript 3 the transient mutator doesn't exist we emulate it by using a naming convention: public fields that should not be serialized should start with a dollar sign ($). Example: $posx, $gameId, $name.
- In C#, the following line of code is required to permit the API to access the application context:
DefaultSFSDataSerializer.RunningAssembly = Assembly.GetExecutingAssembly();
Also we must be aware that not all classes can be transported. For example anything referencing a local resource (file, socket, database connection) is not serializable. This is a list of the types that can be used with the Class serialization mechanism and how they are translated from one language to the other:
Java | C# | ActionScript 3 |
---|---|---|
null | null | null |
boolean | bool | Boolean |
byte | byte | int |
short | short | int |
int | int | int |
long | long | Number |
float | float | Number |
double | double | Number |
String | String | string |
Collection<Object> (List, Set, Queue...) |
ArrayList | Array |
Map<String, Object> | HashTable | Object |
» The example classes
The following diagram better illustrates our main RpgCharacter class:
There are three different interfaces for each of the above fields with the exclusion of CharacterProperty which is a class itself. Each interface has a number of implementations:
- Skin
- KnightSkin
- SorcererSkin
- Item
- KnifeItem
- ShieldItem
- SledgeHammerItem
- SwordItem
- MagicWandItem
- Spell
- DeathRaySpell
- RisingFlamesSpell
- SolarVortexSpell
- SpiderSwarmSpell
- WaterFloodSpell
Let's see how these interfaces, and the relative implementations, are modeled.
This is the Spell.java interface:
package sfs2x.extension.test.serialization.model; public interface Spell { String getId(); void setId(String id); int getHitPoints(); void setHitPoints(int hit); int getCount(); void cast(); }
And this is one of the implementations, WaterFloodSpell.java:
package sfs2x.extension.test.serialization.model; import com.smartfoxserver.v2.protocol.serialization.SerializableSFSType; public class WaterFloodSpell implements Spell, SerializableSFSType { String id; int hitPoints; int count = 7; public WaterFloodSpell() { // Empty constructor } public WaterFloodSpell(String id, int hitPoints) { this.id = id; this.hitPoints = hitPoints; } public String getId() { return id; } public void setId(String id) { this.id = id; } public int getHitPoints() { return hitPoints; } public void setHitPoints(int hitPoints) { this.hitPoints = hitPoints; } public int getCount() { return count; } @Override public void cast() { if (count > 0) { System.out.println("CASTING SPELL: " + id); count--; } else { System.out.println("CAN'T CAST SPELL. BUY MORE!"); } } }
As you can notice the class implements the SerializableSFSType interface and provides an empty constructor as required by the conventions previously discussed. Also all class fields that need to be exposed publicly provide access via getters and setters.
Let's now take a look at the same interface and implementation in client side language:
interface Spell { string GetId(); void SetId(string id); int GetHitPoints(); void SetHitPoints(int hitPoints); int Count(); void Cast(); }
class WaterFloodSpell : Spell, SerializableSFSType { public string id; public int hitPoints; public int count; public string GetId() { return id; } public void SetId(string id) { this.id = id; } public int GetHitPoints() { return hitPoints; } public void SetHitPoints(int hitPoints) { this.hitPoints = hitPoints; } public int Count() { return count; } public WaterFloodSpell() { } public WaterFloodSpell(string id = null, int hitPoints = 70) { this.id = id; this.hitPoints = hitPoints; } public void Cast() { Console.WriteLine("Casting " + id); } }
package sfs2x.extension.test.serialization.model { public interface Spell { function get id():String function set id(id:String):void function get hitPoints():int function set hitPoints(hit:int):void function get count():int function cast():void } }
package sfs2x.extension.test.serialization.model { import com.smartfoxserver.v2.protocol.serialization.SerializableSFSType public class WaterFloodSpell implements Spell, SerializableSFSType { private var _id:String private var _hitPoints:int private var _count:int public function WaterFloodSpell(id:String=null, hitPoints:int=70) { this._id = id this._hitPoints = hitPoints } public function get id():String { return _id } public function set id(id:String):void { this._id = id } public function get hitPoints():int { return _hitPoints } public function set hitPoints(hitPoints:int):void { this._hitPoints = hitPoints } public function get count():int { return _count } public function set count(value:int):void { _count = value } public function cast():void { trace("Casting " + _id) } } }
You can notice that both versions use the exact same package as requested by the Class serialization conventions.
Now that we have both classes on the client and server side we are ready to send them over the network in a very convenient way. This is a short example snippet, just to give you an idea.
Client side:
ISFSObject params = SFSObject.NewInstance(); params.PutBool("IsActive", true); params.PutInteger("TheNumber", 42); params.PutClass("spell", new WaterFloodSpell()) // This is our custom class instance! sfs.Send(new ExtensionRequest("test", params));
var params:ISFSObject = new SFSObject(); params.putBool("IsActive", true); params.putInteger("TheNumber", 42); params.putClass("spell", new WaterFloodSpell()) // This is our custom class instance! sfs.send(new ExtensionRequest("test", params));
Server side:
public class TestRequestHandler extends BaseClientRequestHandler { @Override public void handleClientRequest(User sender, ISFSObject params) { boolean isActive = params.getBool("IsActive"); int theNumber = params.getInt("TheNumber"); WaterFloodSpell spell = (WaterFloodSpell) params.getClass("spell"); } }
In the source code provided with this article we will create three different instances of the RpgCharacter and populate them with actual game data, which is then exchanged between client and server in both directions to demonstrate the serialization process.
The output of the data structure obtained by the Extension is the following:
+------------------------------------+ RPG Character: Sigfried +------------------------------------+ Type: knight Inventory: longSword -- Price: 500, Active: true shortSword -- Price: 200, Active: true Spells: swarm -- HitPoints: 3, Qty: 10 Properties: endurance -- value: 70/100 combatSkill -- value: 55/100 strength -- value: 60/100 +------------------------------------+ RPG Character: Tristan +------------------------------------+ Type: knight Inventory: DragonSword -- Price: 1500, Active: true IceKnife -- Price: 300, Active: true IronShield -- Price: 150, Active: true Spells: Properties: endurance -- value: 50/100 combatSkill -- value: 75/100 strength -- value: 80/100 +------------------------------------+ RPG Character: Hayden +------------------------------------+ Type: sorcerer Inventory: DiamondKnife -- Price: 600, Active: true Spells: vortex -- HitPoints: 30, Qty: 10 flames -- HitPoints: 20, Qty: 5 flood -- HitPoints: 3, Qty: 7 swarm -- HitPoints: 15, Qty: 10 Properties: endurance -- value: 50/100 combatSkill -- value: 55/100 strength -- value: 50/100
There is also a particular server warning that is logged on the server side when the data structure is sent back from the client to the server:
No public setter. Serializer skipping private field: count, from class: XYZ...
This is not really a problem as it was done on purpose: we decided to make the count property not writable in the server side version of the classes in order to avoid client values overwriting the default values.
» Deploying the Extension
Class serialization requires special attention during the deployment phase because the model classes need to be "seen" by SFS2X at the level of the topmost Class Loader.
As you may recall each Extension runs in a separate Class Loader which implies that if we deploy the model classes inside the Extension .jar file we will isolate those classes in the specific Extension's Class Loader. From the main Class Loader the server won't be able to know about these classes therefore generating an error when attempting to deserialize the data (if you any doubts about how this works in SFS2X we recommend to consult the Extension overview).
In order to avoid this problem we need to make sure that the model classes are deployed in the extensions/__lib__/ folder. So for this example we have prepared two files, RpgModel.jar which goes in extensions/__lib__/ and RpgExtension.jar which goes in extensions/rpg/.
» Conclusions
One last word goes to the performance of Class serialization. In general the process is fast and efficient thanks to the SFS2X protocol compression. In our Rpg game the resulting packet size is only 860 bytes (vs. 4323 bytes uncompressed).
Some of the downsides in using Class serialization is that you don't have direct control in the optimization of numeric types and there is additional overhead due to the runtime type reflection. We think that developers can take the best of both worlds by optimizing via SFSObject the critical messages (those the need maximum efficiency), and use Class serialization where appropriate.
In order to complete the tutorial we highly recommend to study the provided source files and see the Class serialization in action.