-
Notifications
You must be signed in to change notification settings - Fork 4
Session State
A server needs somewhere to keep "who is this connection". znet's answer is the
session itself: SetUserPointer hangs an object of your own off a
PeerSession, and user_pointer<T>() gets it back.
struct ClientState {
std::string name = "<anonymous>";
uint32_t messages_received = 0;
};
bool OnClientConnected(IncomingClientConnectedEvent& event) {
PeerSession& session = *event.session();
session.SetCodec(g_codec);
// attach before the handler, so no packet can arrive and find it missing
session.SetUserPointer(std::make_shared<ClientState>());
session.SetHandler(std::make_shared<MyHandler>(event.session()));
return false;
}
void MyHandler::OnPacket(std::shared_ptr<MessagePacket> packet) {
std::shared_ptr<ClientState> state = session_->user_pointer<ClientState>();
if (!state) {
return;
}
state->messages_received++;
}Without it you end up keeping a session id to player map beside the server
and looking up on every packet. The pointer removes that map: any handler
holding the session can reach the state directly.
It is a shared_ptr, and the session holds one reference for as long as it
lives. Attaching state is therefore the whole of the memory management: when the
session is destroyed it drops its reference, and if nobody else kept one the
state goes with it. There is nothing to free in the disconnect handler.
SetUserPointer replaces whatever was there before, which drops the old
reference. Calling it twice is not an error, but the first object is gone unless
you kept it.
If you also keep a server-wide registry so you can walk every client, that registry holds its own reference and does need clearing on disconnect:
bool OnClientDisconnected(IncomingClientDisconnectedEvent& event) {
std::shared_ptr<ClientState> state =
event.session()->user_pointer<ClientState>();
if (!state) {
return false;
}
std::lock_guard<std::mutex> lock(g_clients_mutex);
auto it = std::find(g_clients.begin(), g_clients.end(), state);
if (it != g_clients.end()) {
std::iter_swap(it, g_clients.end() - 1);
g_clients.pop_back();
}
return false;
}The session is still valid inside the disconnect event, so reading the state there is fine. See Events for which events pair with which.
user_pointer<T>() is a static_pointer_cast through a shared_ptr<void>.
znet stores no type tag, so asking for the wrong T is undefined behavior,
not an error you can catch. Nothing throws, nothing returns null, and the
wrong type is simply believed.
The practical rule is one type per session, decided once. If a connection
genuinely needs to change shape, store a common base and discriminate on a field
of your own rather than trying two different Ts and seeing which works, since
neither will fail.
user_pointer<T>() returns null in exactly one case: nothing was ever set.
That is the only failure worth branching on, and it is worth branching on,
because it is what a missed SetUserPointer looks like.
Two different rules, and confusing them is the usual bug.
The state behind the pointer needs no lock. One session never runs two callbacks at once, so its own state has exactly one thread touching it. Incrementing a counter there is safe without an atomic.
A registry across sessions does. Different sessions run on different
workers, so a vector or map of every client is touched concurrently and
needs its own mutex, as above.
SetUserPointer should be called from the session's own thread, which is what
you get inside the connected event or inside an OnPacket. It is a plain
member with no synchronization, so setting it from elsewhere while the session
is live races the thread reading it. Attaching once at connect and only reading
afterwards sidesteps this entirely.
Full rules in Threading Model.
The mechanism is identical on a client, though the need is smaller: a client has one session, so state can simply live in the handler. It is still useful when the state outlives a handler you intend to swap, since replacing the handler does not disturb the pointer.
examples/userptr
is a server and client built around this: every connection gets a ClientState,
the client names itself with its first packet, the server counts its messages
and reports the total when it disconnects.