Introduction
Author's historical notes...
Around 2005 I found a common business app requirement was to exchange or broadcast messages between threads and processes on the same machine, or between different computers in a local network. The messages only needed to be plain strings, which may seem primitive, but in practice you can send fragments of XML or JSON to compose a simple custom messaging protocol.
To satisfy the messaging requirement I originally created the LiteComms library which uses a Remoting channel and callback to implement a simple flow where the clients send a string request to the server and receive a string response in the callback. The library was used in some live apps.
Remoting support was unfortuntately dropped with the introduction of .NET Core. In response I created the LiteRpcComms library which used gRPC instead of Remoting. The library worked identically to LiteComms, but it had some weird quirks and dependencies that made it a bit clumsy and irritating to use. The library was never used in a live app and the repository was archived and the NuGet package hidden and deprecated in February 2023.
In late 2022 the need arrived again for a simple cross-network messaging system that could be used in both .NET Framework (later versions) and .NET 6 and later apps. I decided to abandon the previous LiteComms libraries and create a fresh one that used simple and familiar .NET classes and communication techniques that are built-in and cross-platform. The result is this repository of small classes that encapsulate a few different types of communication techniques. In summary, the library contains three pairs of server-client classes that implement the following communication techniques:
- Server broadcasting to clients via UDP.
- Duplex communication between threads or processes on the same computer via anonymous pipes.
- Duplex communication between different computers in the local network via TCP sockets.
The library basically contains light wrappers over well-known classes like UdpClient , TcpClient and anonymous Pipes . There are many web articles that discuss these classes in great detail, but most sample code is very simplistic and does not demonstrate how to manage the lifetime of connections, or how to handle errors and edge cases, or how use asynchronous processing in ways that would typically be required in business apps.
The library's classes attempt to wrap the low-level processing in the simplest and most convenient way possible for consumption by parent applications.
ℹ️ Sandcastle generated code documentation is available.
UDP Broadcasting
One of the simplest type of messaging requirements is to broadcast a message to anyone who is listening. The server broadcaster and the client receivers only share knowledge of a port number to use, but in all other respects they are completely decoupled.. The server broadcasts a message in a fire-and-forget manner and any running clients will receive the messages.
The classes UdpBroadcaster
and UdpListener
use UDP network multicast packets for one-way broadcasting. Unlike the other classes in the library which implement reliable two-way communication between one server and one client, the UDP classes are one-way. The UdpBroadcaster
class broadcasts a message on a specified port, then any number of UdpListener
classes can receive the broadcast messages on the same port.
UDP is connectionless and stateless, so it should not be used when reliability is important. It is however, lightweight and efficient and ideal for broadcasting non-critical events in the local network.
◪ UdpBroadcaster
int port = 44445; // Pick some random number from 32768 to 65535
var sender = new UdpBroadcaster(port);
:
await sender.BroadcastAsync("Update,Customer,4087");
:
sender.Dispose();
◪ UdpListener
var listener = new UdpListener(port);
listener.ListenCallback += (s, msg) =>
{
Trace($"RECEIVED -> {msg}");
};
:
listener.Dispose();
Test Project
The repository contains a console command project named DriverUdp
which test drives the pair of broadcasting classes. Start the command with either broadcast
or listen
as the first parameter to run it as the message broadcaster or listener respectively.
Cross-Process Communication
Another common business requirement is to exchange messages between different threads or processes running on the same computer. Anonymous pipes are ideal for this and they are implemented in standard classes in all .NET runtimes with cross-platform support.
The following sections discuss how to use the InProcessServer
and InProcessClient
classes to implement duplex communication.
📘 NOTE — In theory the server could communicate with multiple clients, but practical requirements have revealed that a one-to-one relationship is sufficient for typical usage scenarios.
◪ InProcessServer
The following skeleton code shows how a server is created, an event handler is added and the server is started. The StopAsync
method must be called when the server is no longer needed so that internal resources are released.
var server = new InProcessServer();
server.StateChanged += (s, e) =>
{
switch (e.Type)
{
case InProcessEventType.Received:
Log($"Server received -> {e.Message}");
break;
}
};
await server.StartAsync();
Log($"Server started TxHandle={server.TxHandle} RxHandle={server.RxHandle}");
// See the following notes for an explanation of what is happening here.
LaunchClient(server.Txhandler, server.RxHandler);
server.DisposeLocalHandles();
:
await server.SendAsync("Message from the server to the client");
:
await server.StopAsync();
:
After the server starts, it sets the string values of two properties RxHandle
and TxHandle
. These important values are passed into a new Thread or Process where the client will be launched. The handles define the contract between the server and client that allows them to communicate. In the sample code, an imaginary method named LaunchClient
takes the two handles and passes them down the call chain where they will eventually be passed into the InProcessClient.StartAsync
method.
💥 NOTE — Most reputable samples of using anonymous pipes make a call to DisposeLocalCopyOfClientHandle in the server immediately after the handles have been used to create a client. The library can't know when the app creates the client, so the server class method
DisposeLocalHandles
is provided so the app can request the dispose (as in the code sample above). Omitting the handle dispose doesn't seem to cause any ill effects, but it probably leaves some native handles open for the lifetime of the app's process.
◪ InProcessClient
The following skeleton code shows how a client is created, an event handler is added and the client is started. The StopAsync
method must be called when the client is no longer needed so that internal resources are released.
client = new InProcessClient();
client.StateChanged += async (s, e) =>
{
switch (e.Type)
{
case InProcessEventType.Received:
Console.WriteLine($"Receive <- {e.Message}");
await client.SendAsync($"Echo -> {e.Message}");
if (e.Message == "end")
{
// See notes below
await client.StopAsync();
}
break;
}
};
await client.StartAsync(txHandle, rxHandle);
The client's StartAsync
method requires the transmit and receive handles that were created when the server was started. The handles form the contract between the client and server that allow communication.
In this sample, a message from the server is simply echoed back to the server. The special string "end" is recognised as a request from the server that the client should stop itself. This sort of logic may be useful in coordinating the lifetimes of the server and client. Another useful technique may be for the client to send "ping" messages at intervals to tell the server it is alive.
Error Behaviour
If the client terminates abruptly, the server will not be notified and will continue to wait for client messages that will never arrive. Sending a message to the non-existent client will result in a "Pipe broken" exception, which could be used to detect an absent client.
If the server terminates abruptly, the client asynchronous read immediately fails, which is interpreted as the absence of a server and the client stops itself and raises a Stopped event.
Test Projects
The repository contains the projects DriverInProcessServer
and DriverInProcessClient
. The server project is a simple WPF app that starts and stops an in-process server, and it can launch the Console app client in a separate process to test inter-process communication. Both driver apps are a bit primitive at the moment and will be improved during hobby time.
Cross-Machine Communication
The final supported form of message exchange is between processes running on different computers in a local network. A TCP channel is ideal for this and it's implemented in standard classes in all .NET runtimes with cross-platform support. The OutProcessServer
and OutProcessClient
classes internally use the TCPListener and TcpClient respectively to implement duplex communication.
TCP server-client communication can be implemented in different ways. In C# in a Nutsell , Chapter 16 section Concurrency with TCP, the sample code shows how to Accept and process client requests on the thread pool for enhanced scalability and performance. That high-performance technique is not needed in this library, which is expecting to handle the exchange of modest numbers of messages on a persistent connection. The 'OutProcessServer
and OutProcessClient
classes keep a connection open until one of them terminates or they negotiate a clean shutdown.
📘 NOTE — In theory the TCP server could communicate with multiple clients, but practical requirements have revealed that a one-to-one relationship is sufficient for typical usage scenarios.
◪ OutProcessServer
The following skeleton code shows how a server is created, an event handler is added and the server is started.The StopAsync
method must be called when the server is no longer needed so that internal resources are released.
var server = new OutProcessServer();
server.StateChanged += (s, e) =>
{
switch (e.Type)
{
case OutProcessEventType.ServerAccepting:
Log("Accepting connection...");
break;
case OutProcessEventType.ServerConnected:
Log("Connected");
break;
case OutProcessEventType.ServerStopped:
Log("Stopped");
break;
case OutProcessEventType.Received:
Log($"Received -> {e.Message}");
break;
}
};
await server.StartAsync(34567); // Pick a port 32768-65535
:
await server.SendAsync("Message from the server to the client");
:
await server.StopAsync();
◪ OutProcessClient
The processing flow for client is nearly identical to the server. The client's ConnectAsync
method requires the NetBIOS name of the computer where the server is running and the port number the server is listening on. The name of the server computer may be "localhost". Note that specifying a string IP address as the NetBIOS name does not work.
var client = new OutProcessClient();
client.StateChanged += (s, e) =>
{
switch (e.Type)
{
case OutProcessEventType.ClientConnecting:
Log("Connecting...");
break;
case OutProcessEventType.ClientConnected:
Log("Connected");
break;
case OutProcessEventType.ClientStopped:
Log("Stopped");
break;
case OutProcessEventType.Received:
Log($"Received -> {e.Message}");
break;
}
};
await client.ConnectAsync("SERVER4", 34567);
:
await client.SendAsync("Message from the client to the server");
:
await client.StopAsync();
Error Behaviour
If the client or server terminates abruptly, the listener on the other end of the connection will immediately fault and the class will stop itself and raise the appropriate events. Parent applications can listen for lifetime events from both the server and client classes to produce appropriate behaviour.
Test Project
The repository contains the DriverOutProcess
project which is a simple WPF app that can act as the host of both the server and client. Run the app on any computers in a network to test server-client communication.
Notes
-
The OutProcess classes specifically allow communication across different computers in a local network, but they can be used on the same computer. In that case the client specifies "localhost" as the server machine name.
-
The InProcess classes are specifically designed for efficient inter-proccess communication on the same computer, but they are slightly tricky to use because of the need to pass the pair of handles into the client and then call the server's
DisposeLocalHandles
method to release the handle copies. The full usage pattern is a bit clumsy and hard to remember, so it may be easier to use the OutProcess classes, even though it could be considered overkill. -
The event handlers for all classes will run on unpredictable pooled threads, so parent apps with UIs may need to marshal event processing back to the UI thread. The IProgress related classes can be useful in that case.
-
The InProcess server and client
Start
methods simply start the background message processing Thread and return immediately, probably a short time before the background processing has started. In this case the host must listen for theServerStarted
orClientStarted
events to know when background processing has reached a started state. -
The InProcess server and client
StartAsync
methods start the background message processing Thread and wait for an internal signal that processing has reached a started state. There is probably no value in listening for the started events in this case.