Meta

Category: IPC

Introduction

TL; DR; A description of several techniques to pass messages between processes running on the same machine.

At times, there is the need for multiple processes running on the same machine to talk to each other. This is, for example, so that they can synchronize or share some kind of data. This is generally called Interprocess Communication, or IPC.

In this post, I am going to cover several ways by which .NET code can communicate with other .NET code on the same machine. The techniques I will talk about are:

You might have noticed that I am only going to talk about Windows/.NET native stuff, no additional libraries or services. Of course, keep in mind that the code I am going to show is just proof of concept, if it was to be used in real-life applications, it would need some improvements.

This is a long post, beware!

Contracts

So, we shall have an interface that describes the client side (IIpcClient) and another that describes the server side (IIpcServer). Their definitions are:

[ServiceContract]

publicinterface IIpcClient

{

[OperationContract(IsOneWay = true)]

void Send(string data);

}

publicinterface IIpcServer : IDisposable

{

void Start();

void Stop();

event EventHandler<DataReceivedEventArgs> Received;

}

[Serializable]

publicsealedclass DataReceivedEventArgs : EventArgs

{

public DataReceivedEventArgs(string data)

{

this.Data = data;

}

publicstring Data { get; private set; }

}

As you can see, these are very simple, just one-way communication from client to server. In case you are wondering, the [ServiceContract] and [OperationContract] attributes are really only useful for the WCF implementation, but I left them here because they really won’t cause any harm. More on this in a minute.

IIpcClient only allows to send a text message.

IIpcServer is slightly more complex, since one can start and stop the server, as well as receive events from it. It implements IDisposable because some implementations may need to free unmanaged resources.

WCF

So, the first implementation uses WCF and the NetNamedPipeBinding binding (transport). The reasons I chose this binding were:

It is binary;

It is fast;

Doesn’t need to open TCP sockets;

Is optimized for same machine (actually, the WCF implementation only works this way, even though the named pipes protocol can be used across machines).

Because my contract’s Send method is decorated with a OperationContractAttribute with the IsOneWay property set, the message is sent without the need to wait for a response message, making it slightly faster.

Again, nothing special, only worth noticing that, for simplicity’s sake, I only allow a single instance of the server (InstanceContextMode.Single).

Sockets

Windows does not support the AF_UNIX family of sockets, only TCP/IP, so, for demonstrating IP network communication I could have chosen either TCP or UDP, but I went for UDP because of the better performance and because of the relative simplicity that this example required.

Here is the client part:

publicclass SocketClient : IIpcClient

{

publicvoid Send(string data)

{

using (var client = new UdpClient())

{

client.Connect(string.Empty, 9000);

var bytes = Encoding.Default.GetBytes(data);

client.Send(bytes, bytes.Length);

}

}

}

It leverages the UdpClient class to send byte-array messages, because this class hides some of the complexity of Socket, making it easier to use. As a side note, it is even possible to use WCF with a UDP transport (binding).

The matching server class:

publicsealedclass SocketServer : IIpcServer

{

privatereadonly UdpClient server = new UdpClient(9000);

void IDisposable.Dispose()

{

this.Stop();

(this.server as IDisposable).Dispose();

}

publicvoid Start()

{

Task.Factory.StartNew(() =>

{

var ip = new IPEndPoint(IPAddress.Any, 0);

while (true)

{

var bytes = this.server.Receive(ref ip);

var data = Encoding.Default.GetString(bytes);

this.OnReceived(new DataReceivedEventArgs(data));

}

});

}

privatevoid OnReceived(DataReceivedEventArgs e)

{

var handler = this.Received;

if (handler != null)

{

handler(this, e);

}

}

publicvoid Stop()

{

this.server.Close();

}

publicevent EventHandler<DataReceivedEventArgs> Received;

}

Because the Receive method blocks until there is some contents to receive, we need to spawn a thread to avoid blocking.

.NET Remoting

.NET Remoting, in the old days, was .NET’s response to Java RMI, and basically was a remote references implementation, similar to CORBA. With Remoting, one can call methods on an object that resides in a different machine. .NET Remoting has long since been superseded by WCF, but it is still a viable alternative, particularly because WCF does not allow remote references.

Again, because we need to wait for someone to kill the server, we launch it in another thread. There’s more to it, because .NET Remoting is slightly more complex, but let’s leave it like it is.

Message Queues

Windows has included a message queues implementation for a long time, something that is often forgotten by developers. If you don’t have it installed – you can check if the Message Queuing service exists – you can install it through Programs and Features – Turn Windows features on and off on the Control Panel.

Once again, because the Receive method is blocking, we have to launch another thread and use a ManualResetEvent to terminate it. I am using a private queue without transactional support to make things simpler.

Named Pipes

Named pipes in Windows is a duplex means of sending data between Windows hosts. We used it in the WCF implementation, shown earlier, but .NET has its own built-in support for named pipes communication.

Again, we see the same pattern of spawning a thread pool thread to handle the requests, but this time we terminate it by calling Disconnect.

Memory-Mapped Files

Memory-mapped files in Windows allow us to either map a “window” of a large file on the filesystem, or to create a named memory area that can be shared among processes. In this sample, I am going to create a shared area on the fly and use named AutoResetEvents to control access to it.

