.NET Framework中如何通过IPC实现跨进程数据交换与事件触发
.NET Remoting IPC通道实现双向属性同步+事件通知方案
你现有代码已经跑通了基础的单向方法调用,要实现客户端修改属性、服务端/所有客户端同步收到变更通知,核心要解决三个问题:服务端持有单例对象引用、双向通道支持回调、跨进程事件的序列化与容错。
第一步:修改共享类库(RemotingTool)
首先给共享类加可序列化的事件参数,把公开字段改成属性,赋值时自动触发事件,同时加事件调用容错,避免单个客户端断开导致整个事件链崩溃。
using System; namespace RemotingTool { // 跨进程传递的事件参数必须标记可序列化 [Serializable] public class ValueChangedEventArgs : EventArgs { public int NewValue { get; set; } public DateTime UpdateTime { get; set; } } }
修改原有的Chat类:
using System; namespace RemotingTool { public class Chat : MarshalByRefObject { private int _value = 0; // 定义值变更事件,服务端、客户端都可以订阅 public event EventHandler<ValueChangedEventArgs> ValueChanged; public override object InitializeLifetimeService() { // 保持原有逻辑,返回null让远程对象永不过期,避免租约回收导致事件失效 return null; } // 把原有的公开字段改成属性,赋值时自动触发事件 public int Value { get => _value; set { _value = value; OnValueChanged(new ValueChangedEventArgs { NewValue = _value, UpdateTime = DateTime.Now }); } } public bool TransfertMessage(string message) { Console.WriteLine($"Message reçu:{message}"); return true; } public void IncrementValue() { // 走属性赋值,自动触发变更事件 Value++; } protected virtual void OnValueChanged(ValueChangedEventArgs e) { if (ValueChanged == null) return; // 逐个调用订阅者,单个订阅者报错不影响其他订阅者接收消息 foreach (var subscriber in ValueChanged.GetInvocationList()) { try { subscriber.DynamicInvoke(this, e); } catch (Exception ex) { Console.WriteLine($"通知订阅者失败,已移除失效订阅: {ex.Message}"); // 移除失效的订阅者,避免后续重复报错 ValueChanged -= (EventHandler<ValueChangedEventArgs>)subscriber; } } } } }
第二步:修改服务端代码
你之前用RegisterWellKnownServiceType注册的单例是延迟激活的(第一次客户端调用才创建实例),服务端本地拿不到对象引用,自然没法订阅事件、读取属性。这里改成手动创建实例后用RemotingServices.Marshal发布,保证服务端持有的和客户端访问的是同一个实例。
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Ipc; using RemotingTool; namespace RemotingServer { class Program { static void Main(string[] args) { try { // 注册IPC服务端通道,参数为IPC管道名(注意不是TCP端口,你之前写的localhost:9090本质是管道名) var channel = new IpcChannel("ChatServerPipe"); ChannelServices.RegisterChannel(channel, false); // 手动创建Chat单例实例 var serverChat = new Chat(); // 发布实例到IPC通道,客户端访问地址为ipc://ChatServerPipe/Chat RemotingServices.Marshal(serverChat, "Chat"); // 服务端本地订阅值变更事件 serverChat.ValueChanged += (sender, e) => { Console.WriteLine($"[服务端] Value更新为: {e.NewValue}, 更新时间: {e.UpdateTime}"); }; Console.WriteLine("# Serveur en ligne # \n"); Console.ReadLine(); // 保持服务端运行,不要注释这行 } catch (Exception ex) { Console.WriteLine($"# Erreur Serveur # : {ex.Message}\n"); Console.ReadLine(); } } } }
第三步:修改客户端代码
客户端需要注册带独立管道名的IPC通道,支持服务端反向回调触发事件,同时可以直接修改共享对象的Value属性。
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Ipc; using RemotingTool; namespace RemotingClient { class Program { static void Main(string[] args) { try { // 客户端注册通道时指定唯一管道名(多客户端同时运行时不能重名,加Guid后缀避免冲突) var clientPipeName = $"ChatClient_{Guid.NewGuid():N}"; var channel = new IpcChannel(clientPipeName); ChannelServices.RegisterChannel(channel, false); // 获取远程对象代理 var chat = (Chat)Activator.GetObject(typeof(Chat), "ipc://ChatServerPipe/Chat"); // 客户端订阅值变更事件,服务端/其他客户端修改Value时当前客户端都会收到通知 chat.ValueChanged += (sender, e) => { Console.WriteLine($"[客户端通知] 全局Value当前值: {e.NewValue}"); }; Console.WriteLine("# Connexion au serveur réussie # \n 输入消息发送,输入set:数字修改Value,空行退出"); bool exit = false; while(!exit) { var input = Console.ReadLine(); if(!string.IsNullOrEmpty(input)) { try { if(chat == null) continue; // 支持直接修改共享属性 if (input.StartsWith("set:")) { if (int.TryParse(input.Substring(4), out int newVal)) { chat.Value = newVal; Console.WriteLine($"已提交Value修改请求,新值: {newVal}"); } continue; } var result = chat.TransfertMessage(input); if(result) { Console.WriteLine("Le message à été transmis au serveur"); } else { throw new Exception("消息发送失败"); } chat.IncrementValue(); } catch { Console.WriteLine("Erreur communication serveur"); } } else { exit = true; } } } catch (Exception ex) { Console.WriteLine($"Erreur de connexion au serveur: {ex.Message}"); } } } }
关键注意点
- IPC通道地址格式为
ipc://[管道名]/[对象URI],和TCP的ip:port格式无关,你之前写的localhost:9090只是管道名字符串,只要服务端和客户端保持一致就能正常通信,只是命名容易产生误解。 - 所有跨进程传递的自定义类型(比如事件参数)必须标记
[Serializable]特性,否则会抛出序列化异常。 - 不要用无参构造创建客户端IPC通道,无参构造生成的随机管道名在部分系统版本下会出现回调失败的问题,手动指定唯一管道名稳定性更高。
- 事件触发必须加异常捕获和失效订阅清理逻辑,否则客户端异常退出后未取消订阅,会导致后续事件触发持续报错,甚至拖垮服务端。
- 如果需要多个客户端同时收到属性变更通知,当前代码已经支持,所有订阅了ValueChanged事件的客户端都会收到广播。
内容的提问来源于stack exchange,提问作者AdrienCMS
相关产品推荐
相关产品推荐

