![]() |
VOOZH | about |
dotnet add package WatsonTcp --version 6.3.1
NuGet\Install-Package WatsonTcp -Version 6.3.1
<PackageReference Include="WatsonTcp" Version="6.3.1" />
<PackageVersion Include="WatsonTcp" Version="6.3.1" />Directory.Packages.props
<PackageReference Include="WatsonTcp" />Project file
paket add WatsonTcp --version 6.3.1
#r "nuget: WatsonTcp, 6.3.1"
#:package WatsonTcp@6.3.1
#addin nuget:?package=WatsonTcp&version=6.3.1Install as a Cake Addin
#tool nuget:?package=WatsonTcp&version=6.3.1Install as a Cake Tool
WatsonTcp is the fastest, easiest, most efficient way to build TCP-based clients and servers in C# with integrated framing, reliable transmission, and fast disconnect detection.
IMPORTANT WatsonTcp provides framing to ensure message-level delivery which also dictates that you must either 1) use WatsonTcp for both the server and the client, or, 2) ensure that your client/server exchange messages with the WatsonTcp node using WatsonTcp's framing. Refer to FRAMING.md for a reference on WatsonTcp message structure.
This project is part of the .NET Foundation along with other projects like the .NET Runtime.
Special thanks to the following people for their support and contributions to this project!
@brudo @MrMikeJJ @mikkleini @pha3z @crushedice @marek-petak @ozrecsec @developervariety @NormenSchwettmann @karstennilsen @motridox @AdamFrisby @Job79 @Dijkstra-ru @playingoDEERUX @DuAell @syntacs @zsolt777 @broms95 @Antwns @MartyIX @Jyck @Memphizzz @nirajgenius @cee-sharp @jeverz @cbarraco @DenisBalan @Markonius @Ahmed310 @markashleybell @thechosensausage @JVemon @eatyouroats @bendablegears @Laiteux @fisherman6v6 @wesoos @YorVeX @tovich37 @sancheolz @lunedis @ShayanFiroozi
If you'd like to contribute, please jump right into the source code and create a pull request, or, file an issue with your enhancement request.
WatsonTcp now includes a performance-focused maintenance release aimed at lower latency, higher throughput, and faster steady-state operation without changing the public API surface for normal usage.
Key improvements include:
The repository now includes src/Test.PerformanceBenchmark, a console benchmark suite that measures:
Run it from the repository root with:
RunBenchmarks.bat
The benchmark writes timestamped summaries to benchmarks/.
WatsonTcp now supports awaited stream callbacks on both server and client:
server.Callbacks.StreamReceivedAsync = async (args, token) =>
{
await using var file = File.Create("payload.bin");
await args.DataStream.CopyToAsync(file, 81920, token);
};
client.Callbacks.StreamReceivedAsync = async (args, token) =>
{
await using var file = File.Create("server-payload.bin");
await args.DataStream.CopyToAsync(file, 81920, token);
};
Use Callbacks.StreamReceivedAsync when you need to await stream processing, especially for payloads at or above Settings.MaxProxiedStreamSize.
Receive mode selection is now:
Events.MessageReceivedCallbacks.StreamReceivedAsyncEvents.StreamReceivedWarnings for conflicting receive-mode configuration are emitted through Settings.Logger.
Shared Touchstone-backed coverage now includes:
WatsonTcp now supports an explicit server-side admission callback:
server.Callbacks.AuthorizeConnectionAsync = async (ctx, token) =>
{
if (!ctx.IpPort.StartsWith("127.0.0.1"))
return ConnectionAuthorizationResult.Reject("Local connections only.");
return ConnectionAuthorizationResult.Allow();
};
Rejected connections raise ConnectionRejected events and can surface ConnectionRejectedException on compatible clients.
WatsonTcp also supports framed pre-registration handshakes without exposing the raw stream:
server.Callbacks.HandshakeAsync = async (session, token) =>
{
HandshakeMessage msg = await session.ReceiveAsync(token);
string apiKey = Encoding.UTF8.GetString(msg.Data);
return apiKey == "valid-api-key-123"
? HandshakeResult.Succeed()
: HandshakeResult.Fail("Invalid API key.");
};
client.Callbacks.HandshakeAsync = async (session, token) =>
{
await session.SendAsync(new HandshakeMessage
{
Type = "api-key",
Data = Encoding.UTF8.GetBytes("valid-api-key-123")
}, token);
return HandshakeResult.Succeed();
};
Handshake-enabled servers require compatible clients that understand the new control-plane statuses.
Automated tests are now defined once in src/Test.Shared and exposed through:
src/Test.Automated for the Touchstone CLI runnersrc/Test.XUnit for dotnet test via xUnitsrc/Test.NUnit for dotnet test via NUnitRun them with:
dotnet run --project src/Test.Automated --framework net8.0 -- --results test-results/cli-results.json
dotnet test src/Test.XUnit/Test.XUnit.csproj --framework net8.0
dotnet test src/Test.NUnit/Test.NUnit.csproj --framework net8.0
MemoryStream accumulator with direct byte comparisonArrayPool<byte> pooling instead of allocating new buffers on every iterationClientMetadataManager from 5 independent ReaderWriterLockSlim instances to a single lock, eliminating race conditions during multi-dictionary operations (ReplaceGuid, Remove)GetClient() (ContainsKey then indexer across separate lock acquisitions); now uses TryGetValueAutoResetEvent + event-based sync response matching with ConcurrentDictionary<Guid, TaskCompletionSource<SyncResponse>> in both client and server, eliminating handler registration race conditions and signal lossWaitHandle resource leak in WatsonTcpClient.Connect() (was commented out, now properly closed)ClientMetadata.Dispose() and WatsonTcpClient.Disconnect() with Task.Wait(timeout)Settings.MaxHeaderSize (client and server, default 256KB) guards against memory exhaustion from oversized or malicious headersSettings.EnforceMaxConnections (server, default true) actively rejects connections at capacity; set to false for legacy behaviorTaskCanceledException and OperationCanceledException catch blocksSettings.EnforceMaxConnections defaults to true. If you relied on accepting connections beyond MaxConnections, set EnforceMaxConnections = false.SyncMessageReceived callbackRefer to for a detailed overview of the internal design, message flow, threading model, and key design decisions.
For the wire protocol specification (header format, delimiter, payload layout), see .
Test projects for both client and server are included which will help you understand and exercise the class library. Shared automated coverage lives in Test.Shared, while Test.Automated, Test.XUnit, and Test.NUnit are the supported unattended test hosts.
WatsonTcp supports data exchange with or without SSL. The server and client classes include constructors that allow you to include fields for the PFX certificate file and password. An example certificate can be found in the test projects, which has a password of 'password'.
WatsonTcp allows you to receive messages using either byte arrays or streams.
Events.MessageReceived if you want a buffered byte[]Callbacks.StreamReceivedAsync if you want awaited stream ownershipEvents.StreamReceived if you want the legacy synchronous stream eventIt is important to note the following:
Events.MessageReceived
Callbacks.StreamReceivedAsync
Settings.MaxProxiedStreamSize, WatsonTcp first copies it into a MemoryStreamSettings.MaxProxiedStreamSize, the callback receives a live proxied stream over the underlying connectionEvents.StreamReceived
Callbacks.StreamReceivedAsync for new workEvents.MessageReceived > Callbacks.StreamReceivedAsync > Events.StreamReceivedSettings.LoggerExample:
server.Callbacks.StreamReceivedAsync = async (args, token) =>
{
using MemoryStream ms = new MemoryStream();
await args.DataStream.CopyToAsync(ms, 81920, token);
Console.WriteLine("Received " + ms.Length + " bytes");
};
Should you with to include metadata with any message, use the Send or SendAsync method that allows you to pass in metadata (Dictionary<string, object>). Refer to the TestClient, TestServer, TestClientStream, and TestServerStream projects for a full example. Keys must be of type string.
Note: if you use a class instance as either the value, you'll need to deserialize on the receiving end from JSON.
object myVal = args.Metadata["myKey"];
MyClass instance = myVal.ToObject<MyClass>();
This is not necessary if you are using simple types (int, string, etc). Simply cast to the simple type.
IMPORTANT
Metadata is serialized into the message header as JSON, increasing header size. While v6.1.0 significantly improved header parsing performance (eliminating O(n^2) allocations), it is still recommended to keep metadata small (less than 1KB) as large metadata increases JSON serialization overhead and network transfer time. Use Settings.MaxHeaderSize to control the maximum allowed header size (default 256KB).
IMPORTANT
127.0.0.1 as the listener IP address in WatsonTcpServer, it will only be able to accept connections from within the local host.null, *, +, or 0.0.0.0 for the listener IP address (requires admin privileges to listen on any IP address).NET Core should always be the preferred option for multi-platform deployments. However, WatsonTcp works well in Mono environments with the .NET Framework to the extent that we have tested it. It is recommended that when running under Mono, you execute the containing EXE using --server and after using the Mono Ahead-of-Time Compiler (AOT). Note that TLS 1.2 is hard-coded, which may need to be downgraded to TLS in Mono environments.
NOTE: Windows accepts '0.0.0.0' as an IP address representing any interface. On Mac and Linux you must be specified ('127.0.0.1' is also acceptable, but '0.0.0.0' is NOT).
mono --aot=nrgctx-trampolines=8096,nimt-trampolines=8096,ntrampolines=4048 --server myapp.exe
mono --server myapp.exe
The following examples show a simple client and server example using WatsonTcp without SSL and consuming messages using byte arrays instead of streams. For full examples, please refer to the Test.* projects.
using WatsonTcp;
static void Main(string[] args)
{
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000);
server.Events.ClientConnected += ClientConnected;
server.Events.ClientDisconnected += ClientDisconnected;
server.Events.MessageReceived += MessageReceived;
server.Callbacks.SyncRequestReceivedAsync = SyncRequestReceived;
server.Start();
// list clients
IEnumerable<ClientMetadata> clients = server.ListClients();
// send a message
await server.SendAsync([guid], "Hello, client!");
// send a message with metadata
Dictionary<string, object> md = new Dictionary<string, object>();
md.Add("foo", "bar");
await server.SendAsync([guid], "Hello, client! Here's some metadata!", md);
// send and wait for a response
try
{
SyncResponse resp = await server.SendAndWaitAsync(
[guid],
5000,
"Hey, say hello back within 5 seconds!");
Console.WriteLine("My friend says: " + Encoding.UTF8.GetString(resp.Data));
}
catch (TimeoutException)
{
Console.WriteLine("Too slow...");
}
}
static void ClientConnected(object sender, ConnectionEventArgs args)
{
Console.WriteLine("Client connected: " + args.Client.ToString());
}
static void ClientDisconnected(object sender, DisconnectionEventArgs args)
{
Console.WriteLine(
"Client disconnected: "
+ args.Client.ToString()
+ ": "
+ args.Reason.ToString());
}
static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
Console.WriteLine(
"Message from "
+ args.Client.ToString()
+ ": "
+ Encoding.UTF8.GetString(args.Data));
}
static async Task<SyncResponse> SyncRequestReceived(SyncRequest req)
{
return new SyncResponse(req, "Hello back at you!");
}
using WatsonTcp;
static void Main(string[] args)
{
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.Events.ServerConnected += ServerConnected;
client.Events.ServerDisconnected += ServerDisconnected;
client.Events.MessageReceived += MessageReceived;
client.Callbacks.SyncRequestReceivedAsync = SyncRequestReceived;
client.Connect();
// check connectivity
Console.WriteLine("Am I connected? " + client.Connected);
// send a message
client.Send("Hello!");
// send a message with metadata
Dictionary<string, object> md = new Dictionary<string, object>();
md.Add("foo", "bar");
await client.SendAsync("Hello, client! Here's some metadata!", md);
// send and wait for a response
try
{
SyncResponse resp = await client.SendAndWaitAsync(
5000,
"Hey, say hello back within 5 seconds!");
Console.WriteLine("My friend says: " + Encoding.UTF8.GetString(resp.Data));
}
catch (TimeoutException)
{
Console.WriteLine("Too slow...");
}
}
static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
Console.WriteLine("Message from server: " + Encoding.UTF8.GetString(args.Data));
}
static void ServerConnected(object sender, ConnectionEventArgs args)
{
Console.WriteLine("Server connected");
}
static void ServerDisconnected(object sender, DisconnectionEventArgs args)
{
Console.WriteLine("Server disconnected");
}
static async Task<SyncResponse> SyncRequestReceived(SyncRequest req)
{
return new SyncResponse(req, "Hello back at you!");
}
The examples above can be modified to use SSL as follows. No other changes are needed. Ensure that the certificate is exported as a PFX file and is resident in the directory of execution.
// server
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000, "test.pfx", "password");
server.Settings.AcceptInvalidCertificates = true;
server.Settings.MutuallyAuthenticate = true;
server.Start();
// client
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000, "test.pfx", "password");
client.Settings.AcceptInvalidCertificates = true;
client.Settings.MutuallyAuthenticate = true;
client.Connect();
Refer to the Test.ClientStream and Test.ServerStream projects for a full example.
// server
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000);
server.Events.ClientConnected += ClientConnected;
server.Events.ClientDisconnected += ClientDisconnected;
server.Events.StreamReceived += StreamReceived;
server.Start();
static void StreamReceived(object sender, StreamReceivedEventArgs args)
{
long bytesRemaining = args.ContentLength;
int bytesRead = 0;
byte[] buffer = new byte[65536];
using (MemoryStream ms = new MemoryStream())
{
while (bytesRemaining > 0)
{
bytesRead = args.DataStream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
ms.Write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
}
}
}
Console.WriteLine(
"Stream received from "
+ args.Client.ToString()
+ ": "
+ Encoding.UTF8.GetString(ms.ToArray()));
}
// client
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.Events.ServerConnected += ServerConnected;
client.Events.ServerDisconnected += ServerDisconnected;
client.Events.StreamReceived += StreamReceived;
client.Connect();
static void StreamReceived(object sender, StreamReceivedEventArgs args)
{
long bytesRemaining = args.ContentLength;
int bytesRead = 0;
byte[] buffer = new byte[65536];
using (MemoryStream ms = new MemoryStream())
{
while (bytesRemaining > 0)
{
bytesRead = args.DataStream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
ms.Write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
}
}
}
Console.WriteLine("Stream received from server: " + Encoding.UTF8.GetString(ms.ToArray()));
}
If you wish to specify a client's GUID, you can modify WatsonTcpClient.Settings.Guid prior to calling WatsonTcpClient.Connect().
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.Events.ServerConnected += ServerConnected;
client.Events.ServerDisconnected += ServerDisconnected;
client.Events.StreamReceived += StreamReceived;
client.Settings.Guid = Guid.Parse("12345678-1234-1234-123456781234");
client.Connect();
The first step in troubleshooting is to implement a logging method and attach it to Settings.Logger, and as a general best practice while debugging, set Settings.DebugMessages to true.
client.Settings.DebugMessages = true;
client.Settings.Logger = MyLoggerMethod;
private void MyLoggerMethod(Severity sev, string msg)
{
Console.WriteLine(sev.ToString() + ": " + msg);
}
Additionally it is recommended that you implement the Events.ExceptionEncountered event.
client.Events.ExceptionEncountered += MyExceptionEvent;
private void MyExceptionEvent(object sender, ExceptionEventArgs args)
{
Console.WriteLine(args.Json);
}
The project TcpTest (https://github.com/jchristn/TcpTest) was built specifically to provide a reference for WatsonTcp to handle a variety of disconnection scenarios. The disconnection tests for which WatsonTcp is evaluated include:
| Test case | Description | Pass/Fail |
|---|---|---|
| Server-side dispose | Graceful termination of all client connections | PASS |
| Server-side client removal | Graceful termination of a single client | PASS |
| Server-side termination | Abrupt termination due to process abort or CTRL-C | PASS |
| Client-side dispose | Graceful termination of a client connection | PASS |
| Client-side termination | Abrupt termination due to a process abort or CTRL-C | PASS |
| Network interface down | Network interface disabled or cable removed | Partial (see below) |
Additionally, as of v4.3.0, support for TCP keepalives has been added to WatsonTcp, primarily to address the issue of a network interface being shut down, the cable unplugged, or the media otherwise becoming unavailable. It is important to note that keepalives are supported in .NET Core and .NET Framework, but NOT .NET Standard. As of this release, .NET Standard provides no facilities for TCP keepalives.
TCP keepalives are NOT enabled by default. To enable and configure:
server.Keepalive.EnableTcpKeepAlives = true;
server.Keepalive.TcpKeepAliveInterval = 5; // seconds to wait before sending subsequent keepalive
server.Keepalive.TcpKeepAliveTime = 5; // seconds to wait before sending a keepalive
server.Keepalive.TcpKeepAliveRetryCount = 5; // number of failed keepalive probes before terminating connection
Some important notes about TCP keepalives:
Keepalive.TcpKeepAliveRetryCount is only applicable to .NET Core; for .NET Framework, this value is forced to 10If you wish to have WatsonTcpServer automatically disconnect clients that have been idle for a period of time, set WatsonTcpServer.IdleClientTimeoutSeconds to a positive integer. Receiving a message from a client automatically resets their timeout. Client timeouts are evaluated every 5 seconds by Watson, so the disconnection may not be precise (for instance, if you use 7 seconds as your disconnect interval).
If you would like to financially support my efforts, first of all, thank you! Please refer to DONATIONS.md.
Please refer to CHANGELOG.md for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net461 net461 was computed. net462 net462 is compatible. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 is compatible. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
Showing the top 5 NuGet packages that depend on WatsonTcp:
| Package | Downloads |
|---|---|
|
CoreRemoting
Easy to use Remoting library for .NET Core and .NET Framework |
|
|
WatsonMesh
A simple C# mesh networking library using TCP (with or without SSL) with integrated framing for reliable transmission and receipt of data amongst multiple nodes. Does not support NAT. |
|
|
BigQ.Client
BigQ is a messaging platform using TCP sockets and websockets featuring sync, async, channel, and private communications. This package includes the BigQ client and associated libraries. |
|
|
BigQ.Server
BigQ is a messaging platform using TCP sockets and websockets featuring sync, async, channel, and private communications. This package includes the BigQ server and associated libraries. |
|
|
WatsonCluster
A simple C# class using Watson TCP to enable a one-to-one high availability cluster. Events are used to notify the encompassing application when the cluster is healthy (client and server connected), unhealthy (client or server disconnected), or a message is received. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.3.1 | 334 | 5/20/2026 |
| 6.3.0 | 254 | 5/5/2026 |
| 6.2.0 | 148 | 5/4/2026 |
| 6.1.0 | 1,279 | 3/23/2026 |
| 6.0.12 | 2,911 | 12/28/2025 |
| 6.0.11 | 3,377 | 10/25/2025 |
| 6.0.10 | 22,185 | 6/10/2025 |
| 6.0.9 | 9,009 | 2/3/2025 |
| 6.0.8 | 1,904 | 12/23/2024 |
| 6.0.6 | 18,851 | 5/1/2024 |
| 6.0.5 | 7,771 | 1/16/2024 |
| 6.0.4 | 935 | 1/10/2024 |
| 6.0.3 | 689 | 1/10/2024 |
| 6.0.2 | 562 | 1/9/2024 |
| 6.0.1 | 9,115 | 1/3/2024 |
| 6.0.0 | 924 | 12/18/2023 |
| 5.1.7 | 15,783 | 8/25/2023 |
| 5.1.6 | 1,257 | 8/13/2023 |
| 5.1.5 | 5,765 | 7/4/2023 |
| 5.1.3 | 2,112 | 6/14/2023 |
Performance-focused release with lower-allocation framing and stream handling, buffered header parsing, reduced disconnect hot-path overhead, internal async connection setup, and an automated performance benchmark suite.