Skip to content

Session State

irrld edited this page Aug 4, 2026 · 2 revisions

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.

Lifetime

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.

The cast is unchecked

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.

Threading

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.

Clients too

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.

A worked example

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.

Clone this wiki locally