.NET 进程间通信






4.89/5 (54投票s)
一个易于使用、零配置的解决方案,用于 .NET 应用程序边界间的通信。一个基于底层 Windows 消息的简单库,可作为 .NET Remoting 的替代方案。
 
 
引言
本文演示了一个通过利用 Windows 本地消息传递,在 .NET 中实现跨 AppDomain 通信的快速易用的方案。XDMessaging 库基于为最近一个 Vista 项目开发的旨在帮助快速开发的代码,该项目需要在受限环境中进行大量的跨 AppDomain 通信。事实证明,它在许多 .NET Remoting 不切实际甚至不可能的场景中极其有用,并且由于其简单性,它实际上解决了比我预想的更多问题。该库旨在在同一台机器上的多个应用程序之间发送消息。例如,一个任务托盘应用程序可能希望与一个独立的桌面应用程序通信或监控它。该库不实现跨网络的跨域通信,在这种情况下 .NET Remoting 足以胜任。
更新:XDMessaging 2.0 现已在此处提供,并引入了对 Windows 服务和控制台应用程序的支持。
背景
那么,为什么不使用 .NET Remoting 呢?嗯,过去我个人发现它设置和配置起来极其繁琐。另一个问题是当出现问题时(通常是权限问题)缺乏有用的错误报告。别误解我的意思,我并不反对 .NET Remoting。它比我自己的实现有更多的功能,当然也不限于单机通信。然而,对于单机通信,它不需要那么复杂。那么,为什么不利用 Windows 消息呢?毕竟,这是非托管应用程序正是为此目的而使用的机制。嗯,这倒是个主意……
如果你从未听说过,Windows 消息是 Windows 操作系统用于广播用户输入、系统更改和系统上运行的应用程序可以响应的其他事件的低级通信。例如,应用程序重绘是由 WM_PAINT 消息触发的。除了系统消息,非托管应用程序还可以定义自定义 Windows 消息并使用它们与其他窗口通信。这些通常以 WM_USER 消息的形式出现。如果你安装了 Spy++(Visual Studio 工具),你可以实时监控窗口接收到的所有消息。
XDMessaging 库
XDMessaging 库为同机跨 AppDomain 通信提供了一个易于使用、零配置的解决方案。它提供了一个简单的 API,用于在应用程序边界之间发送和接收目标 string 消息。该库允许使用用户定义的伪“通道”来发送和接收消息。任何应用程序都可以向任何通道发送消息,但它必须注册为该通道的监听器才能接收。通过这种方式,开发人员可以快速且以编程方式设计其应用程序如何最好地相互通信并和谐工作。
示例:发送消息
// Send shutdown message a channel named commands
XDBroadcast.SendToChannel("commands", "shutdown");
示例:监听通道
// Create our listener instance
XDListener listener = new XDListener();
// Register channels to listen on
listener.RegisterChannel("events");
listener.RegisterChannel("status");
listener.RegisterChannel("commands");
// Stop listening on a specific channel
listener.UnRegisterChannel("status");
示例:处理消息
// Attach an event handler to our instance
listener.MessageReceived+=XDMessageHandler(this.listener_MessageReceived);
// process the message
private void listener_MessageReceived(object sender, XDMessageEventArgs e)
{
    // e.DataGram.Message is the message
    // e.DataGram.Channel is the channel name
    switch(e.DataGram.Message)
    {
        case "shutdown":
            this.Close();
            break;
    }
}
信使演示
要查看演示,您需要启动多个 Messenger.exe 应用程序实例。演示应用程序除了演示 XDMessaging 库的使用外,没有其他实际目的。它展示了如何在应用程序边界之间将消息传递给桌面应用程序的多个实例。该应用程序使用两个任意通道,名为 Status 和 UserMessage。窗口事件(如 onClosing 和 onLoad)作为消息在 Status 通道(以绿色显示)上广播,用户消息在 UserMessage 通道(以蓝色显示)上广播。通过勾选或取消勾选选项,您可以切换窗口将监听哪些通道消息。
工作原理
该库利用了类型为 WM_COPYDATA 的 Windows 系统消息。此系统消息允许通过携带指向我们希望复制的数据(在本例中为 string)的指针,在多个应用程序之间传递数据。这通过 PInvoke 使用 SendMessage Win32 API 发送到其他窗口。
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
    public IntPtr dwData;
    public int cbData;
    public IntPtr lpData;
}
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SendMessage(IntPtr hwnd, int wMsg, 
    int wParam, ref COPYDATASTRUCT lParam);
