BlackBeard.Telegraph 0.2.0

dotnet add package BlackBeard.Telegraph --version 0.2.0
                    
NuGet\Install-Package BlackBeard.Telegraph -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BlackBeard.Telegraph" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BlackBeard.Telegraph" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="BlackBeard.Telegraph" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BlackBeard.Telegraph --version 0.2.0
                    
#r "nuget: BlackBeard.Telegraph, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BlackBeard.Telegraph@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BlackBeard.Telegraph&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=BlackBeard.Telegraph&version=0.2.0
                    
Install as a Cake Tool

Telegraph

A small, generic newline-delimited-JSON-over-TCP pub/sub transport for .NET. It has no opinion about what you send over it.

dotnet add package BlackBeard.Telegraph

Why

Sometimes you want to move some data from one process to another — a console host publishing a live feed, a UI app subscribing to it — without designing a wire protocol, writing a client SDK, or standing up a broker. Telegraph is that: one TCP port, one JSON object per line, any shape.

Quick start

// Publisher (e.g. a console host generating or replaying data)
using var publisher = new TelegraphPublisher(5000);
await publisher.StartAsync();

publisher.Publish(new { Message = "hello" });

// Subscriber (e.g. a UI app)
using var subscriber = new TelegraphSubscriber("localhost", 5000);
await subscriber.ConnectAsync();

await foreach (var item in subscriber.ReadAsync<MyMessageType>())
{
    // ...
}

Publish<T>/ReadAsync<T> are generic over any type System.Text.Json can (de)serialise. TelegraphEnvelope/Pose6Dof (below) are one ready-made shape for 6DOF-plus-metadata streams, not a requirement.

Wire format

Newline-delimited UTF-8 JSON: one message per line, on a plain TCP stream. No framing beyond the newline, no handshake, no compression. Inspectable with nc localhost 5000. A late-connecting subscriber only sees messages published after it connects — there is no replay buffer.

This is the default (TelegraphFraming.NewlineDelimited), not the only option. If a message's JSON could ever contain a raw newline byte, pass TelegraphFraming.LengthPrefixed to both the publisher and subscriber constructors instead: a 4-byte big-endian length prefix ahead of each message rather than a trailing \n. It gives up nc-inspectability for immunity to that failure mode — pick whichever trade-off fits.

TLS

Plaintext by default, matching the nc-inspectable quick start above. For anything crossing a network boundary that isn't already trusted, pass SslServerAuthenticationOptions/ SslClientAuthenticationOptions to the publisher/subscriber constructors instead:

using var publisher = new TelegraphPublisher(5000, new SslServerAuthenticationOptions
{
    ServerCertificate = myCertificate,
});

using var subscriber = new TelegraphSubscriber("localhost", 5000, new SslClientAuthenticationOptions
{
    TargetHost = "localhost",
});

A connection whose handshake the publisher's own AuthenticateAsServerAsync step rejects (a protocol/cipher mismatch, a required client certificate that's missing) is dropped before it is ever added to the broadcast list, so it never receives a partial message and never counts toward SubscriberCount. A subscriber that locally distrusts the publisher's certificate is a different case: from the publisher's side that handshake step can still complete, since trust is the client's own decision to make and there's no protocol-level step where it reports that decision back — that connection is cleaned up the same way any other dead one is, the next time Publish tries to write to it and fails.

Pre-shared-key handshake

Open to any connection by default. For "don't let an arbitrary process on this host or LAN subscribe to my stream" without provisioning PKI, pass a shared secret to both constructors:

using var publisher = new TelegraphPublisher(5000, "correct-horse-battery-staple");
using var subscriber = new TelegraphSubscriber("localhost", 5000, "correct-horse-battery-staple");

The subscriber proves it knows the secret (a keyed hash of a publisher-issued nonce) before it's added to the broadcast list; a mismatch closes the connection immediately, before any application data is exchanged. This authenticates the connection, not the transport — the stream itself stays plaintext, so it is not a substitute for TLS where the network itself isn't trusted.

CIDR allow-list

Accepts a connection from any address by default. For "only this /24 may subscribe" without standing up firewall rules per-process, add to AllowedRanges:

using var publisher = new TelegraphPublisher(5000)
{
    AllowedRanges = { IPNetwork.Parse("10.0.0.0/24") },
};

A connection from outside every configured range is closed immediately after being accepted, before it's added to the broadcast list — it never receives a partial message and never counts toward SubscriberCount. Populate it before calling StartAsync; it's read without synchronisation while the accept loop is running.

UDP transport

