BlackBeard.Telegraph
0.2.0
dotnet add package BlackBeard.Telegraph --version 0.2.0
NuGet\Install-Package BlackBeard.Telegraph -Version 0.2.0
<PackageReference Include="BlackBeard.Telegraph" Version="0.2.0" />
<PackageVersion Include="BlackBeard.Telegraph" Version="0.2.0" />
<PackageReference Include="BlackBeard.Telegraph" />
paket add BlackBeard.Telegraph --version 0.2.0
#r "nuget: BlackBeard.Telegraph, 0.2.0"
#:package BlackBeard.Telegraph@0.2.0
#addin nuget:?package=BlackBeard.Telegraph&version=0.2.0
#tool nuget:?package=BlackBeard.Telegraph&version=0.2.0
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 blockingPublish, and keeps it connected for later messages.DisconnectAfterTimeout: bounds how long a write may block, viaBackpressureTimeout(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 | Versions 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. |
-
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.