SRO.Net.BIDHASP Framework
SRO.Net.BIDHASP is a .NET Framework library that implements a Bidirectional Hub-and-Spoke Protocol (BIDHASP) for inter-process communication. It provides a structured, event-driven messaging architecture where a central hub server manages connections from multiple spoke clients, with full support for raw sockets, plain TCP streams, and TLS-encrypted streams.
Table of Contents
Architecture Overview
BIDHASP follows a hub-and-spoke topology:
[Spoke A] ──┐
[Spoke B] ──┼──► [Hub]
[Spoke C] ──┘
- The Hub is a TCP server that listens for incoming connections, assigns each connected spoke a unique integer ID, and can broadcast messages to all connected spokes.
- A Spoke is a TCP client that connects to the hub, registers itself with a hostname-derived name, and can send and receive typed data packets.
All data is exchanged as TransactedData packets — fixed-header, variable-payload frames that carry a BasicDataType (String or Binary), a DetailedDataType (e.g., PlainString, JSON, Int32), an application-defined MessageType, and the payload bytes.
Projects
| Project |
Description |
SRO.Net.BIDHASP.Core |
Core protocol classes, base communication units, configuration manager, and the BidiDataPipe transport abstraction. |
SRO.Net.BIDHASP.Hub |
HubManager — the server-side hub that accepts spoke connections and broadcasts messages. |
SRO.Net.BIDHASP.Spoke |
SpokeManager — the client-side spoke manager that connects to the hub and routes messages. |
SRO.TestHarness.POCO |
Plain-object test types (Person, Employee, Address) used by the test harnesses. |
BIDHASPHubTestHarness |
WinForms test application for the hub side. |
BIDHASPSpokeTestHarness |
WinForms test application for the spoke side. |
Getting Started
Configuration
Before creating a HubManager or SpokeManager, initialize the APIConfigurationManager. The configuration is read from a JSON file named {appname}.config.json.
Quick-start (development only — no TLS):
// Uses defaults: port 4800, localhost, plain Stream channel.
// Creates bidhasp.config.json in the user profile folder if it does not exist.
APIConfigurationManager.Initialize();
Named application config from user profile directory:
APIConfigurationManager.Initialize("myapp");
// Loads %USERPROFILE%\myapp.config.json
Named application config from an explicit directory:
APIConfigurationManager.Initialize(@"C:\myapp\config", "myapp");
// Loads C:\myapp\config\myapp.config.json
Configuration file structure (myapp.config.json):
{
"HubHost": "localhost",
"Port": 4800,
"CommunicationChannelType": "Stream",
"CertificateFilePath": "bidhasp_cert.pfx",
"KeyFilePath": "bidhasp_key.pem",
"Thumbprint": "",
"SubjectName": "",
"PathIsRelativeToProfileDirectory": true,
"PrivateKeyPassword": ""
}
| Field |
Default |
Description |
HubHost |
"localhost" |
Hostname or IP of the hub (used by spokes to connect). |
Port |
4800 |
TCP port for hub/spoke communication. |
CommunicationChannelType |
"Stream" |
Transport type: Socket, Stream, or TLSStream. |
CertificateFilePath |
"bidhasp_cert.pfx" |
Path to the TLS certificate (PFX or PEM). |
KeyFilePath |
"bidhasp_key.pem" |
Path to the PEM private-key file (used when the certificate is not a PFX). |
Thumbprint |
"" |
Certificate store thumbprint — takes lookup priority over files. |
SubjectName |
"" |
Certificate store subject name — second lookup priority. |
PathIsRelativeToProfileDirectory |
true |
When true, relative cert paths are resolved against %USERPROFILE%. |
PrivateKeyPassword |
"" |
Password for the PEM private-key file when it is encrypted. Omit or leave empty if the key is not password-protected. |
Hub Setup
using SRO.Net.BIDHASP.Core;
using SRO.Net.BIDHASP.Hub;
// 1. Initialize configuration.
APIConfigurationManager.Initialize();
// 2. Create the hub manager.
HubManager hub = new HubManager(); // uses the configured port (default: 4800)
// — or —
HubManager hub = new HubManager(5000); // explicit port override
// 3. Wire up logging.
hub.MessagePosted += (sender, e) =>
{
Console.WriteLine($"[{e.Level}] {e.Message}");
if (e.Cause != null) Console.WriteLine(e.Cause);
};
// 4. Handle spoke lifecycle events.
hub.SpokeWorkflowEvent += (sender, e) =>
{
if (e.EventType == SpokeWorkflowEventType.ASSIGNED_ID)
{
Console.WriteLine($"Spoke connected — ID: {e.Spoke.Id}, Name: {e.Spoke.Name}");
// Subscribe to per-spoke data events.
e.Spoke.ReceiveStringData += (s, args) =>
Console.WriteLine($"String from spoke {args.Id} (type {args.MessageType}): {args.Data}");
e.Spoke.ReceiveBinaryData += (s, args) =>
Console.WriteLine($"Binary from spoke {args.Id} (type {args.MessageType}): {args.Data.Length} bytes");
}
};
// 5. Start the hub — binds the socket and begins accepting connections.
hub.Start();
// 6. Broadcast to all connected spokes.
hub.Send(messageType: 1, data: "Hello, all spokes!");
hub.Send(messageType: 2, data: new byte[] { 0x01, 0x02, 0x03 });
// 7. Send to a specific spoke by ID.
Spoke target = hub.SpokeUnits.FirstOrDefault(s => s.Id == 3);
target?.Send(messageType: 1, stringInfo: "Hello, spoke 3 only!");
// 8. Stop the hub — closes all connections.
hub.Stop();
Spoke Setup
using SRO.Net.BIDHASP.Core;
using SRO.Net.BIDHASP.Spoke;
// 1. Initialize configuration.
APIConfigurationManager.Initialize();
// 2. Create the spoke manager.
SpokeManager spokeManager = new SpokeManager();
// — or —
SpokeManager spokeManager = new SpokeManager(hubHostname: "192.168.1.10", port: 4800);
// 3. Wire up logging.
spokeManager.MessagePosted += (sender, e) =>
Console.WriteLine($"[{e.Level}] {e.Message}");
// 4. Subscribe to data events on the hub facade.
spokeManager.Hub.ReceiveStringData += (sender, e) =>
Console.WriteLine($"Received string from hub (type {e.MessageType}): {e.Data}");
spokeManager.Hub.ReceiveBinaryData += (sender, e) =>
Console.WriteLine($"Received binary from hub (type {e.MessageType}): {e.Data.Length} bytes");
// 5. Subscribe to the ID-assignment event (fired once, shortly after Connect).
spokeManager.Hub.AssignedIdReceived += (sender, e) =>
Console.WriteLine($"Hub assigned this spoke ID: {e.AssignedId}");
// 6. Connect to the hub (synchronous — includes a brief settlement delay).
spokeManager.Hub.Connect();
// 7. Send data to the hub.
spokeManager.Hub.Send(messageType: 1, stringInfo: "Hello, hub!");
spokeManager.Hub.Send(messageType: 2, bytes: new byte[] { 0xAA, 0xBB });
Wire Protocol
Every message is framed as a TransactedData packet with a fixed 12-byte header followed by a variable-length payload:
Offset 0 4 6 7 8 12 12+N
+-------+-------+----+-------+----------+------+
| Id |MsgType|BDT | DDT |PayloadSz | ... |
| int32 |uint16 |byte| byte | uint32 | N |
+-------+-------+----+-------+----------+------+
|<------------- 12-byte header -------->|<pay->|
| Field |
Type |
Description |
Id |
int32 |
Sender's unit ID. Hub = 0, unregistered spoke = -2, registered spoke = positive integer. |
MessageType |
uint16 |
Application-defined discriminator identifying the message kind. |
BasicDataType |
byte |
Major data category: String (1) or Binary (2). |
DetailedDataType |
byte |
Sub-type such as PlainString, JSONSerialization, Int32 (see enum below). |
PacketSize |
uint32 |
Payload length in bytes. |
| Payload |
byte[] |
The actual data bytes. |
TransactedData can be serialized with ToByteArray() and deserialized with FromByteArray(byte[]). The payload size can be extracted from the header alone using GetHeaderPayloadSize(byte[]).
Sending and Receiving Data
Sending
Both HubManager and BIDCommunicationsUnitBase (the base for Spoke and the spoke-side Hub facade) expose the same family of send methods:
| Method |
Description |
Send(ushort messageType, byte[] bytes) |
Sends a raw byte array with DetailedDataType.ByteArray. |
Send(ushort messageType, string stringInfo) |
Sends a UTF-8 string with DetailedDataType.PlainString. |
BasicBinarySend(DetailedDataType ddt, ushort messageType, byte[] bytes) |
Sends bytes with an explicit DetailedDataType. |
BasicStringSend(DetailedDataType ddt, ushort messageType, string stringInfo) |
Sends a string with an explicit DetailedDataType. |
CoreSend(BasicDataType bdt, DetailedDataType ddt, ushort messageType, byte[] bytes) |
Low-level send with full type control. |
Note: HubManager.Send() broadcasts to all connected spokes. To target a specific spoke, call Send() directly on the Spoke instance retrieved from HubManager.SpokeUnits.
Receiving
Subscribe to the ReceiveStringData or ReceiveBinaryData events on the relevant object:
unit.ReceiveStringData += (sender, e) =>
{
// e.Id — sender's unit ID
// e.MessageType — application-defined message type
// e.DetailedDataType — e.g. PlainString, JSONSerialization
// e.Data — the string payload
};
unit.ReceiveBinaryData += (sender, e) =>
{
// e.Id — sender's unit ID
// e.MessageType — application-defined message type
// e.DetailedDataType — e.g. ByteArray, Int32, Double …
// e.Data — the raw byte array
};
Events
HubManager Events
| Event |
Args Type |
Description |
MessagePosted |
BIDHASPMessageEventArgs |
Diagnostic/logging messages from the hub and its spokes. |
ReceiveBinaryData |
ReceiveBinaryEventArgs |
Fired when any spoke sends binary data to the hub. |
ReceiveStringData |
ReceiveStringEventArgs |
Fired when any spoke sends string data to the hub. |
SpokeWorkflowEvent |
SpokeWorkflowEventArgs |
Spoke lifecycle events (see SpokeWorkflowEventType). |
SpokeWorkflowEventType Values
| Value |
Description |
NEW |
A new incoming socket connection has been accepted. |
CONNECTED |
The spoke's communication channel has been fully established. |
ASSIGNED_ID |
The spoke has been assigned a unique integer ID — safe to begin sending per-spoke data. |
DISCONNECTED |
The spoke has disconnected or its socket has closed. |
BIDCommunicationsUnitBase / Spoke / Hub Events
| Event |
Args Type |
Description |
MessagePosted |
BIDHASPMessageEventArgs |
Diagnostic/logging messages from this unit. |
ReceiveBinaryData |
ReceiveBinaryEventArgs |
Fired when binary data arrives from the partner. |
ReceiveStringData |
ReceiveStringEventArgs |
Fired when string data arrives from the partner. |
Hub (Spoke-side facade) Additional Event
| Event |
Args Type |
Description |
AssignedIdReceived |
AssignedIdEventArgs |
Fired once after Connect() when the hub assigns the spoke its unique ID. |
Communication Channel Types
| Type |
Enum Value |
Description |
| Direct Socket |
CommunicationChannelType.Socket |
Raw TCP socket with no stream wrapper. |
| Network Stream |
CommunicationChannelType.Stream |
NetworkStream wrapping the socket. Recommended for development. |
| TLS Stream |
CommunicationChannelType.TLSStream |
TLS 1.2/1.3 encrypted SslStream over a NetworkStream. Required for production. |
TLS Configuration
When CommunicationChannelType is TLSStream, the certificate is resolved in the following priority order:
- Thumbprint — Searches the
CurrentUser\My and LocalMachine\My certificate stores.
- Subject Name — Searches the same stores by subject name.
- File-based:
.pfx / .p12 — PKCS#12 bundle containing the certificate and private key.
- PEM certificate paired with a separate PEM private-key file (
KeyFilePath). If the private key is encrypted, set PrivateKeyPassword in the configuration file.
The hub authenticates as the TLS server using the loaded certificate; spokes authenticate as TLS clients against the configured HubHost. Both TLS 1.2 and TLS 1.3 are accepted.
Message Levels
BIDHASPMessageLevel categorizes messages surfaced by the MessagePosted event:
| Level |
Value |
Meaning |
Verbose |
0 |
Detailed trace output; safe to suppress in production. |
Information |
1 |
Normal operational status messages. |
Warning |
2 |
Something unexpected occurred but the system continues. |
Error |
3 |
A recoverable error that should be investigated. |
Fatal |
4 |
An unrecoverable error requiring immediate attention. |
API Reference
APIConfigurationManager (static — SRO.Net.BIDHASP.Core)
| Member |
Type |
Description |
Initialize() |
Method |
Loads or creates the default bidhasp.config.json in %USERPROFILE%. |
Initialize(appname) |
Method |
Loads {appname}.config.json from %USERPROFILE%. |
Initialize(folder, appname) |
Method |
Loads {appname}.config.json from an explicit absolute folder path. |
LoadConfiguration(folder, appname) |
Method |
Deserializes an existing configuration file into memory. |
SaveConfiguration(folder, appname) |
Method |
Serializes the current in-memory configuration to disk. |
HubHost |
string |
Configured hub hostname (default: "localhost"). |
Port |
int |
Configured port (default: 4800). |
CommunicationChannelType |
CommunicationChannelType |
Active transport type (Socket, Stream, or TLSStream). |
CertificateFilePath |
string |
Path to the TLS certificate file. |
KeyFilePath |
string |
Path to the PEM private-key file. |
Thumbprint |
string |
Certificate thumbprint for store-based lookup. |
SubjectName |
string |
Certificate subject name for store-based lookup. |
PathIsRelativeToProfileDirectory |
bool |
Whether relative cert paths are resolved against %USERPROFILE%. |
PrivateKeyPassword |
string |
Password for the encrypted PEM private-key file; null if the key is not password-protected. |
DefaultPort |
const int |
4800 |
DefaultAppName |
const string |
"bidhasp" |
DefaultHubHost |
const string |
"localhost" |
HubManager (SRO.Net.BIDHASP.Hub)
| Member |
Type |
Description |
HubManager() |
Constructor |
Creates a hub using the port from APIConfigurationManager. |
HubManager(int port) |
Constructor |
Creates a hub listening on the specified port. |
Start() |
Method |
Binds the TCP socket and begins accepting spoke connections. |
Stop() |
Method |
Closes all active spoke connections and the listening socket. |
Send(ushort, byte[]) |
Method |
Broadcasts a byte array to all connected spokes. |
Send(ushort, string) |
Method |
Broadcasts a UTF-8 string to all connected spokes. |
BasicBinarySend(DetailedDataType, ushort, byte[]) |
Method |
Broadcasts bytes with an explicit DetailedDataType. |
BasicStringSend(DetailedDataType, ushort, string) |
Method |
Broadcasts a string with an explicit DetailedDataType. |
CoreSend(BasicDataType, DetailedDataType, ushort, byte[]) |
Method |
Low-level broadcast with full type control. |
PostMessage(string, BIDHASPMessageLevel, Exception) |
Method |
Posts a diagnostic message (raises MessagePosted). |
Port |
int |
The port this hub is listening on. |
SpokeUnits |
List<Spoke> |
Live list of currently connected spoke units. |
Name |
string |
Always "#HUB#". |
Id |
int |
Always 0. |
State |
CommunicationsUnitState |
Current operational state of the hub. |
LastException |
Exception |
Last exception encountered by the hub, if any. |
UnitType |
CommunicationUnitType |
Always HubServer. |
MessagePosted |
Event |
Diagnostic/logging messages. |
ReceiveBinaryData |
Event |
Aggregated binary data received from all spokes. |
ReceiveStringData |
Event |
Aggregated string data received from all spokes. |
SpokeWorkflowEvent |
Event |
Spoke lifecycle notifications. |
SpokeManager (SRO.Net.BIDHASP.Spoke)
| Member |
Type |
Description |
SpokeManager() |
Constructor |
Creates a spoke manager using configured hub hostname and port. |
SpokeManager(string hubHostname, int port) |
Constructor |
Creates a spoke manager with explicit hub address. |
Hub |
Hub |
The hub facade; call Connect() and Send() through this property. |
HubHostname |
string |
The hub's hostname. |
Port |
int |
The hub's port. |
PostMessage(string, BIDHASPMessageLevel, Exception) |
Method |
Posts a diagnostic message (raises MessagePosted). |
MessagePosted |
Event |
Diagnostic/logging messages from the spoke and its hub channel. |
Hub — Spoke-side facade (SRO.Net.BIDHASP.Spoke)
| Member |
Type |
Description |
Connect() |
Method |
Resolves the hub's IP, opens a TCP connection, and completes the name-assignment handshake. |
Send(ushort, byte[]) |
Method |
Sends a byte array to the hub. |
Send(ushort, string) |
Method |
Sends a UTF-8 string to the hub. |
BasicBinarySend(DetailedDataType, ushort, byte[]) |
Method |
Sends bytes with an explicit DetailedDataType. |
BasicStringSend(DetailedDataType, ushort, string) |
Method |
Sends a string with an explicit DetailedDataType. |
Manager |
SpokeManager |
The owning SpokeManager. |
HubHostname |
string |
The hub's hostname. |
Port |
int |
The hub's port. |
Id |
int |
This spoke's hub-assigned ID (available after AssignedIdReceived fires). |
State |
CommunicationsUnitState |
Current connection state. |
AssignedIdReceived |
Event |
Fires once after Connect() when the hub assigns the spoke its ID. |
ReceiveBinaryData |
Event |
Fired when binary data arrives from the hub. |
ReceiveStringData |
Event |
Fired when string data arrives from the hub. |
MessagePosted |
Event |
Diagnostic/logging messages. |
DetailedDataType Enum (SRO.Net.BIDHASP.Core)
| Value |
Description |
Undefined (0) |
Unspecified — plain byte array or string with no further sub-typing. |
RequestName (1) |
Control: spoke sends its hostname to the hub to request a name assignment. |
AssignId (2) |
Control: hub sends the spoke's assigned integer ID (int32 payload). |
ByteArray (3) |
Generic raw byte array. |
PlainString (4) |
Plain UTF-8 text. |
JSONSerialization (5) |
JSON-encoded string. |
Int8 (6) |
Signed 8-bit integer. |
UInt8 (7) |
Unsigned 8-bit integer. |
Int16 (8) |
Signed 16-bit integer. |
UInt16 (9) |
Unsigned 16-bit integer. |
Int32 (10) |
Signed 32-bit integer. |
UInt32 (11) |
Unsigned 32-bit integer. |
Int64 (12) |
Signed 64-bit integer. |
UInt64 (13) |
Unsigned 64-bit integer. |
Float (14) |
32-bit IEEE floating-point. |
Double (15) |
64-bit IEEE floating-point. |
Boolean (16) |
Boolean value. |
Character (17) |
Single character. |
Decimal (18) |
128-bit decimal value. |
DateTime (19) |
DateTime.Ticks encoded as a signed 64-bit integer. |
BinarySerialization (20) |
Custom plug-in binary serialization (see ExternalBinarySerializationManager). |
CommunicationsUnitState Enum (SRO.Net.BIDHASP.Core)
| Value |
Description |
Undefined (-1) |
Not yet initialized. |
Disconnected (0) |
Not connected to a partner (spoke/client state). |
Connected (1) |
TCP connection established and channel active (spoke/client state). |
Active (2) |
Hub is running and accepting connections (hub state). |
Inactive (3) |
Hub has been stopped (hub state). |
ExceptionDetected (4) |
A socket or stream error occurred; check LastException. |
BIDCommunicationsUnitBase Constants (SRO.Net.BIDHASP.Core)
| Constant |
Value |
Description |
HUB_ID |
0 |
Reserved ID for the hub. |
UNDEFINED_ID |
-1 |
Default ID before a unit is registered. |
CONTROL_MSG_ID |
-2 |
Temporary ID used during the connection handshake. |