Build a Mock Simulator
Difficulty: Beginner ยท Time: ~25 minutes
In the Quickstart and the previous tutorial, you used a pre-built mock simulator without writing any of it yourself. Now you'll build one from scratch, understanding exactly what a simulator client does and why.
Goal: Understand how simulators communicate with NSB.
What a Simulator Client Isโ
An NSBSimClient is the bridge between NSB and a network simulator. It has exactly two jobs:
- Fetch payloads that are waiting to be routed through the simulated network
- Post those payloads back once the simulator has finished routing them
Everything else โ how the payload is actually delayed, dropped, or rerouted โ is up to you. NSB doesn't care what happens between fetch() and post(); that's the whole point of being simulator-agnostic. For the full method reference used throughout this tutorial, see Python API โ NSBSimClient.
Step 1 โ Connect NSBSimClientโ
Every simulator client starts the same way: construct it with an identifier and the daemon's address.
import nsb_client as nsb
sim = nsb.NSBSimClient("node0", "127.0.0.1", 65432)
The identifier ("node0" here) is especially important in Per-Node mode, where the simulator is associated with a specific node identifier. In System-Wide mode, a single simulator client can handle messages across nodes. See Simulator Modes if you haven't already.
Step 2 โ Poll with fetch()โ
fetch() retrieves a payload that's waiting to be routed. It supports both blocking and non-blocking usage:
# Blocking โ waits indefinitely until a message arrives
entry = sim.fetch()
# Non-blocking โ returns immediately, None if nothing is waiting
entry = sim.fetch(timeout=0)
# Blocking with a timeout โ waits up to 10 seconds
entry = sim.fetch(timeout=10)
Use blocking (timeout=None, the default) for a simple single-purpose simulator. Use non-blocking (timeout=0) when your simulator needs to poll multiple sources in a loop โ exactly like the two-node example in the previous tutorial, which polled sim0 and sim1 without blocking on either.
Step 3 โ Inspect the MessageEntryโ
fetch() returns a MessageEntry object (or None if nothing was waiting). It carries everything you need to route the payload:
entry = sim.fetch()
if entry:
print("Source: ", entry.src_id)
print("Destination: ", entry.dest_id)
print("Payload: ", entry.payload)
print("Payload size:", entry.payload_size)
Always check if entry: before accessing its fields โ a None result just means nothing was waiting yet. See MessageEntry for the full attribute table.
Step 4 โ Call post() to Deliverโ
Once you've "routed" the payload through your simulated network โ even if that routing is just a time.sleep() for now โ call post() to hand it back to NSB for delivery:
sim.post(entry.src_id, entry.dest_id, entry.payload)
This is what makes the payload available to NSBAppClient(entry.dest_id).receive() on the other side. Until post() is called, the application never sees the message โ fetch() alone doesn't deliver anything.
Step 5 โ Build a Simple Loopโ
A real simulator runs continuously, fetching and posting as messages arrive. Here's the minimal loop:
import nsb_client as nsb
import time
sim = nsb.NSBSimClient("node0", "127.0.0.1", 65432)
while True:
entry = sim.fetch(timeout=0)
if entry:
time.sleep(0.1) # stand-in for real network delay
sim.post(entry.src_id, entry.dest_id, entry.payload)
else:
time.sleep(0.05) # small pause to avoid a busy loop
The else branch matters โ without it, a non-blocking fetch() in a tight loop will burn CPU checking for messages that aren't there yet.
Step 6 โ Add Per-Message Loggingโ
A simulator that doesn't tell you what it's doing is hard to debug. Add print statements so you can see every routing decision as it happens:
import nsb_client as nsb
import time
sim = nsb.NSBSimClient("node0", "127.0.0.1", 65432)
print("[mock-sim] Connected. Waiting for messages...")
while True:
entry = sim.fetch(timeout=0)
if entry:
print(f"[mock-sim] Fetched: {entry.src_id} -> {entry.dest_id} "
f"({entry.payload_size} bytes)")
time.sleep(0.1)
sim.post(entry.src_id, entry.dest_id, entry.payload)
print(f"[mock-sim] Posted: {entry.src_id} -> {entry.dest_id}")
else:
time.sleep(0.05)
Full Working Codeโ
# mock_simulator.py
import nsb_client as nsb
import time
sim = nsb.NSBSimClient("node0", "127.0.0.1", 65432)
print("[mock-sim] Connected. Waiting for messages...")
while True:
entry = sim.fetch(timeout=0)
if entry:
print(f"[mock-sim] Fetched: {entry.src_id} -> {entry.dest_id} "
f"({entry.payload_size} bytes)")
time.sleep(0.1)
sim.post(entry.src_id, entry.dest_id, entry.payload)
print(f"[mock-sim] Posted: {entry.src_id} -> {entry.dest_id}")
else:
time.sleep(0.05)
Run it alongside the daemon and an application client exactly as you did in the Quickstart โ but now every line of the simulator is something you wrote and understand.
What You Just Learnedโ
- What a simulator client's two responsibilities are (fetch, post)
- Blocking vs. non-blocking
fetch()and when to use each - How to read the source, destination, payload, and payload size from a
MessageEntry - Why
post()โ notfetch()โ is what actually delivers a message - How to structure a continuous polling loop without burning CPU
- Why logging every fetch/post pair makes a simulator debuggable
Go Deeperโ
- Python API โ NSBSimClient โ full method reference including
listen()for async simulators - Payload Lifecycle โ how fetch/post fits into the complete message round-trip