Deep dive into server architecture, channel lifecycle, and the background thread model
repod uses a classic one-channel-per-client architecture. Each time a client connects, the server creates a new Channel instance to represent that connection.
class GameChannel(Channel): def Network_chat(self, data: dict) -> None: print(f"Message from {self.addr[0]}:{self.addr[1]}") # Broadcast to all OTHER clients for channel in self.server.channels: if channel != self: channel.send(data)
The Client class runs asyncio in a background thread:
low_level.py
from repod import Clientclient = Client(host="localhost", port=5071)client.start_background()# Send messages from main threadclient.send({"action": "hello"})# Poll for received messageswhile not client._receive_queue.empty(): message = client._receive_queue.get() print(message)
From client.py:81-86:
client.py
def start_background(self) -> None: """Start the network event loop in a daemon thread.""" if self._thread is None or not self._thread.is_alive(): self._thread = threading.Thread(target=self._run_loop, daemon=True) self._thread.start()
The pump() method is the heart of the client-side API. Call it once per frame in your game loop.From client.py:242-266:
client.py
def pump(self) -> None: """Process all pending network messages. Call this once per frame in your game loop. Each queued message is dispatched to the matching ``Network_{action}`` method, or to :meth:`network_received` as a fallback. """ if self._connection is None: return while not self._connection._receive_queue.empty(): try: data = self._connection._receive_queue.get_nowait() except queue.Empty: break action = data.get("action", "") method_name = f"Network_{action}" handler = getattr(self, method_name, None) if handler is not None: log.debug("message_dispatched", action=action) handler(data) else: log.debug("message_unhandled", action=action) self.network_received(data)
pump() must be called from your main game loop thread. It is not thread-safe.
Use start_background() when you need the main thread free for other work (e.g., hosting a game while playing):
host_and_play.py
from repod import Server, Channel, ConnectionListenerimport timeclass GameChannel(Channel): def Network_move(self, data: dict) -> None: self.server.send_to_all(data)class GameServer(Server): channel_class = GameChannelclass GameClient(ConnectionListener): def Network_move(self, data: dict) -> None: print(f"Player moved: {data}")# Start server in backgroundserver = GameServer(host="127.0.0.1", port=5071)server_thread = server.start_background()# Connect as a client in main threadclient = GameClient()client.connect("127.0.0.1", 5071)# Game loopwhile True: client.pump() # Your game logic here time.sleep(0.01)
From server.py:168-179:
server.py
def _run_in_thread(self) -> None: """Create a new event loop and run the server inside it.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(self.start()) try: loop.run_until_complete(self.run()) except Exception as exc: log.error("background_server_error", error=str(exc)) finally: loop.run_until_complete(self.stop()) loop.close()
Both Client and Channel disable Nagle’s algorithm for low-latency real-time communication.From channel.py:69-73:
channel.py
# Disable Nagle's algorithm for low-latency real-time communication.sock: socket.socket | None = writer.get_extra_info("socket")if sock is not None: with contextlib.suppress(OSError): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
Nagle’s algorithm batches small packets to reduce overhead, but this adds latency. For games, we want messages sent immediately, so TCP_NODELAY is enabled.