Skip to main content

Overview

In this guide, you’ll build a simple client-server application where the client sends messages to the server, and the server responds. This demonstrates the core concepts of repod:
  • Defining a Channel subclass for server-side client representation
  • Creating a Server subclass with custom connection handling
  • Building a ConnectionListener subclass for the client
  • Using action-based message dispatch with Network_* callbacks
  • Running synchronous pump loops
1

Create the server

First, create a file named server.py. You need to subclass two classes to make your own server.Each time a client connects, a new Channel instance is created, so you subclass Channel to make your server-side representation of a client:
server.py
from repod import Server, Channel

class ClientChannel(Channel):
    
    def network_received(self, data: dict) -> None:
        print("Received from client:", data)
    
    def Network_myaction(self, data: dict) -> None:
        print("myaction:", data)
        # Echo the message back to the client
        self.send({"action": "hello", "message": "hello client!"})
The network_received() fallback is called if no specific handler exists. The method Network_myaction() is only called if your data has an "action" key with a value of "myaction".
Next, subclass Server:
server.py
class MyServer(Server):
    channel_class = ClientChannel
    
    def on_connect(self, channel, addr):
        print(f"New connection from {addr[0]}:{addr[1]}")
        print(f"Total clients: {len(self.channels)}")
Set channel_class to the Channel subclass you created above. The on_connect() method is called whenever a new client connects.To run the server, call launch():
server.py
if __name__ == "__main__":
    MyServer(host="0.0.0.0", port=5071).launch()
launch() handles the event loop internally and catches Ctrl+C for clean shutdown. Use host="0.0.0.0" to listen on all network interfaces.
2

Create the client

Create a file named client.py. To connect to your server, subclass ConnectionListener:
client.py
import time
from repod import ConnectionListener

class MyClient(ConnectionListener):
    
    def Network_connected(self, data: dict) -> None:
        print("Connected to the server")
        # Send a message to the server
        self.send({"action": "myaction", "blah": 123, "things": [3, 4, 3, 4, 7]})
    
    def Network_error(self, data: dict) -> None:
        print("Error:", data["error"])
    
    def Network_disconnected(self, data: dict) -> None:
        print("Disconnected from the server")
    
    def Network_hello(self, data: dict) -> None:
        print("Received hello:", data["message"])
Network events are received by Network_* callback methods. Replace * with the value of the "action" key you want to catch. The connected, disconnected, and error events are sent automatically by repod.Connect and pump:
client.py
if __name__ == "__main__":
    client = MyClient()
    client.connect("localhost", 5071)
    
    try:
        while True:
            client.pump()
            time.sleep(0.01)
    except KeyboardInterrupt:
        print("\nDisconnecting...")
Call pump() once per game loop and repod handles everything — reading from the socket, deserializing, and dispatching to your Network_* methods.
3

Run the server

Open a terminal and start the server:
python server.py
You should see output indicating the server is running (the exact output depends on your logging configuration). The server will block and wait for connections.
4

Run the client

Open another terminal and start the client:
python client.py
You should see output like:
Connected to the server
Received hello: hello client!
On the server terminal, you should see:
New connection from 127.0.0.1:xxxxx
Total clients: 1
myaction: {'action': 'myaction', 'blah': 123, 'things': [3, 4, 3, 4, 7]}

Complete code

Here are the complete files for reference:
from repod import Server, Channel

class ClientChannel(Channel):
    
    def network_received(self, data: dict) -> None:
        print("Received from client:", data)
    
    def Network_myaction(self, data: dict) -> None:
        print("myaction:", data)
        # Echo the message back to the client
        self.send({"action": "hello", "message": "hello client!"})

class MyServer(Server):
    channel_class = ClientChannel
    
    def on_connect(self, channel, addr):
        print(f"New connection from {addr[0]}:{addr[1]}")
        print(f"Total clients: {len(self.channels)}")

if __name__ == "__main__":
    MyServer(host="0.0.0.0", port=5071).launch()

Understanding the flow

Let’s break down what happens when you run this example:
1

Server starts

The server binds to 0.0.0.0:5071 and begins listening for TCP connections.
2

Client connects

When client.connect() is called, a background thread is started that establishes a TCP connection to the server. Once connected, the server:
  1. Creates a new ClientChannel instance for this connection
  2. Adds it to server.channels list
  3. Calls on_connect() callback
  4. Sends {"action": "connected"} to the client
3

Client receives connected event

The client’s pump loop receives the {"action": "connected"} message and dispatches it to Network_connected(), which sends a message to the server.
4

Server processes message

The server receives {"action": "myaction", ...} and dispatches it to ClientChannel.Network_myaction(), which echoes a response back.
5

Client receives response

The client’s pump loop receives {"action": "hello", ...} and dispatches it to Network_hello(), which prints the message.

Key concepts

Action-based dispatch

repod uses the "action" key in message dictionaries to route messages to the appropriate handler:
# This message calls Network_myaction()
client.send({"action": "myaction", "data": "..."}) 

# This message calls Network_hello()
client.send({"action": "hello", "data": "..."})

# This message calls network_received() fallback
client.send({"somekey": "value"})

Synchronous pump loops

The client runs networking in a background thread, but you interact with it synchronously from your main game loop:
while True:
    # Handle input
    handle_user_input()
    
    # Process network events
    client.pump()
    
    # Update game state
    update_game_state()
    
    # Render
    render_frame()
    
    time.sleep(0.01)
This works with any game framework that has a main loop: pygame, raylib, arcade, pyglet, etc. Just drop pump() into the loop.

Sending data

Both client and server can send arbitrary data structures:
# Send simple messages
client.send({"action": "move", "x": 10, "y": 20})

# Send complex nested data
channel.send({
    "action": "gamestate",
    "players": [
        {"id": 1, "name": "Alice", "pos": [10, 20]},
        {"id": 2, "name": "Bob", "pos": [30, 40]}
    ],
    "items": ["sword", "shield", "potion"],
    "time": 12.5
})
All data is automatically serialized with msgpack, so you can send strings, numbers, lists, dictionaries, and even binary data.

Next steps

Core Concepts

Dive deeper into how repod works under the hood

Building a Server

Learn advanced server patterns and best practices

Building a Client

Integrate repod with game frameworks like pygame

Examples

Explore complete example projects with source code

Build docs developers (and LLMs) love