WatsonTcp
WatsonTcp is the easiest way to build TCP-based clients and servers in C#.
Install / Use
/learn @dotnet/WatsonTcpREADME
WatsonTcp
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.
- If you want a library that doesn't use framing, but has a similar implementation, use SuperSimpleTcp
- If you want a library that doesn't use framing and provides explicit control over how much data to read, use CavemanTcp
.NET Foundation
This project is part of the .NET Foundation along with other projects like the .NET Runtime.
Contributions
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.
New in v6.1.0
Performance
- Rewrote message header parsing to eliminate O(n^2) array allocations and per-byte LINQ overhead; now uses a
MemoryStreamaccumulator with direct byte comparison - Send operations now use
ArrayPool<byte>pooling instead of allocating new buffers on every iteration
Thread Safety
- Consolidated
ClientMetadataManagerfrom 5 independentReaderWriterLockSliminstances to a single lock, eliminating race conditions during multi-dictionary operations (ReplaceGuid,Remove) - Fixed TOCTOU race in
GetClient()(ContainsKeythen indexer across separate lock acquisitions); now usesTryGetValue - Replaced
AutoResetEvent+ event-based sync response matching withConcurrentDictionary<Guid, TaskCompletionSource<SyncResponse>>in both client and server, eliminating handler registration race conditions and signal loss
Bug Fixes
- Fixed
WaitHandleresource leak inWatsonTcpClient.Connect()(was commented out, now properly closed) - Replaced busy-wait spin loops in
ClientMetadata.Dispose()andWatsonTcpClient.Disconnect()withTask.Wait(timeout) - Stale kicked/timed-out client records now automatically purged every 60 seconds (previously accumulated forever)
New Features
Settings.MaxHeaderSize(client and server, default 256KB) guards against memory exhaustion from oversized or malicious headersSettings.EnforceMaxConnections(server, defaulttrue) actively rejects connections at capacity; set tofalsefor legacy behavior
Observability
- Added debug-level logging to all previously silent
TaskCanceledExceptionandOperationCanceledExceptioncatch blocks
Testing
- 10 new automated tests (46 total) covering MaxConnections enforcement, MaxHeaderSize validation, rapid connect/disconnect, concurrent sync requests, SSL, server stop detection, duplicate GUIDs, and send-with-offset
Breaking Changes
Settings.EnforceMaxConnectionsdefaults totrue. If you relied on accepting connections beyondMaxConnections, setEnforceMaxConnections = false.- All other changes are internal with identical public API and wire protocol.
Previous in v6.0.x
- Remove unsupported frameworks
- Async version of
SyncMessageReceivedcallback - Moving usings inside namespace
- Remove obsolete methods
- Mark non-async APIs obsolete
- Modified test projects to use async
- Ensured background tasks honored cancellation tokens
- Ability to specify a client's GUID before attempting to connect
Architecture
Refer to ARCHITECTURE.md 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 FRAMING.md.
Test Applications
Test projects for both client and server are included which will help you understand and exercise the class library. The Test.XUnit project provides dotnet test-compatible xUnit tests suitable for CI/CD pipelines.
SSL
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'.
To Stream or Not To Stream...
WatsonTcp allows you to receive messages using either byte arrays or streams. Set Events.MessageReceived if you wish to consume a byte array, or, set Events.StreamReceived if you wish to consume a stream.
It is important to note the following:
- When using
Events.MessageReceived- The message payload is read from the stream and sent to your application
- The event is fired asynchronously and Watson can continue reading messages while your application processes
- When using
Events.StreamReceived- If the message payload is smaller than
Settings.MaxProxiedStreamSize, the data is read into aMemoryStreamand sent to your application asynchronously - If the message payload is larger than
Settings.MaxProxiedStreamSize, the underlying data stream is sent to your application synchronously, and WatsonTcp will wait until your application responds before continuing to read
- If the message payload is smaller than
- Only one of
Events.MessageReceivedandEvents.StreamReceivedshould be set;Events.MessageReceivedwill be used if both are set
Including Metadata with a Message
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).
Local vs External Connections
IMPORTANT
- If you specify
127.0.0.1as the listener IP address in WatsonTcpServer, it will only be able to accept connections from within the local host. - To accept connections from other machines:
- Use a specific interface IP address, or
- Use
null,*,+, or0.0.0.0for the listener IP address (requires admin privileges to listen on any IP address)
- Make sure you create a permit rule on your firewall to allow inbound connections on that port
- If you use a port number under 1024, admin privileges will be required
Running under Mono
.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
Examples
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.
Server
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 m
