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_disconnectevent arrives on both hosts once it is done. The optionaldataargument reaches the other side asEnetEvent.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.