Skip to main content

โ† Back to Tutorials

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:

  1. Fetch payloads that are waiting to be routed through the simulated network
  2. 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() โ€” not fetch() โ€” 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โ€‹

Next: Build a NetworkX Graph Simulator โ†’