The Service Loop

Everything ENet does happens inside EnetHost.service(): it sends the packets queued since the last call, receives what has arrived, runs the timeouts and pings, and hands back one event at a time. A host that is not serviced does not send, does not receive, and eventually times out its peers. The shape of the loop matters.

Draining the queue

Call service in a loop until it returns None, every frame on a client and every tick on a server. The optional timeout_ms blocks the first call for up to that long waiting for something to happen, which keeps a dedicated server from spinning; the drain that follows uses 0.

def pump(host, first_timeout_ms=0):
    event = host.service(first_timeout_ms)
    while event is not None:
        dispatch(event)
        event = host.service(0)

EnetHost.flush() sends whatever is queued without receiving, for the rare case where a message must leave before the next service.

Guarding the handlers

One bad packet must not stop the loop for everybody. A handler that reads a truncated datagram raises; a handler with a bug raises. Catch around the dispatch, log, and drop the event:

def dispatch(event):
    try:
        if event.type == EnetEvent.T_connect:
            on_connect(event.peer)
        elif event.type == EnetEvent.T_disconnect:
            on_disconnect(event.peer, event.event_data)
        elif event.type == EnetEvent.T_receive:
            on_message(event.peer, event.channel, event.get_datagram())
    except Exception:
        log_exception("handling event from %s" % event.peer.address)

Events cannot be put back

An event taken from service is gone. Code that services a host while it is not ready to act on messages – a loading screen keeping the connection alive, say – must store the payloads it receives for later, and must handle the connection and disconnection events when they come: a swallowed T_connect is a join that never completes, silently. Deferring a datagram is cheap; skipping a lifecycle event is a bug.

Disconnecting

EnetPeer.disconnect()

Tells the other side and waits for the acknowledgement; the T_disconnect event arrives on both hosts once it is done. The optional data argument reaches the other side as EnetEvent.event_data, which is a convenient place for a reason code.

EnetPeer.disconnect_later()

The same, after every queued packet has been delivered – the right call after a final message.

EnetPeer.disconnect_now()

Sends the notification once and forgets the peer without waiting.

EnetPeer.reset()

Forgets the peer without telling it.

A disconnecting peer holds its slot in the host until the exchange completes, which is why a client wants a few slots even for a single connection.

Timeouts and pings

ENet pings idle connections and drops a peer that stops answering. EnetPeer.set_timeout() sets the retry limit and the minimum and maximum wait in milliseconds; EnetPeer.set_ping_interval() the ping interval; EnetPeer.throttle_configure() how the throttle reacts to packet loss. The defaults suit a game on the open internet.

Threads

A host must be serviced from one thread; nothing in ENet is thread-safe. Service it from the task that owns it, and hand the results to the rest of the program from there.

Testing

A server and any number of clients can live in one process, as the module’s own tests do: create the hosts, connect, and service all of them in a loop until the expected events have arrived. For a game, a headless test can run the real server code against bare EnetHost clients that play the role of players without a window, and a single off-screen ShowBase for the real client – one ShowBase per process is the engine’s rule, not the module’s.