WatsonTcp is the fastest, most efficient way to build TCP-based clients and servers in C# with integrated framing, reliable transmission, fast disconnect detection, and easy to understand callbacks.
- Changed .NET Framework minimum requirement to 4.6.1 to support use of
TcpClient.Dispose
- Better disconnect handling and support (thank you to @mikkleini)
- Async Task-based callbacks
- Configurable connect timeout in WatsonTcpClient
- Clients can now connect via SSL without a certificate
- Big thanks to @MrMikeJJ for his extensive commits and pull requests
- Bugfix for graceful disconnect through dispose (thank you @mikkleini!)
Test projects for both client and server are included which will help you understand and exercise the class library.
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'.
.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
Special thanks to @brudo and @MrMikeJJ for their support of this project! If you'd like to contribute, please jump right into the source code and create a pull request.
The following examples show a simple client and server example using WatsonTcp without SSL.
IMPORTANT
- If you specify
127.0.0.1
as 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, specify a specific IP address, or, use
null
for the listener IP address. - If you use
null
for the IP address, or any variant representing any IP address such as0.0.0.0
,+
, or*
, you may have to run WatsonTcpServer with administrative privileges (this is required by the operating system).
using WatsonTcp;
static void Main(string[] args)
{
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000);
server.ClientConnected = ClientConnected;
server.ClientDisconnected = ClientDisconnected;
server.MessageReceived = MessageReceived;
server.Debug = false;
server.Start();
bool runForever = true;
while (runForever)
{
Console.Write("Command [q cls list send]: ");
string userInput = Console.ReadLine();
if (String.IsNullOrEmpty(userInput)) continue;
List<string> clients;
string ipPort;
switch (userInput)
{
case "q":
runForever = false;
break;
case "cls":
Console.Clear();
break;
case "list":
clients = server.ListClients();
if (clients != null && clients.Count > 0)
{
Console.WriteLine("Clients");
foreach (string curr in clients) Console.WriteLine(" " + curr);
}
else Console.WriteLine("None");
break;
case "send":
Console.Write("IP:Port: ");
ipPort = Console.ReadLine();
Console.Write("Data: ");
userInput = Console.ReadLine();
if (String.IsNullOrEmpty(userInput)) break;
server.Send(ipPort, Encoding.UTF8.GetBytes(userInput));
break;
}
}
}
static async Task ClientConnected(string ipPort)
{
Console.WriteLine("Client connected: " + ipPort);
}
static async Task ClientDisconnected(string ipPort)
{
Console.WriteLine("Client disconnected: " + ipPort);
}
static async Task MessageReceived(string ipPort, byte[] data)
{
string msg = "";
if (data != null && data.Length > 0) msg = Encoding.UTF8.GetString(data);
Console.WriteLine("Message received from " + ipPort + ": " + msg);
}
using WatsonTcp;
static void Main(string[] args)
{
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.ServerConnected = ServerConnected;
client.ServerDisconnected = ServerDisconnected;
client.MessageReceived = MessageReceived;
client.Debug = false;
client.Start();
bool runForever = true;
while (runForever)
{
Console.Write("Command [q cls send auth]: ");
string userInput = Console.ReadLine();
if (String.IsNullOrEmpty(userInput)) continue;
switch (userInput)
{
case "q":
runForever = false;
break;
case "cls":
Console.Clear();
break;
case "send":
Console.Write("Data: ");
userInput = Console.ReadLine();
if (String.IsNullOrEmpty(userInput)) break;
client.Send(Encoding.UTF8.GetBytes(userInput));
break;
case "auth":
Console.Write("Preshared key: ");
userInput = Console.ReadLine();
if (String.IsNullOrEmpty(userInput)) break;
client.Authenticate(userInput);
break;
}
}
}
static async Task MessageReceived(byte[] data)
{
Console.WriteLine("Message from server: " + Encoding.UTF8.GetString(data));
}
static async Task ServerConnected()
{
Console.WriteLine("Server connected");
}
static async Task ServerDisconnected()
{
Console.WriteLine("Server disconnected");
}
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 WatsonTcpSslServer("127.0.0.1", 9000, "test.pfx", "password");
server.ClientConnected = ClientConnected;
server.ClientDisconnected = ClientDisconnected;
server.MessageReceived = MessageReceived;
server.AcceptInvalidCertificates = true;
server.MutuallyAuthenticate = true;
server.Start();
// client
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000, "test.pfx", "password");
client.ServerConnected = ServerConnected;
client.ServerDisconnected = ServerDisconnected;
client.MessageReceived = MessageReceived;
client.AcceptInvalidCertificates = true;
client.MutuallyAuthenticate = true;
client.Start();
Refer to the TestClientStream
and TestServerStream
projects for a full example
// server
WatsonTcpServer server = new WatsonTcpSslServer("127.0.0.1", 9000);
server.ClientConnected = ClientConnected;
server.ClientDisconnected = ClientDisconnected;
server.StreamReceived = StreamReceived;
server.ReadDataStream = false;
server.Start();
static async Task StreamReceived(string ipPort, long contentLength, Stream stream)
{
// read contentLength bytes from the stream from client ipPort and process
return true;
}
// client
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.ServerConnected = ServerConnected;
client.ServerDisconnected = ServerDisconnected;
client.StreamReceived = StreamReceived;
client.ReadDataStream = false;
client.Start();
static async Task StreamReceived(long contentLength, Stream stream)
{
// read contentLength bytes from the stream and process
}
The project TcpTest (https://github.com/jchristn/TcpTest) was built specifically to provide a reference for WatsonTcp to handle a variety of disconnection scenarios. These 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 |
v1.3.x
- Numerous fixes to authentication using preshared keys
- Authentication callbacks in the client to handle authentication events
AuthenticationRequested
- authentication requested by the server, return the preshared key string (16 bytes)AuthenticationSucceeded
- authentication has succeeded, return trueAuthenticationFailure
- authentication has failed, return true
- Support for sending and receiving larger messages by using streams instead of byte arrays
- Refer to
TestServerStream
andTestClientStream
for a reference implementation. You must setclient.ReadDataStream = false
andserver.ReadDataStream = false
and use theStreamReceived
callback instead ofMessageReceived
v1.2.x
- Breaking changes for assigning callbacks, various server/client class variables, and starting them
- Consolidated SSL and non-SSL clients and servers into single classes for each
- Retargeted test projects to both .NET Core and .NET Framework
- Added more extensible framing support to later carry more metadata as needed
- Added authentication via pre-shared key (set Server.PresharedKey class variable, and use Client.Authenticate() method)
v1.1.x
- Re-targeted to both .NET Core 2.0 and .NET Framework 4.5.2
- Various bugfixes
v1.0.x
- Initial release
- Async support and IDisposable support
- IP filtering/permitted IP addresses support
- Improved disconnect detection
- SSL support