COPYDATASTRUCT 结构包含有关我们想要传输到另一个应用程序的消息数据的信息。这由成员 lpData 和 dwData 引用。lpData 是指向存储在内存中的 string 数据的指针。dwData 是传输数据的大小。cdData 成员在我们的例子中没有使用。为了传递数据,我们必须首先将消息 string 分配到内存中的一个地址并获取指向该数据的指针。为此,我们使用 Marshal API,如下所示
// Serialize our raw string data into a binary stream
BinaryFormatter b = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
b.Serialize(stream, raw);
stream.Flush();
int dataSize = (int)stream.Length;
// Create byte array and transfer the stream data
byte[] bytes = new byte[dataSize];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(bytes, 0, dataSize);
stream.Close();
// Allocate a memory address for our byte array
IntPtr ptrData = Marshal.AllocCoTaskMem(dataSize);
// Copy the byte data into this memory address
Marshal.Copy(bytes, 0, ptrData, dataSize);
现在 string 数据已在内存中,由上面代码中的 ptrData 指针引用,我们可以创建我们的 COPYDATASTRUCT 实例并相应地填充 lpData 和 dwData 成员。所以,现在我们已经将消息封装成一个 COPYDATASTRUCT 对象,我们准备使用 SendMessage API 将其发送到另一个窗口。然而,要做到这一点,我们首先需要知道哪些应用程序应该接收数据,即哪些正在监听正确的通道。此外,如果应用程序没有窗口句柄来发送我们的消息怎么办?
为了克服这个问题,我们使用了一些本地窗口属性和我们的 XDListener 类。当一个类的实例被调用时,它会在桌面上创建一个隐藏窗口,作为所有 Windows 消息的监听器。这是通过扩展 System.Windows.Forms 的 NativeWindow 类来完成的。通过重写 WndProc 方法,这使我们能够过滤 Windows 消息并查找包含消息数据的 WM_COPYDATA 消息。
XDListener 类还利用窗口属性来创建属性标志,指示实例正在监听哪些通道,因此它应该接收哪些消息。当消息广播时,它使用 EnumChildWindows Win32 API 枚举所有桌面窗口。它会在窗口上查找代表通道名称的标志(属性名)。如果找到,则将 Windows WM_COPYDATA 消息发送到该窗口。一旦到达那里,它将被拥有隐藏窗口的 XDListener 实例捕获和处理。要读取消息数据,我们使用本机 Windows 消息的 lParam 来展开 COPYDATASTRUCT 实例。从中,我们可以找到并恢复先前存储在内存中的原始 string 消息。
// NativeWindow override to filter our WM_COPYDATA packet
protected override void WndProc(ref Message msg)
{
    // We must process all the system messages and propagate them
    base.WndProc(ref msg);
    
    // If our message
    if (msg.Msg == Win32.WM_COPYDATA)
    {
        // msg.LParam contains a pointer to the COPYDATASTRUCT struct
        Win32.COPYDATASTRUCT dataStruct = 
            (Win32.COPYDATASTRUCT)Marshal.PtrToStructure(
            msg.LParam , typeof(Win32.COPYDATASTRUCT));
        
        // Create a byte array to hold the data
        byte[] bytes = new byte[this.dataStruct.cbData];
        
        // Make a copy of the original data referenced by 
        // the COPYDATASTRUCT struct
        Marshal.Copy(this.dataStruct.lpData, bytes, 0, 
            this.dataStruct.cbData);
        // Deserialize the data back into a string
        MemoryStream stream = new MemoryStream(bytes);
        BinaryFormatter b = new BinaryFormatter();
        
        // This is the message sent from the other application
        string rawmessage = (string)b.Deserialize(stream);
        
        // do something with our message
    }
}
请注意,由于消息在广播到其他应用程序时存储在内存中,因此我们必须记住在发送消息后释放该内存。每个接收数据的窗口都会制作自己的副本,因此一旦消息发送完毕,原始数据就可以安全销毁。这是必要的,因为数据存储在非托管内存中,否则会导致内存泄漏。
// Free the memory referenced by the given pointer
Marshal.FreeCoTaskMem(lpData);
以下是用于设置和删除窗口属性以及枚举桌面窗口的其他 PInvoke 方法。
// Delegate used during window enumeration
public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);
// The Win32 API used to enumerate children of the desktop window 
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumChildWindows(IntPtr hwndParent, 
    EnumWindowsProc lpEnumFunc, IntPtr lParam);
// The API used to look for a named property on a window
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int GetProp(IntPtr hwnd, string lpString);
// The API used to set a named property on a window, and 
// hence register a messaging channel
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SetProp(IntPtr hwnd, string lpString, int hData);
// The API used to remove a property from a window, and hence unregister a 
// messaging channel
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int RemoveProp(IntPtr hwnd, string lpString);
延伸阅读
历史
- 2007 年 2 月:初始发布
- 2007 年 2 月 25 日:演示中添加了 VB 移植
- 2007 年 5 月 29 日:文章编辑并发布到 CodeProject.com 主文章库
- 2008 年 6 月 2 日:改进了线程支持以避免应用程序挂起


