Skip to main content

Architecture Overview

This page describes the internal architecture of NSB β€” its components, communication model, operation modes, and the differences between its two transport backends.

High-Level Overview​

NSB consists of three logical layers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Application Layer β”‚
β”‚ NSBAppClient (Python or C++) β”‚
β”‚ send() ──────────────────────────── receive() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ TCP Socket or RabbitMQ
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ NSB Daemon β”‚
β”‚ Receives, routes, and optionally stores payloads in Redis β”‚
β”‚ Manages client registration and configuration distribution β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ TCP Socket or RabbitMQ
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Simulator Layer β”‚
β”‚ NSBSimClient (Python or C++) β”‚
β”‚ fetch() ─────────────────────────── post() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Components​

NSB Daemon​

The NSB Daemon is the central server process. It:

  • Listens for TCP connections (socket backend) or manages a RabbitMQ configuration queue (RabbitMQ backend)
  • Registers clients that connect and identify themselves via the INIT protocol
  • Distributes system configuration (system mode, simulator mode, database settings) to all connecting clients
  • Routes payloads between application clients and simulator clients (socket backend only)
  • Optionally stores payloads in Redis when use_db: true is set in the configuration

The daemon is launched from its compiled binary with a configuration YAML file:

./nsb_daemon config.yaml

NSB Application Client (NSBAppClient)​

The application-side client. It presents a simplified network interface to application code. Key methods:

  • send(dest_id, payload) β€” send a payload toward a destination
  • receive() / receive(dest_id, timeout) β€” receive an incoming payload
  • listen() β€” async coroutine (Python only) for receiving payloads

Full method reference: Python API Β· C++ API

NSB Simulator Client (NSBSimClient)​

The simulator-side client. It integrates into a network simulator. Key methods:

  • fetch() / fetch(src_id, timeout) β€” fetch a payload that was sent and is waiting to traverse the simulated network
  • post(src_id, dest_id, payload) β€” notify NSB that a payload has arrived at its destination in the simulator
  • listen() β€” async coroutine (Python only) for fetching payloads

Full method reference: Python API Β· C++ API

Redis (Optional)​

When use_db: true is set, payloads are stored in a Redis database rather than transmitted directly. The key for each stored payload is routed through NSB, and the receiving side retrieves the full payload from Redis using that key. This is useful for large payloads that exceed network buffer sizes. See Redis Storage for the caching sequence in detail.

Protocol: Protobuf Messages (nsbm)​

All communication between NSB components uses a single Protobuf message type: nsbm (defined in proto/nsb.proto).

Message Structure​

message nsbm {
message Manifest {
enum Operation {
PING = 0;
INIT = 1;
SEND = 2;
FETCH = 3;
POST = 4;
RECEIVE = 5;
FORWARD = 6;
EXIT = 7;
}
Operation op = 1;

enum Originator {
DAEMON = 0;
APP_CLIENT = 1;
SIM_CLIENT = 2;
}
Originator og = 2;

enum OpCode {
SUCCESS = 0;
FAILURE = 1;
CLIENT_REQUEST = 2;
DAEMON_RESPONSE = 3;
IMPLICIT_TARGET = 4;
EXPLICIT_TARGET = 5;
MESSAGE = 6;
NO_MESSAGE = 7;
}
OpCode code = 3;
}

message Metadata {
string src_id = 1;
string dest_id = 2;
int32 payload_size = 3;
}

message ConfigParams {
enum SystemMode { PULL = 0; PUSH = 1; }
enum SimulatorMode { SYSTEM_WIDE = 0; PER_NODE = 1; }
SystemMode sys_mode = 1;
SimulatorMode sim_mode = 2;
bool use_db = 3;
string db_address = 4;
int32 db_port = 5;
int32 db_num = 6;
}

message IntroDetails {
string identifier = 1;
string address = 2;
int32 ch_CTRL = 3;
int32 ch_SEND = 4;
int32 ch_RECV = 5;
}

oneof message {
bytes payload = 3;
string msg_key = 4;
IntroDetails intro = 5;
ConfigParams config = 6;
}
}

The full schema with every field explained lives in the Protobuf Reference.

Operations​

OperationDirectionDescription
PINGAny β†’ AnyHeartbeat / connectivity check
INITClient β†’ DaemonClient registration and configuration request
SENDAppClient β†’ DaemonSend a payload for a destination
FETCHSimClient β†’ DaemonFetch a payload to transmit through the simulator
POSTSimClient β†’ DaemonMark a payload as delivered (post-simulation)
RECEIVEAppClient β†’ DaemonReceive a payload that arrived from the simulator
FORWARDDaemon β†’ ClientDaemon forwards a message to a client (PUSH mode)
EXITClient β†’ DaemonGraceful client disconnection

For the full operations table including channel assignment and status codes, see Message Flow.

System Modes​

PULL Mode (mode: 0) β€” Default​

In PULL mode, clients poll the daemon to check for messages. No message is sent unless explicitly requested.

  • NSBAppClient.receive() sends a RECEIVE request to the daemon, which responds with either a payload (MESSAGE) or nothing (NO_MESSAGE)
  • NSBSimClient.fetch() sends a FETCH request to the daemon, which responds similarly

Best for: Most configurations. Easier to manage, no persistent connection requirements.

PUSH Mode (mode: 1)​

In PUSH mode, the daemon automatically forwards payloads to clients as soon as they are available.

  • When NSBAppClient.send() is called, the daemon immediately forwards the payload to the simulator client
  • When NSBSimClient.post() is called, the daemon immediately forwards the payload to the destination application client
  • Clients must maintain persistent connections

