- Home /
Sending structs over network (NetworkTransport API)
Hello,
I am developing a multiplayer game on top of the Low Level API (NetworkTransport API). I'm at the point where I want to send different information, structured in different structs. (e.g. SpawnInformation, PositionUpdate and so on)
So my idea is to convert the struct to a byte array, send the array and convert it back. But here is the problem. How does the receiver know what type of struct that is? One thing I noticed is that if I add a type byte at the start i can use this to cast it to different objects.
But I am interested in your ideas, since this is probably a pretty common task for network developing.
Thanks!
Answer by Bunny83 · Mar 03, 2016 at 07:08 PM
Of course you need to serialize the type of the struct in some way. Generally structs are a bad choice when the data should be serialized / deserialized because of their valuetype nature.
Also you usually don't want to just send the plain information but usually "messages". Well, that's what the HighLevelAPI actually does for you.
So if you want to roll your own, you have to define your own message format. A message usually contains a message type and often a variable amount of payload data. There are examples in the docs. Keep in mind that a packet is limited to the configured "MaxPacketSize". So larger data streams need to be split into several packets.
When dealing with binary streaming data the best solution is to use a BinaryReader with a MemoryStream. That way you can simply read your message component by component. I haven't use Unity's NetworkReader yet, but even they claim it's part of the high level API, it looks like it's just a standalone stream helper just like the BinaryReader but has support for most Unity types.
Why are structs a bad choice? Could you elaborate this a bit please?
Right now I'm converting my struct to bytes like this:
public static byte[] GetBytes( object myStruct ) {
int size = $$anonymous$$arshal.SizeOf(myStruct);
byte[] buffer = new byte[size];
IntPtr ptr = $$anonymous$$arshal.AllocHGlobal(size);
$$anonymous$$arshal.StructureToPtr(myStruct, ptr, true);
$$anonymous$$arshal.Copy(ptr, buffer, 0, size);
$$anonymous$$arshal.FreeHGlobal(ptr);
return buffer;
}
Now when I try to get my struct back I do this:
public static object FromBytes( byte[] buffer ) {
object myStruct;
switch(buffer[0]) {
case (int)PAC$$anonymous$$ET_TYP$$anonymous$$SPAWN_OBJECT:
myStruct = new SpawnObject();
break;
default:
myStruct = null;
break;
}
int size = $$anonymous$$arshal.SizeOf(myStruct);
IntPtr ptr = $$anonymous$$arshal.AllocHGlobal(size);
$$anonymous$$arshal.Copy(buffer, 0, ptr, size);
myStruct = (object) $$anonymous$$arshal.PtrToStructure(ptr, myStruct.GetType());
$$anonymous$$arshal.FreeHGlobal(ptr);
return myStruct;
}
This seems to be pretty ugly. One of my other concerns is the speed. What would you do?
Because value types are difficult to handle when you pass them between methods. What you've done here is also extremely dangerous as you use pointers here. Also you treat your struct as "object". So you always box the struct on the heap and on every access you need to unbox it. That's why a class would be better. $$anonymous$$arshalling a struct into a byte array is an "evil hack" and might fail depending on the machines architecture and memory alignment. You would need at least a StructLayoutAttribute on it.
It's usually better to either use a BinaryFormatter and simply serialize a pure data class, or serialize the needed things manually using the above mentioned BinaryReader / Writer or NetworkReader / Writer.
How you prepare your data for network transfer won't be your bottleneck. That will be 10x or 100x faster than the actual transfer.
I'll add a small example to my answer.
First of all thanks for your help.
What I have now is this:
public static byte[] SerializeClass( object obj ) {
$$anonymous$$emoryStream stream = new $$anonymous$$emoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(stream, obj);
return stream.GetBuffer();
}
public static object DeserializeClass( byte[] buffer ) {
$$anonymous$$emoryStream stream = new $$anonymous$$emoryStream(buffer);
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(stream);
}
Is this what you had in $$anonymous$$d? If yes, I still have the problem how to know what kind of data arrived at the receiver. I could add a byte identifying the packet. Or is there a better way?
EDIT:
Okay I just noticed I can check of what type my message is like this:
SpawnInformation so = new SpawnInformation(10, 10, 1);
byte[] buffer = SNProtocol.SerializeClass(so);
object obj = SNProtocol.DeserializeClass(buffer);
if(obj is SpawnInformation) {
Debug.Log((SpawnInformation) obj);
}
PositionUpdate pu = new PositionUpdate(20, 20, "abcdefg");
buffer = SNProtocol.SerializeClass(pu);
obj = SNProtocol.DeserializeClass(buffer);
if(obj is PositionUpdate) {
Debug.Log((PositionUpdate) obj);
}
I checked the size of the buffer, it is in both cases 256. Is this acceptable for just a position update?
Your answer
