Reloaded.Messaging.Serializer.MessagePack:
Reloaded.Messaging.Serializer.ReloadedMemory:
Reloaded.Messaging.Compressor.ZStandard:
Reloaded.Networking is Reloaded II's extensible "event-like" solution for passing messages across a local or remote network that extends on the base functionality of LiteNetLib by Ruslan Pyrch (RevenantX) .
It has been slightly extended in the hope of becoming more general purpose, perhaps to be reused in other projects.
Reloaded.Networking
is a simple barebones library to solve a deceptively annoying problem: Writing code that distinguishes the type of message received over a network and performs a specific action.
- Minimal networking overhead in most use cases (1 byte)*.
- Choice of serializer/compressor on a per type (struct/class) basis.
- Simple to use.
*Assuming user has less than 256 unique types of network messages.
Alternative unmanaged types (e.g. short, int) can be specified increasing overhead to sizeof(type)
and respectively increasing max unique types.
A. User specifies or chooses an individual unmanaged type TMessageType
(recommend enum), where each unique value corresponds to different message structure.
B. User implements interface IMessage<TMessageType>
in types they want to send over the network.
C. User creates SimpleHost
instance(s) for Server/Client with type from A
as generic type.
D. User registers methods to handle different values for TMessageType
.
And then message sending/receiving can proceed.
Note: A complete working example can be found in the basic test collection, Reloaded.Messaging.Tests
.
A unique message identifier TMessageType
can be any unmanaged type.
The recommended type is enum.
// We have less than 256 values, so use byte.
// Default for enum is int, but we don't need extra 3 bytes of overhead in every message.
public enum MessageType : byte
{
String,
Vector3
}
The interface specifies the compressor and serializer used to pack the specified structure. It acts as a contract and therefore should match between the server and client.
No information about the serializer or compressor is sent with any of the packets as that would incur unnecessary additional overhead.
public struct Vector3 : IMessage<MessageType>
{
// IMessage
public MessageType GetMessageType() => MessageType.Vector3;
public ISerializer GetSerializer() => new MsgPackSerializer(true);
public ICompressor GetCompressor() => null;
// Members
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
A serializer mist be specified. Compressor is optional.
The following example creates a new server and client on the local machine and connects them to each other.
// DefaultPassword = "RandomString"
// MessageType = enum (byte) (see above)
SimpleServer = new SimpleHost<MessageType>(true, DefaultPassword);
SimpleClient = new SimpleHost<MessageType>(false, DefaultPassword);
SimpleServer.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0);
SimpleClient.NetManager.Start(IPAddress.Loopback, IPAddress.IPv6Loopback, 0);
SimpleClient.NetManager.Connect(new IPEndPoint(IPAddress.Loopback, SimpleServer.NetManager.LocalPort), DefaultPassword);
// Register a function "Handler" to deal with incoming messages of type Vector3. (MessageType is obtained from IMessage Interface)
SimpleClient.MessageHandler.AddOrOverrideHandler<Vector3>(Handler);
static void Handler(ref NetMessage<Vector3> netMessage)
{
var vector3 = netMessage.Message;
// Do something with Vector3
}
To send a message, create an instance of Message<TMessageType, TStruct>
, where TStruct
is a struct from B
that inherits IMessage<TMessageType>
.
var vectorMessage = new Message<MessageType, Vector3>(message); // Wraps the message for sending.
byte[] data = vectorMessage.Serialize(); // Serializes and compresses using Serializer/Compressor defined in IMessage implementation.
SimpleServer.NetManager.FirstPeer.Send(data, DeliveryMethod.ReliableOrdered); // Regular LiteNetLib usage.
Reloaded.Messaging allows you to specify your own serializers and compressors responsible for the serialization of the message to be transmitted.
Adding support for these is very trivial.
To implement a serializer, simply make a class that implements the ISerializer
interface.
public interface ISerializer
{
TStruct Deserialize<TStruct>(byte[] serialized);
byte[] Serialize<TStruct>(ref TStruct item);
}
A simple example implementation using MessagePack-CSharp could look like this:
public class MsgPackSerializer : ISerializer
{
public TStruct Deserialize<TStruct>(byte[] serialized) => MessagePackSerializer.Deserialize<TStruct>(serialized);
public byte[] Serialize<TStruct>(ref TStruct item) => MessagePackSerializer.Serialize(item);
}
There is no default serializer, however one must be specified.
All serializers can be found in the Reloaded.Messaging.Serializer
namespace, with serializers available as their own NuGet packages.
Implementing Compressors is virtually identical to implementing a Serializer.
public interface ICompressor
{
byte[] Compress(byte[] data);
byte[] Decompress(byte[] data);
}
A simple example using ZstdNet could look like this:
public class ZStandardCompressor : ICompressor, IDisposable
{
public readonly ZstdNet.Compressor Compressor;
public readonly Decompressor Decompressor;
public ZStandardCompressor(CompressionOptions compressionOptions = null, DecompressionOptions decompressionOptions = null)
{
Compressor = compressionOptions != null ? new ZstdNet.Compressor(compressionOptions) : new ZstdNet.Compressor();
Decompressor = decompressionOptions != null ? new Decompressor(decompressionOptions) : new Decompressor();
}
// Disposal
~ZStandardCompressor()
{
Dispose();
}
public void Dispose()
{
Compressor?.Dispose();
Decompressor?.Dispose();
GC.SuppressFinalize(this);
}
// ICompressor
public byte[] Compress(byte[] data) => Compressor.Wrap(data);
public byte[] Decompress(byte[] data) => Decompressor.Unwrap(data);
}
All compressors can be found in the Reloaded.Messaging.Compressor
namespace, with compressors available as separate NuGet packages.
If null
is specified for the compressor, no compression will be performed.
Note: This feature is mainly intended for benchmarking and testing. Changes are client-side (program-side) only and not broadcasted to clients etc.
It is possible to override the compressor and/or serializer used to handle a specific type at runtime.
Usage Examples:
// Override Vector3
Overrides.SerializerOverride[typeof(Vector3)] = new MsgPackSerializer(false);
Overrides.CompressorOverride[typeof(Vector3)] = null;
// Remove overrides for Vector3
Overrides.SerializerOverride.Remove(typeof(Vector3));
Overrides.CompressorOverride.Remove(typeof(Vector3));
SimpleHost
uses the following default settings for the LiteNetLib library.
- Subscribes to
ConnectionRequestEvent
, allowing clients to connect only with password set in constructor. (Which can be changed after instantiation)
- Sets
UnsyncedEvents
to true. Messages are received automatically on background thread. - Sets
AutoRecycle
to true. Automatically recycling NetPacketReader. - Subscribes to
NetworkReceiveEvent
, to automatically handle incoming packets that have been assigned to theMessageHandler
.