Packets
A packet is a class that carries data between a client and a server. Implement the empty marker interface IDataPacket and give the class public properties:
using BSGameLib.Networking;
public class ChatPacket : IDataPacket
{
public string Sender { get; set; }
public string Message { get; set; }
}
The client and the server must both have the same class, with the same name and the same properties in the same order.
What is sent
Every public instance property is sent, in the order the class declares them. Static properties are not sent.
A property needs a setter that the receiver can call. { get; private set; } works. { get; }
does not: the packet is sent, but it fails to arrive. Fields, methods and non-public properties
are not sent.
These types are sent:
| Type | How it is sent |
|---|---|
bool, byte, sbyte, char, short, ushort, int, uint, long, ulong, float, double, decimal |
Their binary values. |
string |
UTF-8, with its length. null arrives as null. |
DateTime |
Its value and its Kind, so a UTC time arrives as UTC. A Local time arrives as the same moment, in the receiver's time zone. |
| Enums | As a 32-bit integer, so the enum must keep its default underlying type, int. |
byte[] |
Its length and its raw bytes. Use it for binary data: it is the fast path. |
Arrays and List<T> |
Each element in turn. |
IEnumerable<T>, ICollection<T>, IReadOnlyList<T>, IList<T> and other generic collections |
Each element in turn. The property arrives as a List<T>, or as a HashSet<T> if it is typed ISet<T>, and a concrete type such as HashSet<T> arrives as itself. |
Dictionary<TKey, TValue>, IDictionary<TKey, TValue> and IReadOnlyDictionary<TKey, TValue> |
Each key and value in turn. The property arrives as a Dictionary<TKey, TValue>. |
Guid |
Its 16 bytes. |
| Classes | Their public properties, under the same rules. A nested class needs no constructor, and its property initialisers do not run when it arrives. |
| Structs | Their public properties, as a class's are, so every one needs a setter the receiver can call, and there must be at least one. |
Nullable value types, such as int? or a nullable struct |
The value, or null. |
These are not sent:
- A struct with no public properties, or with one that has no setter, such as
TimeSpan,DateTimeOffsetorIntPtr. Sending one throwsNotSupportedException. Send the parts as primitives, or use a class. - A non-generic collection, such as
ArrayListorHashtable. It sends, but the receiver has no element types to rebuild it from, so the packet fails to arrive. Use a generic collection.
Adding and changing properties
The receiver reads the values in order and matches them by position, not by name. A change to a packet has to be compatible with the build on the other side:
- Add new properties at the end of the class. An older packet ends before them, so they arrive as
their type's default:
null,0orfalse. Their initialisers do not run, so handle the default in your code. - A newer packet with extra properties at the end is read by an older receiver, which ignores the extra values.
- Do not reorder properties, insert one in the middle, or change a property's type. The receiver then reads the wrong values, or fails.
Packet identity
A packet is identified by its class name, without the namespace, hashed with SHA-1. Two things follow from that:
- Renaming a packet class changes its identity. Moving it to another namespace does not.
- Two packet classes with the same name collide, even in different namespaces, and that is refused
rather than allowed to pass silently. The server host refuses it when it starts. On a client,
RegisterPacket refuses the second class, and
so does making a DataPacketManager over a provider that holds both. Each
throws
InvalidOperationException, naming both full class names and the shared short name. See Two packet classes have the same name.
Registering packets and handlers
A handler implements IDataPacketHandler<TPacket> for one packet type. Its
Handle method runs once for each packet of that type that arrives:
using System.Collections.Concurrent;
using BSGameLib.Networking;
public class ChatHandler(ConcurrentQueue<ChatPacket> inbox) : IDataPacketHandler<ChatPacket>
{
public void Handle(INetConnection connection, ChatPacket packet) => inbox.Enqueue(packet);
}
Register the packets and the handlers with the two providers, then give both to a DataPacketManager:
using System.Collections.Concurrent;
using BSGameLib.Networking;
using Microsoft.Extensions.Logging;
ILogger logger = Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
var inbox = new ConcurrentQueue<ChatPacket>();
var packets = new SimpleDataPacketProvider();
packets.RegisterPacket<ChatPacket>();
var handlers = new SimpleDataPacketHandlerProvider();
handlers.RegisterHandler(new ChatHandler(inbox));
var manager = new DataPacketManager(logger, packets, handlers);
Pass manager to a ClientAdapter or a
ServerAdapter. The rules for registration are these:
- Sending does not need registration. Receiving does. A packet that arrives unregistered is refused
with an
ArgumentOutOfRangeException, which the adapter logs. Each side registers every packet type it receives. - A registered packet with no handler is accepted and dropped without an error.
SimpleDataPacketHandlerProviderkeeps one handler per packet type. A secondRegisterHandlerfor the same packet type is ignored, without an error.- A handler that throws does not close the connection. The adapter logs the failure, naming the packet, and carries on.
Handlers run on the threads that the adapter receives data on, not on your game's thread. The client page covers what that means for your code.
Packets and the server host
BSGameLib.Server finds the packets and handlers in your assemblies for you. See
How it finds your code.
The attribute DoNotRegisterAttribute keeps a class, or an assembly, out of that search. On a packet class it only matters for a packet the server never receives, because a packet with a handler is registered whatever the attribute says: its handler names it. Only the server host reads the attribute. A client registers what you tell it to.