# SockBus Specification v2.2.0 **Author:** Are Bjørby **Date:** 2026-08-18 **Language:** C++17 **License:** MIT This document is **library-versioned**: its version is the `libsockbus` release it describes, and it moves when the library does. It was previously spec-versioned and the two drifted apart on day one — spec v2.0.0 is dated 2026-03-16 and library v2.0.0 is dated 2026-03-17, a different release one day later. --- ## 1. Overview SockBus is a TCP socket IPC library. It provides a broker-based broadcast bus with length-prefixed framing over TCP. Bytes in, bytes out, broadcast to all readers including sender. Completely dumb — no routing, no parsing, no protocol logic. **Design principles:** - Completely dumb. Raw byte transport only. - Broker per bus. Each bus runs a listener thread accepting TCP connections. - Broadcast semantics. All readers (including the writer) receive every write. - Lossy for slow readers — frames dropped, not buffered. - Thread-safe client handles via per-handle mutex. - Length-prefixed framing preserves message boundaries. --- ## 2. Architecture ### 2.1 Broker Model Each `sockbus::create()` spawns a broker thread that: 1. Binds and listens on the resolved `host:port`. 2. Accepts client connections (up to `MAX_READERS`). 3. Receives framed messages from any client. 4. Broadcasts each received frame to all connected clients (including the sender). 5. Drops slow readers that cannot accept a frame without blocking. Brokers are stored in a process-global registry (max `MAX_BROKERS`, 16). The registry is protected by a global mutex. ### 2.2 Client Handle Constructing a `sockbus::Bus` connects to a broker. The handle is RAII and move-only, with its state behind a pimpl pointer: - TCP socket file descriptor - Receive buffer (default 64 KB) - Per-handle mutex protecting receive state - Bus name and debug label There is no open function and no close function. A constructed `Bus` is connected, and its destructor disconnects. ### 2.3 Wire Frame ``` ┌──────────────────────────┬─────────────────────┐ │ 4 bytes: payload length │ payload (N bytes) │ │ (uint32_t, network order)│ │ └──────────────────────────┴─────────────────────┘ ``` Each write produces exactly one frame. Each read returns exactly one frame's payload. The 4-byte header preserves message boundaries over the TCP byte stream. ### 2.4 Name Resolution Bus names are resolved in two ways: 1. **Literal:** If the name contains `:`, it is parsed directly as `host:port`. 2. **Symbolic:** Otherwise, the name is looked up in a config file: - `$SOCKBUS_CONF` environment variable, or - `/etc/sockbus/buses.conf` (default) - Format: `name host:port` (one per line, `#` comments, blank lines OK) --- ## 3. Constants Public, in `include/sockbus.hpp`, namespace `sockbus`: | Constant | Value | Description | |----------|-------|-------------| | `DEFAULT_SIZE` | 65536 (64 KB) | Default per-client recv buffer capacity | | `MAX_READERS` | 32 | Maximum concurrent clients per broker | | `READER_NAME_LEN` | 32 | Maximum reader debug label length, including the terminator | | `MAX_MSG_SIZE` | 65532 (`DEFAULT_SIZE - 4`) | Maximum payload (recv buffer minus frame header) | Internal, in `src/sockbus.cpp`, not part of the public surface: | Constant | Value | Description | |----------|-------|-------------| | `MAX_BROKERS` | 16 | Maximum broker threads per process | | `FRAME_HDR_SIZE` | 4 | Wire frame header size (bytes) | There is **no maximum bus name length**. A bus name is a `std::string` and is bounded only by memory. A `MAX_NAME = 256` constant was published here and in the public header until v2.2.0; it was a survival from the C11 implementation, whose handle stored the name in a `char name[256]`. Nothing enforced it after the rewrite and nothing referenced it, so it promised a limit that did not exist. --- --- ## 4. Data Structures ### 4.1 sockbus::Bus (Client Handle) An RAII handle. Constructing one connects; destroying one disconnects, closes the socket and releases the buffer. There is no free function to close it and no pointer to own. `Bus` is **move-only** — move-constructible and move-assignable, not copyable — and uses the pimpl idiom, so no implementation state appears in the public header and the ABI is stable across implementation changes. The fields below are behind that pointer and are described for understanding, not as a contract: | Field | Type | Description | |-------|------|-------------| | bus_name | `std::string` | Bus name | | fd | `int` | TCP socket file descriptor | | reader_name | `char[READER_NAME_LEN]` | Debug label | | mtx | `std::mutex` | Protects recv state | | recv_buf | `std::vector` | Receive buffer | | recv_len | `size_t` | Bytes currently in buffer | ### 4.2 Broker (Internal) Per-bus broker state, held in a process-global registry. Entirely internal; no part of it is reachable from the public header. | Field | Type | Description | |-------|------|-------------| | name | `std::string` | Bus name | | listen_fd | `int` | TCP listener socket | | thread | `std::thread` | Broker thread | | running | `std::atomic` | Shutdown flag | | pipe_fd | `int[2]` | Shutdown signal pipe | | buf_size | `size_t` | Per-client recv buffer size | | fds | `int[MAX_READERS]` | Client socket array | | recv_bufs | per-client buffers | Receive buffers | | count | `int` | Active client count | | mtx | `std::mutex` | Protects client arrays | --- --- ## 5. Broker Loop The broker runs in a dedicated thread, using `poll()` with 100ms timeout: 1. **Poll** on: shutdown pipe, listener socket, all client sockets. 2. **Shutdown:** If the pipe is readable, exit the loop. 3. **Accept:** New connections added to client arrays. `TCP_NODELAY` set. If at capacity (`sockbus::MAX_READERS`), new connections are immediately closed. 4. **Receive:** Read available data from each ready client into its recv buffer. Clients that return 0 or error are marked dead. 5. **Extract & Broadcast:** For each client with buffered data, extract complete frames. Each frame is broadcast to ALL clients (including sender) via `MSG_DONTWAIT | MSG_NOSIGNAL`. - `EAGAIN`/`EWOULDBLOCK`: frame dropped for that reader (lossy). - Partial send: reader marked dead (stream corrupted). 6. **Cleanup:** Dead clients removed in reverse index order (stable indices). 7. **Exit cleanup:** All remaining client sockets closed, buffers freed. --- ## 6. API Reference All entry points are in namespace `sockbus`, declared in `include/sockbus.hpp`. Failures are reported by **throwing `std::system_error`**, never by a return code; see section 7. ### 6.1 sockbus::create ```cpp void create(std::string_view name, size_t size = 0); ``` Start a new broker (TCP listener thread) for the named bus. | Parameter | Description | |-----------|-------------| | name | Bus name or literal `host:port` | | size | Per-client recv buffer size (0 = `DEFAULT_SIZE`) | **Returns:** nothing. Throws on failure. ### 6.2 sockbus::Bus::Bus ```cpp explicit Bus(std::string_view name); ``` Connect to an existing bus broker. This is the C++ replacement for an open function: there is no separate open step and no handle to check for null. | Parameter | Description | |-----------|-------------| | name | Bus name or literal `host:port` | **Returns:** a connected `Bus`. Throws on connection failure, so a constructed `Bus` is always connected. ### 6.3 sockbus::Bus::write ```cpp size_t write(const uint8_t* data, size_t len); size_t write(std::string_view sv); ``` Write a framed message to the bus. Broadcast to all connected peers, including the sender. | Parameter | Description | |-----------|-------------| | data / sv | Payload | | len | Payload size in bytes | **Returns:** payload bytes written. Throws `std::system_error(EMSGSIZE)` if the payload exceeds `MAX_MSG_SIZE`. ### 6.4 sockbus::Bus::read ```cpp size_t read(uint8_t* buf, size_t len); ``` Non-blocking read. Returns one complete message per call. | Parameter | Description | |-----------|-------------| | buf | Destination buffer | | len | Maximum bytes to read | **Returns:** bytes read, or 0 if no complete message is available. Throws `std::system_error(ECONNRESET)` if the broker closed the connection. ### 6.5 sockbus::Bus::read_wait ```cpp size_t read_wait(uint8_t* buf, size_t len, int timeout_ms = -1); ``` Blocking read with timeout. Uses `poll()` to wait for data. | Parameter | Description | |-----------|-------------| | buf | Destination buffer | | len | Maximum bytes to read | | timeout_ms | -1 = block forever, 0 = poll (same as `read`), >0 = ms | **Returns:** bytes read, or 0 on timeout with no data. ### 6.6 sockbus::Bus::~Bus ```cpp ~Bus(); ``` Disconnect. Closes the socket and releases the buffer. Runs at scope exit; there is no close call to forget and no double-close to guard against. ### 6.7 sockbus::destroy ```cpp void destroy(std::string_view name); ``` Stop the broker, close the listener, disconnect all clients and remove the registry entry. Signals the broker thread via its shutdown pipe and joins it. **Returns:** nothing. Throws `std::system_error(ENOENT)` if no broker is registered for the name. ### 6.8 sockbus::Bus::set_reader_name ```cpp void set_reader_name(std::string_view label); ``` Set a human-readable debug label for this connection. Truncated to `READER_NAME_LEN - 1` (31 characters). ### 6.9 sockbus::Bus::name ```cpp std::string_view name() const; ``` The bus name this handle is connected to. --- --- ## 7. Error Handling Every function that can fail **throws `std::system_error`**, carrying the errno value below in `std::system_category()`. Nothing returns -1, nothing returns null, and `errno` is not part of the contract — a caller reads `.code().value()`. | Code | Condition | |------|-----------| | `EINVAL` | Empty name, invalid address format | | `EEXIST` | Broker already registered for this name | | `ENOMEM` | `MAX_BROKERS` limit reached | | `ENOENT` | No broker registered for name (destroy), config entry not found | | `EMSGSIZE` | Payload exceeds `MAX_MSG_SIZE` | | `ECONNRESET` | Broker closed the connection | A read that finds no complete message is **not** an error: `read` returns 0, and so does `read_wait` on timeout. --- --- ## 8. Threading - **Broker thread:** one `std::thread` per bus, owning all client socket I/O, protected by a per-broker `std::mutex`. - **Client handles:** each holds a `std::mutex` protecting recv buffer state. A handle may be shared between threads if they serialize access. `Bus` is move-only, so ownership is explicit rather than shared by default. - **Global registry:** protected by a process-global `std::mutex`, taken during create and destroy. - **Shutdown:** a `std::atomic` running flag signals between the destroying thread and the broker thread, alongside the shutdown pipe that wakes `poll()`. --- --- ## 9. Platform Requirements - **OS:** Linux (uses `accept4`, `pipe2`, `SOCK_CLOEXEC`, `SOCK_NONBLOCK`) - **Standard:** C++17 (`-std=c++17`) - **POSIX:** `poll`, TCP sockets - **Libraries:** `-lpthread` (for `std::thread`) --- --- ## 10. Build ```bash make # Build libsockbus.a + libsockbus.so make test # Build and run the unit test suite sudo make install # Install to /usr/local/{lib,include} make clean # Remove build artifacts ``` **Compiler flags:** `-std=c++17 -Wall -Wextra -Werror -O2 -fPIC` **Link flags:** `-lpthread` **Output:** `build/libsockbus.a` (static) and `build/libsockbus.so` (shared) **Install targets:** `/usr/local/lib/libsockbus.{a,so}`, `/usr/local/include/sockbus.hpp` The suite's size is what `make test` prints. This document stated a count until v2.2.0 and it was the C11 figure, four years of releases out of date. --- --- ## 11. Limitations - **Linux only.** Uses GNU extensions (`accept4`, `pipe2`, `SOCK_CLOEXEC`). - **Lossy by design.** Slow readers silently lose frames. Architectural choice. - **No message ordering guarantees across buses.** Each bus is independent. - **32-client hard limit** per broker. Compile-time constant. - **16-broker hard limit** per process. Compile-time constant. - **Partial send = dead client.** If the kernel cannot send a complete frame in one `MSG_DONTWAIT` call, the client's stream is considered corrupted and the client is disconnected. - **No reconnect logic.** Consumer responsibility. - **No encryption.** Plaintext TCP. Intended for trusted networks.