Serialization
Packet
Packet
is the high level API used to serialize objects.
The Packet
class is defined by an ID (ushort) and data (byte[]).
The packet ID is called
Code
.
Serialization
Simple id (used to trigger an event)
var packet = new Packet(42);
Append content in
packet.Write(myClass2);
packet.Write(myClass3);
packet.Write(myInt);
packet.Write(myString);
Using helpers
var packet = Packet.Create(42, myClass);
var packet = Packet.Create(42, myClass, myClass2, myClass3, myInt, myString);
About
Create
method: a byte[] is only usable as the last argument. If you want add many byte arrays, use theWrite
method.
Deserialization
Read
var myClass = packet.Read<MyClass>();
if (myClass != null) {
// ok
}
var myInt = packet.Read<int>();
var myString = packet.Read<string>();
Inside
var packet = Packet.Create(42, 123456789, new string(){ "ab", "cd" });
42 | 1203456789 | 2 | 2 | a | b | 2 | c | d
In this example the array length equals 2, so the size is stored with a byte. If the array length > 250, an int is append after this byte to store the length.
Serializable fields
- All public fields will be serialized.
[System.Serializable]
is not required to write a class values in a Packet.- If the field has
[System.NonSerialized]
, this field will not be serialized. - If the field is not public but has
[SerializeField]
, this field will be serialized
Example
public class TestSerialized {
public int Value = 12;
public int Value2 = 14;
public string SimpleString = "abcdef";
public List<string> StringList = new List<string>();
public Dictionary<string, Vector2> TestDictionary = new Dictionary<string, Vector2>();
}
public class TestSerialized2 {
public int Value3 = 12;
public int Value4 = 14;
public string SimpleString2 = "prout";
}
using System.Collections;
using System.Collections.Generic;
using EODE.Wonderland.Networking;
using UnityEngine;
public class TestBehavi : MonoBehaviour {
void Start() {
// test create packet
var packet = new Packet(30);
var test = new TestSerialized();
test.Value = 34;
test.StringList.Add("Omelette");
test.StringList.Add("de");
test.StringList.Add("fromage");
test.TestDictionary["vector1"] = new Vector2(8f, 98f);
test.TestDictionary["vector2"] = new Vector2(7f, 478f);
packet.Write(test);
// test read packet
var test2 = packet.Read<TestSerialized>();
Debug.Log("-- List<string>");
foreach (var str in test2.StringList) {
Debug.Log(str);
}
Debug.Log("-- Dict<string, Vector2>");
foreach (var kp in test2.TestDictionary) {
Debug.Log(kp.Key + " = " + kp.Value);
}
// bad packet 1
var badPacket = new Packet(10);
var test3 = new TestSerialized2();
badPacket.Write(test3);
var test4 = badPacket.Read<TestSerialized>();
Debug.log(test4 == null ? "test4 == null" : "test4 != null");
}
}
Advanced
Enum size
Enums values are limited [-32,768 to 32,767] (there are stored into a short value).
From binary
new Packet(networkId, data, seek);
For example, you can create a packet from a file
var packet1 = Packet.Create(0, "text data", 1, 2, 3);
System.IO.File.WriteAllBytes(filepath, packet.Data);
var data = System.IO.File.ReadAllBytes(filepath);
var packet2 = new Packet(0, data, 0);
var str = packet2.Read<string>();
var v1 = packet2.Read<int>();
var v2 = packet2.Read<int>();
var v3 = packet2.Read<int>();
Using a ref
T val = default(T);
packet.Read(ref val);
False positive
When you read a class, it is relatively easy to see if the data length or other is false (this case return null). But it is not perfect. If the class is small or data is a primitive, the read can be a success with bad values.
class Test1 {
public ushort A;
public ushort B;
public int C;
}
class Test2 {
public int A;
public uint B;
}
In this example, Test1 and Test2 match.
var packet = Packet.Create(1, myTest1);
var myTest2 = packet.Read<Test2>();
Debug.Log(myTest2);
Test2 is not null. This is a false positive.
Checksum
CheckSumData
can be used to verify the data integrity.
using EODE.Wonderland.Networking;
using UnityEngine;
public class SerialTest : MonoBehaviour {
void Start() {
var packet = Packet.Create(1, "Hello world", 42);
var check = new CheckSumData(packet.DataWithoutCode);
Debug.Log("Hash: " + check.Hash);
Debug.Log("Test: " + CheckSumData.Check(packet.DataWithoutCode, check.Hash));
Debug.Log("--------------------");
var csd = check.GetBytes();
var check2 = new CheckSumData();
check2.FromBytes(csd, 0);
Debug.Log("(Ok) Data is valid: " + check2.IsValid);
Debug.Log("--------------------");
var badData = csd;
badData[46] = 0; // break a byte
var check3 = new CheckSumData();
check3.FromBytes(badData, 0);
Debug.Log("(Err) Data is valid: " + check3.IsValid);
}
}
[Log] Hash: System.Byte[]
[Log] Test: True
[Log] --------------------
[Log] (Ok) Data is valid: True
[Log] --------------------
[Log] (Err) Data is valid: False
Data helpers
Ignore
Jump a number of bytes. (seek += value)
packet.Ignore<MyPrimitive>(); // primitives only !
packet.Ignore(size);
Back
seek -= value
packet.Back<MyPrimitive>(); // primitives only !
packet.Back(size);
Data Access
packet.Data; // Code+Data
packet.DataWithoutCode; // Data
packet.DataRest; // Data without seek (seek is not updated)
Example: packet data in another packet
var packet2 = Packet.Create(packetCode, ValueA, valueB, packet1.DataWithoutCode);
Do not forget: a byte array is always the last argument. To read a byte array, use DataRest or Read().
Preserve
Dont forget, in release build, non used fields are removed by the code stripping. It may cause issues if:
- using a dev build client with a release build server (and vice versa)
- using a field in a client build but not in the server build (and vice versa)
IBinary
You can override and optimize the read/write of a class with the IBinary interface.
using System.Linq;
using EODE.Wonderland;
class Test1 : IBinary {
public ushort A;
public ushort B;
public int C;
public byte[] GetBytes() {
var result = UnityBitConverter.BaseData();
result = result.Concat(UnityBitConverter.GetBytes(A));
result = result.Concat(UnityBitConverter.GetBytes(B));
result = result.Concat(UnityBitConverter.GetBytes(C));
return result.ToArray();
}
public int FromBytes(byte[] origin, int seek) {
A = UnityBitConverter.ToUInt16(origin, seek);
seek += UnityBitConverter.Size.UInt16;
B = UnityBitConverter.ToUInt16(origin, seek);
seek += UnityBitConverter.Size.UInt16;
C = UnityBitConverter.ToInt32(origin, seek);
seek += UnityBitConverter.Size.Int32;
return seek;
}
}
This methods will be called automatically with write and read in a packet.
Low level
You can use ClassHeader
to serialize or deserialize without the Packet
class.
// from Packet source
public Packet Write<T>(T value) {
if (value == null) Debug.LogError("Cannot write a null value");
_data = _data.Concat(ClassHeader.GetBytes(value)).ToArray();
return this;
}
/// <summary>
/// Read and create a value
/// </summary>
public T Read<T>() {
try {
return ClassHeader.FromBytes<T>(_data, ref _seek);
}
catch {
return default(T);
}
}
/// <summary>
/// Read and create a value; Prefer Read<T> version (better perfs)
/// </summary>
public object Read(System.Type type) {
try {
return ClassHeader.FromBytes(type, _data, ref _seek);
}
catch {
return System.Activator.CreateInstance(type);
}
}
/// <summary>
/// Read and update a value
/// </summary>
public Packet Read<T>(ref T value) {
try {
ClassHeader.FromBytes(ref value, _data, ref _seek);
}
catch {
value = default(T);
}
return this;
}