Best for: Latency-sensitive applications. Requires stable network and connection management.

Full concept page: System Modes. To configure this field, see Configuration β†’ System Modes.

Simulator Modes​

System-Wide Mode (simulator_mode: 0)​

A single simulator client handles all message routing. When a payload is fetched, it can come from any source node.

AppClient(node0) ──send──► Daemon ──fetch──► SimClient (global)
AppClient(node1) ──send──► Daemon ──fetch──► SimClient (global)

Best for: Top-down simulators like ns-3, where a single simulation script manages the entire network.

Constraint: Only one NSBSimClient may connect to the daemon at a time in this mode.

Per-Node Mode (simulator_mode: 1) β€” Default​

Each simulated node has its own simulator client. The client identifier for NSBSimClient must match the corresponding NSBAppClient identifier.

AppClient("node0") ──send──► Daemon ──fetch──► SimClient("node0")
AppClient("node1") ──send──► Daemon ──fetch──► SimClient("node1")

When NSBSimClient("node0") calls fetch(), it only retrieves messages sent from AppClient("node0"). When it calls post(), the payload is made available to AppClient at the destination.

Best for: Bottom-up simulators like OMNeT++, where each simulated host module directly handles its own traffic.

Full concept page: Simulator Modes. To configure this field, see Configuration β†’ Simulator Modes.

Transport Backends​

Socket Backend (Default)​

NSBAppClient ──TCP──► NSBDaemon ──TCP──► NSBSimClient
β”‚
Redis
(optional)
  • Uses raw TCP sockets for all communication
  • The daemon actively routes all messages
  • Three logical channels per client: CTRL, SEND, RECV
  • Best for single-machine setups

RabbitMQ Backend​

NSBAppClient ──AMQP──► RabbitMQ Broker ◄──AMQP── NSBSimClient
β”‚
NSBDaemon
(config only)
  • Uses AMQP protocol via RabbitMQ
  • The broker handles all message routing β€” the daemon only handles client initialization and configuration
  • Three channels still exist but are implemented as RabbitMQ queues: nsb.ctrl.{client_id}, nsb.send.{client_id}, nsb.recv.{client_id}
  • Additional queues: nsb.config.request and nsb.config.response.{client_id}

Full queue naming convention and RabbitMQ message flow: Message Flow. Complete usage guide: RabbitMQ Backend.

Backend Comparison​

FeatureSocket BackendRabbitMQ Backend
TransportRaw TCPAMQP (RabbitMQ)
Message RoutingDaemon-mediatedBroker-native
Daemon RoleFull routerConfig service only
ScalabilitySingle-machineHorizontal scale
Async Implementationselect()asyncio + polling
Channel ArchitectureTCP channelsRabbitMQ queues
Protobuf formatSame (nsbm)Same (nsbm)
PULL/PUSH supportYesYes
Simulator modesYesYes
Redis supportYesYes

Unified Client​

NSB provides a Unified Client (nsb_unified.py in Python, NSBUnified in C++) that allows switching between socket and RabbitMQ backends with a single parameter β€” no code changes needed.

import nsb_unified as nsb

# RabbitMQ backend
app = nsb.NSBAppClient("app_node_0", "localhost", 5672, backend="rabbitmq")

# Socket backend β€” same code
app = nsb.NSBAppClient("app_node_0", "localhost", 5555, backend="socket")

Backend can also be selected via environment variables:

export NSB_BACKEND=rabbitmq
export NSB_PORT=5672

Cross-Language Interoperability​

Python and C++ clients are fully wire-compatible. They use the same Protobuf message format (nsb.proto) and the same queue/channel naming conventions.

This means:

  • A Python NSBAppClient can send to a C++ NSBAppClientRMQ and vice versa
  • Either the Python daemon or the C++ daemon can serve any language's clients
  • Client IDs must be unique across all clients sharing the same broker/daemon

C++ Internal Details​

Key Constants​

#define SERVER_CONNECTION_TIMEOUT 10 // seconds
#define DAEMON_RESPONSE_TIMEOUT 30 // seconds
#define RECEIVE_BUFFER_SIZE 4096 // bytes
#define SEND_BUFFER_SIZE 4096 // bytes

Class Hierarchy (Socket Backend)​

NSBClient (base: init, ping, exit, config)
β”œβ”€β”€ NSBAppClient (send, receive, listenReceive)
└── NSBSimClient (fetch, post, listenFetch)

Class Hierarchy (RabbitMQ Backend)​

NSBClientRMQ (base: INIT, PING, EXIT, config)
β”œβ”€β”€ NSBAppClientRMQ (send, receive, listen)
└── NSBSimClientRMQ (fetch, post, listen)

MessageEntry Struct (C++)​

Used as the return type for receive() and fetch() operations:

struct MessageEntry {
std::string source; // identifier of original source
std::string destination; // identifier of final destination
std::string payload_obj; // the actual payload bytes
int payload_size; // original payload size

bool exists(); // returns true if this entry is populated
};

Full reference: C++ MessageEntry.

Bytestring Representation​

The C++ implementation uses std::string to represent payload bytestrings β€” constructed from the reception buffer with specified lengths. This maintains data integrity and simplifies memory management. Note: some simulators (e.g. OMNeT++ without INET) may not properly handle null characters (\00) within payloads β€” this is a known limitation under active development.

Go Deeper​

  • Message Flow β€” RabbitMQ queue sequences and the full operations/status-code tables
  • Payload Lifecycle β€” the exact PULL/PUSH sequences and the INIT handshake
  • Deep Architecture β€” daemon internals, multi-channel rationale, and modular abstractions from the ACM paper