Skip to content
irrld edited this page Sep 5, 2026 · 8 revisions

znet reports everything through one callback per object. You set it once, and dispatch inside it by type.

void OnEvent(Event& event) {
  EventDispatcher dispatcher{event};
  dispatcher.Dispatch<IncomingClientConnectedEvent>(
      ZNET_BIND_GLOBAL_FN(OnClientConnected));
  dispatcher.Dispatch<IncomingClientDisconnectedEvent>(
      ZNET_BIND_GLOBAL_FN(OnClientDisconnected));
}

Each Dispatch<T> runs its function only if the event is a T. Handlers return bool, which is ORed into event.handled(). Dispatch is by type alone, so returning true does not stop the Dispatch calls after it, and nothing inside znet reads the flag. false is the usual answer; true is only worth returning if your own code reads handled() afterwards.

ZNET_BIND_GLOBAL_FN wraps a free function; ZNET_BIND_FN wraps a member function, capturing this.

Server events

Event When Accessors
ServerStartupEvent The listener is up, before any connection server()
IncomingClientConnectedEvent A session finished its handshake and is ready, just before a worker takes it over session()
IncomingClientDisconnectedEvent A session that had become ready ended session()
ServerShutdownEvent The listener is closing, before sessions are torn down server()

IncomingClientConnectedEvent is where a session gets its codec and handler. It fires once the handshake is done, so encryption is already negotiated and the session can send immediately. The acceptor fires it before handing the session to a worker, so nothing is dispatching on the session while you install them. It is also where per-connection state of your own is attached; see Session State.

A session that dies before becoming ready produces no IncomingClientDisconnectedEvent, because it never produced a connected event either. Events come in pairs, so you do not have to track half-open connections to keep your own bookkeeping balanced.

Shutdown is the one exception: sessions still open when the server stops are closed without a disconnect event, so ServerShutdownEvent is the last thing you hear. Do that bookkeeping there rather than expecting a final round of disconnects.

Client events

Event When Accessors
ClientConnectedToServerEvent The handshake completed session()
ClientDisconnectedFromServerEvent The session ended, after having connected session()
ClientConnectionFailedEvent The session existed but died before it was ready session()

The pairing holds on the client too: a disconnect event only ever follows a connect event, and a deliberate Disconnect() on a ready session still produces one. A session that dies or runs past connection_timeout while still handshaking fires ClientConnectionFailedEvent instead.

Everything that fails before a session exists is Connect()'s return value rather than an event: TCP connects synchronously, so a refused port or an unreachable host comes straight back, and ZDT runs its cookie exchange and MTU probe inside Connect() too, so a server that never answers, is full, or speaks another protocol version fails there. Calling Disconnect() yourself before the session is ready counts as a failure and fires the event too. Destroying the client instead races its stop request against the loop, so the event may or may not arrive; do not depend on it during teardown.

P2P events

Covered in Peer-to-Peer: PeerLocatorReadyEvent, PeerConnectedEvent, PeerLocatorFailedEvent, PeerLocatorCloseEvent.

p2p::Host, the shared socket everything punches from, is the exception: it reports each gather through the GatherCallback passed to Gather and each punch through the PunchCallback passed to Punch, rather than through events. Both run on the host's own thread, the punch one only once the punched session has finished its handshake, so setting a codec and handler inside it is the same pattern as a connected event. p2p::PeerLocator is what turns those callbacks into the events above.

Which thread calls you

The callback does not run on the thread that created the server or client.

  • Server: the acceptor thread for ServerStartupEvent, ServerShutdownEvent and IncomingClientConnectedEvent, since a session is still the acceptor's when it becomes ready; the worker that owns the session for IncomingClientDisconnectedEvent
  • Client: the client's loop thread

So anything your handler touches is shared state, and needs its own synchronization. Full rules in Threading Model.

Defining your own events

Any type deriving from Event and carrying the two class macros can go through the same dispatcher:

class MatchStartedEvent : public Event {
 public:
  ZNET_EVENT_CLASS_TYPE(MatchStartedEvent)
  // categories are bit flags and znet defines three, EventCategoryServer,
  // EventCategoryClient and EventCategoryP2P, so start your own above them
  ZNET_EVENT_CLASS_CATEGORY(1 << 3)
};

Useful for routing your own state changes through the same callback rather than maintaining a second path beside it.

Clone this wiki locally