For high-rate telemetry where a dropped packet beats head-of-line blocking, or a subscriber that only cares about the latest value: TelegraphUdpPublisher/TelegraphUdpSubscriber sit next to the TCP pair above, not in place of them.

using var publisher = new TelegraphUdpPublisher(5000);
await publisher.StartAsync();
publisher.Publish(new { Message = "hello" });

using var subscriber = new TelegraphUdpSubscriber("localhost", 5000);
await subscriber.ConnectAsync();
await foreach (var item in subscriber.ReadAsync<MyMessageType>())
{
    // ...
}

One JSON object per datagram — UDP already delivers message boundaries, so there's no newline framing to worry about. No reliability, ordering, or delivery guarantees of any kind; that's the trade for reaching past TCP. A message over roughly 1472 bytes (TelegraphUdpPublisher.MaxDatagramSize) throws on Publish instead of being silently fragmented by the OS.

Bind address

Binds to IPAddress.Any (every interface) by default. To keep the publisher off the network entirely, or to pick one interface on a multi-homed host, pass a bind address explicitly:

using var publisher = new TelegraphPublisher(IPAddress.Loopback, 5000);

This overload composes with the framing, TLS, and pre-shared-key constructors above — it's the same bindAddress parameter on each of them.

Subscriber visibility

SubscriberCount is a number; Subscribers is the list behind it — one TelegraphSubscriberInfo per connection, with RemoteEndPoint, ConnectedAt, BytesSent, and MessagesSent:

foreach (TelegraphSubscriberInfo subscriber in publisher.Subscribers)
{
    Console.WriteLine($"{subscriber.RemoteEndPoint} connected {subscriber.ConnectedAt}, " +
        $"{subscriber.MessagesSent} messages / {subscriber.BytesSent} bytes sent");
}

Pass one of those back to Disconnect to drop just that subscriber, without affecting anyone else or the publisher itself:

publisher.Disconnect(subscriber);

Disconnect returns false rather than throwing if that subscriber had already disconnected on its own (e.g. a dead connection cleaned up during a prior Publish) — there's nothing left to do.

Backpressure policy

Publish blocks until a slow subscriber's socket buffer drains by default — the original behaviour, and still the right one when losing a message is worse than a little latency. Set BackpressurePolicy for a different trade-off:

using var publisher = new TelegraphPublisher(5000)
{
    BackpressurePolicy = TelegraphBackpressurePolicy.DropForSlowSubscriber,
};
  • BlockUntilDrained (the default): waits for the write to complete, however long that takes.
  • DropForSlowSubscriber: skips a subscriber whose buffer is already full for that one message rather than blocking Publish, and keeps it connected for later messages.
  • DisconnectAfterTimeout: bounds how long a write may block, via BackpressureTimeout (default 30 seconds), before dropping that subscriber the same way any other dead connection is dropped.

Pose6Dof and TelegraphEnvelope

An opt-in message shape for 6DOF-plus-metadata streams, so that use case doesn't require designing a type first. Using it is optional — see Quick start above for sending anything else.

The coordinate frame, units, and rotation representation are fixed and documented on the type itself (Pose6Dof.cs), not left to be rediscovered per consumer:

  • Position: WGS84 geodetic — latitude/longitude in degrees, altitude in metres above the reference ellipsoid.
  • Attitude: a unit quaternion (X/Y/Z/W) as the primary representation, with roll/pitch/yaw in degrees carried alongside as a convenience.
  • Linear velocity: local North/East/Down, in metres per second.
  • Angular velocity: body-frame X/Y/Z, in degrees per second.

Every field is nullable; an unsupplied field is null, never 0 — so a downstream mapping to a richer domain type is a field-for-field copy rather than a guess about whether a zero was measured or missing.

TelegraphEnvelope wraps a Pose6Dof? with an EntityId, an optional GroupTag (Telegraph never inspects it — it exists because EntityId alone is not guaranteed unique across producers a subscriber might combine), a TimestampUtc, and a generic IReadOnlyDictionary<string, string>? for anything else.

What this package deliberately does not do

  • No topics or multiplexing. One connection is one implicit channel. (A plausible v2, not built.)
  • No message replay/buffering for late subscribers.
  • No retry/reconnect logic — that's a caller concern, since what "retry" should mean depends on the caller's own semantics (resume from where? tolerate a gap? reset state?).
  • No knowledge of any particular domain. If you're looking for geospatial stream diagnostics, that is a different package entirely; Telegraph doesn't know it exists.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 150 9/15/2026
0.1.1 137 8/31/2026
0.1.0 93 8/31/2026