Notice the way to create named AutoResetEvents is slightly tricky, because we need to create an instance of EventWaitHandle instead. Upon creation, the MemoryMappedFile instance must be passed a capacity in bytes, I am just passing 1024. Upon sending the message, I signal the shared AutoResetEvent which allows interested parties – the server – to proceed.

You can see it follows the same pattern as before, for blocking server implementations.

Event Tracing for Windows

The ETW implementation either requires that you use .NET 4.6 or that you install the Microsoft Event Source Library from NuGet. This is because of API differences in the EventSource and related classes. Of course, ETW is far more useful than just sending text messages between endpoints, but, hey, it can also do that, so here it goes.

Why would ETW for this, you ask? Well, for once, it has a very good performance, and you can use it for more complex scenarios.

Files

I hesitated before including this one, but anyway here it goes. Basically, the server and client classes will be trying to acquire the exclusive lock on a file. The server will check first if the file is not empty, otherwise, it will just loop. Unfortunately, there is no easy way to see if a file is locked, this is a common problem.

Same pattern of spawning a worker thread and using an event to kill it.

COM Interop

COM was introduced in Windows decades ago, and it largely depends on it. It is also the basis for automation and other interesting stuff. It is a standard for interoperability based on language-agnostic interface definitions. COM implementations can be written in a number of languages, from Visual Basic to C and C++, and, of course, C# and .NET.

COM has the concept of a class factory, which is used to build the actual COM interface implementations. We can use our own implementation to always return the same instance – a singleton. For this example, the first instantiation of the COM component will create an instance in memory and the next ones will always access it. Calls to its methods will be serialized and data transferred seamlessly between processes. Now, COM Interop is a complex topic, and I’m only going to scratch the surface of it. This one needs more work than the previous ones.

Without more delay, here is the common code to be shared between the client and the server:

Only worthy of mention is how we create instances to COM objects through the CreateInstance method of the Activator class, passing it a ProgId, which is basically a string moniker for the CLSID, which is the COM component unique identifier.

And the server part is only slightly more complex:

publicsealedclass ComServer : IIpcServer

{

privateuint cookie;

void IDisposable.Dispose()

{

this.Stop();

}

publicvoid Start()

{

this.cookie = IpcClientServer.Register(this);

}

publicvoid Stop()

{

IpcClientServer.Unregister(this.cookie);

}

internalvoid OnReceived(DataReceivedEventArgs e)

{

var handler = this.Received;

if (handler != null)

{

handler(this, e);

}

}

publicevent EventHandler<DataReceivedEventArgs> Received;

}

This one does not have to wait, because all it’s doing is registering the COM instance, its class factory, etc. The instance will be created when it is first instantiated by some code.

Now, two extra things that you need to do:

Compile the COM code as X86, not Any CPU:

Add the following post-build event command line upon successful build: C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe “$(TargetPath)”:

And that’s it. You can even test sending messages to it from a JavaScript file, using Windows Script Host:

var ipc = WScript.CreateObject('IpcTest.IpcClientServer');

ipc.Send('Hello, World!');

ipc = null;

Just save this code in a .JS file and call it using the CSCRIPT.EXE utility.

WM_COPYDATA

WM_COPYDATA probably doesn’t say much to .NET developers, but for old-school Win32 C/C++ developers it certainly does! Basically, it was a way by which one could send arbitrary data, including structured data, between processes (actually, strictly speaking, windows). One would send a WM_COPYDATA message to a window handle, running on any process, and Windows would take care of marshalling the data so that it is available outside the address space of the sending process. It is even possible to send it to all processes, using HWND_BROADCAST, but that probably wouldn’t be wise, because different applications might have different interpretations of it. Also, it needs to be passed with SendMessage, PostMessage won’t work.

Again, first we need to have some shared definitions:

[StructLayout(LayoutKind.Sequential)]

publicstruct COPYDATASTRUCT

{

public IntPtr dwData;

public IntPtr cbData;

public IntPtr lpData;

}

COPYDATASTRUCT is a Win32 data structure used for sending WM_COPYDATA messages and with this we are bringing it to the .NET world.

The extra complexity comes from the need to allocate and marshal data using the Win32 API. FindWindow is used to find the handle to the window we’re interested in from its title – see the server code to better understand this – but we could instead use HWND_BROADCAST to send the message to all windows on the system.

OK, so this is complex. We need to instantiate a NativeWindow, so that we can receive windows messages into it. Don’t worry, it isn’t visible. When we has for its handle to be created (CreateHandle), we say that it should have a certain title (the Caption property). The message pump (WndProc method) continuously listens for messages until it receives the termination message (WM_QUIT), which we send when the server is terminated. As usual, the server runs on its own thread, to avoid blocking.

Conclusion

The code demonstrated could be of course be improved so as to make it more robust and allow for more scenarios, like having different named endpoints instead of a single hardcoded one. It does work, however!

If you were to ask me, I think that WCF would be the way to go. It allows for structured messaging easily, and it is a breeze to switch bindings and settings. The COM implementation has its interest in the sense that we are sharing an instance between processes, so there is only a very slight delay that has to do with the marshaling of the data to send.

Now, I would really, really like to hear from you on this! Anything I missed? Looking forward for your feedback!