[C#] IpcChannel双向通信,参考MAF的AddInProcess开发插件,服务断开重新打开及服务生存周期管理
先前项目太忙了,没时间写博客,发现了一个有趣的东西,匆匆忙忙就写完了,先描述一下需求背景:客户端有几张百万级别的表需要联合统计(如果是最大权限的账号),改变查询条件又要重新统计,因此常常sql执行还没结束就取消了,但不管关闭数据库还是结束线程都必须等到sql执行结束,无奈之下只能考虑进程通信,取消就直接杀掉进程,首先考虑的是现成的MAF,于是有了下面的代码:
仅仅一个接口就需要建这么多个项目,DLL多点也就算了,相关文件夹还必须如上图这么放,这么放就算了,但是公共的DLL(如System.Data.SQLite.dll)还不能一起用,进程隔离的话公共DLL必须插件、主程序各自一份,这不是打包的时候莫名增加了体积吗?这肯定不能接受啊,但我还没有放弃,因为ProcessStartInfo可以指定工作目录,既我只要指定主程序根目录为工作目录,还是可以实现DLL共享,但是绝望的是AddInProcess是密封的,无法控制它是如何启动的。虽然不满足期望,好处是在看它的源码时发现是基于IpcChannel通信的,去官网了解之后,意外地发现使用很简单,再结合MAF里AddInProcess的思想,我的实现如下:
服务端提供什么服务应有客服端指定,既需要一个空白进程,负责加载服务所在程序集,并提供服务,经过一段时间磨砺,发现不需要创建客户端,下面的代码就可以实现:
// 客服端启动代理类 public sealed class IpcProcessProxy<T> { public T Proxy => proxy; public event EventHandler Exited; private Process process; private T proxy; public bool Start(int startUpTimeout = 3000) { var type = typeof(T); // 锲约类型 var ipcServerDic = new Dictionary<string, string> { ["name"] = "ipcServer", // 服务端名称 ["portName"] = Guid.NewGuid().ToString("N") // 服务端地址 }; var contractDic = new Dictionary<string, string> { ["assemblyName"] = type.Assembly.GetName().Name, // 程序集 ["typeName"] = typeName, // 类型名称 ["objectUri"] = objectUri // Ipc提供的服务名称 }; var arg1 = String.Join("&", ipcServerDic.Select(q => $"{q.Key}={q.Value}")); var arg2 = String.Join("&", contractDic.Select(q => $"{q.Key}={q.Value}")); var startInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, FileName = "IpcProcess.exe", Arguments = $"{arg1} {arg2}", WorkingDirectory = Environment.CurrentDirectory }; process = Process.Start(startInfo); // 启动进程,并根据参数启动服务 process.Exited += Exited; // 等待服务端服务启动 using (var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + ipcServerDic["portName"])) { if (readyEvent.WaitOne(startUpTimeout, false)) { var url = $"ipc://{ipcServerDic["portName"]}/{contractDic["objectUri"]}"; proxy = (T)Activator.GetObject(typeof(T), url); // 通过约定url,获取服务端服务代理对象 return proxy != null; } } } } // 服务端 static void Main(string[] args) { var arg1 = args[0]; var arg2 = args[1]; var ipcServerDic = new Dictionary<string, string>(); var contractDic = new Dictionary<string, string>() foreach (var kvStr in arg1.Split('&')) { var kv = kvStr.Split('='); ipcServerDic.Add(kv[0], kv[1]); } foreach (var kvStr in arg2.Split('&')) { var kv = kvStr.Split('='); contractDic.Add(kv[0], kv[1]); } var assembly = Assembly.Load(contractDic["assemblyName"]); // 加载程序集 var type = assembly.GetType(contractDic["typeName"], true, false) // 注册Ipc服务 var channel = new IpcServerChannel(ipcServerDic, new BinaryServerFormatterSinkProvider()); ChannelServices.RegisterChannel(channel, false) // 创建服务对象,并创建代理 var serverObj = (MarshalByRefObject)Activator.CreateInstance(type); RemotingServices.Marshal(serverObj, contractDic["objectUri"]); // 通知客服端,服务已经启动了 var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + ipcServerDic["portName"]); readyEvent.Set(); Console.ReadLine(); }
这里和官方最大的区别是没有在服务端使用RegisterWellKnownServiceType,也没有创建客服端,更没有在客服端使用WellKnownClientTypeEntry,而是在服务端使用RemotingServices.Marshal开启一个服务代理,在客服端使用Activator.GetObject获取服务代理对象,这样做的好处是:在服务端,先拿到对象(如果继承特定类型,可进行特殊处理,后续双向通信会用到),在客服端,不需要通过new()获取服务,而且说不定其他地方new()这个类型并不想用代理服务,再就是Activator.GetObject不需要直接引用类型,可以是继承的接口,这样使代码更低耦合。
以上就是简单的IpcChannel单向通信,适用于调用服务并返回结果,但不适用于大多数插件场景,比如分批返回多个结果,或者某个方法传入回调函数通知进度;一个功能复杂的插件往往需要双向通信,我就有这方面的需求,因此开始不断地探索...
尝试一:给远程对象添加一个事件Event,这样客服端监听这个事件就可以得到通知,但发现只要某个对象监听了这个事件,那么它也会被视为服务端对象,最后发现客服端永远也不可能拿到这个事件。
尝试二:在服务端注册一个IpcServerChannel,在客服端注册一个IpcClientChannel,然后研究他们之间是怎么通信的,突破口应该就在他们的构造函数的第二个参数,分别实现了发送消息和处理请求的方法,但是已经超出我的理解范围了。
尝试三:在服务端和客服端分别注册一个IpcServerChannel,当服务端想要回调给客户端时,就向客服端发送消息,这就必须要求服务端的代理服务对象能拿到客服端的代理服务对象,这样才能调用客服端代理服务对象方法,思路是这样的:
1、首选在客服端注册Ipc服务,并提供代理服务
2、启动服务端进程,并启动自身的服务(继承特定类型,添加一个事件,当需要回调时触发),根据约定,找到客服端提供的代理服务,监听自身服务回调事件,事件触发时调用客服端代理服务方法
3、客服端代理服务收到请求时,再触发类似事件,提供给外部调用者使用
下面是启动代理服务的核心代码:
public class IpcProcessProxy : ISponsor { static IpcProcessProxy() { // 客服端服务地址,将此作为参数传递给服务进程 ipcClientDic = new Dictionary<string, string> { ["name"] = "ipcClient", ["portName"] = Guid.NewGuid().ToString("N") }; // 客服端的代理对象 callRemoteObj = new CallRemoteObject(ipcClientDic["portName"]); callRemoteObj.Called += OnCalled; // 当收到回调时,触发此事件 } [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)] protected void Start() { try { lock (locker) { // 创建Ipc服务 if (channel == null) channel = new IpcServerChannel(ipcClientDic, new BinaryServerFormatterSinkProvider()); // 注册服务 if (ChannelServices.GetChannel(channel.ChannelName) == null) ChannelServices.RegisterChannel(channel, false); // 启动代理服务 objRef = RemotingServices.Marshal(callRemoteObj, "CallRemoteObject"); // 管理服务的生存周期 if (lease == null) { lease = (ILease)RemotingServices.GetLifetimeService(callRemoteObj); lease.Register(this); } } } catch (Exception ex) { Console.WriteLine(ex); Console.WriteLine("启动Ipc通信服务失败!"); } } public TimeSpan Renewal(ILease lease) { Console.WriteLine("租约到期,生存状态:" + lease.CurrentState); return TimeSpan.FromSeconds(30); } /// <summary> /// 注册要监听的回调方法 /// </summary> /// <param name="url">服务端Ipc地址</param> /// <param name="methodName">回调方法名称</param> /// <param name="callback">监听回调方法的委托</param> protected void RegisterCallback(string url, string methodName, Delegate callback) { if (callback == null) return; lock (locker) { var callbacks = urls.GetOrAdd(url, new Dictionary<string, Delegate>()); callbacks.Set(methodName, callback); } } protected void UnRegisterCallback(string url) { lock (locker) { urls.Remove(url); } } /// <summary> /// 收到服务端的回调 /// </summary> /// <param name="url">服务端Ipc地址</param> /// <param name="methodName">回调方法名称</param> /// <param name="parameters">回调方法参数</param> private static void OnCalled(string url, string methodName, object[] parameters) { lock (locker) { // 找到监听该地址且指定方法的委托 if (!urls.TryGetValue(url, out Dictionary<string, Delegate> callbacks) || !callbacks.TryGetValue(methodName, out Delegate callback) || callback == null) return; // 执行委托 callback.DynamicInvoke(parameters); } } protected void Close() { if (lease != null) { lease.Unregister(this); lease = null; } if (objRef != null) { RemotingServices.Unmarshal(objRef); objRef = null; } if (callRemoteObj != null) { RemotingServices.Disconnect(callRemoteObj); callRemoteObj = null; } if (channel != null) { ChannelServices.UnregisterChannel(channel); channel = null; } } private static IpcServerChannel channel; private static CallRemoteObject callRemoteObj; private static ObjRef objRef; private static Dictionary<string, Dictionary<string, Delegate>> urls = new Dictionary<string, Dictionary<string, Delegate>>(); protected static Dictionary<string, string> ipcClientDic; private static object locker = new object(); private static ILease lease; }
项目资源我上传到了CSDN(IpcChannel双向通信,参考MAF的AddInProcess开发插件,服务断开重新打开及服务生存周期管理),本人博客小白一个,有时需要下载CSDN的资源却没有积分,所以想到这个方法,理解万岁!
使用这套IpcChannel双向通信也有一个星期,遇到了几个问题,比如异常类型:System.Runtime.Remoting.RemotingException,异常信息:对象“/CallRemoteObject”已经断开连接或不在服务器上。对于此问题可能是我总是强制结束进程,在传输过程中断开导致的服务挂掉,但测试发现,无论在发送时结束进程,还是在接收时,都无法复现这个异常,希望知道的留言告诉我怎么必然复现,因此只能把希望寄托于如何重新打开服务,经过尝试发现只要重新调用RemotingServices.Marshal就行,记住代理的服务对象还是同一个,如果使用新的对象,就会提示:System.Runtime.Remoting.RemotingException: 找到与同一 URI“/49cb660d_9357_4a1a_9732_4488fbad07eb/CallRemoteObject”关联 的两个不同对象。
为了更稳妥,因为还是无法找到服务关闭的原因,使用了RemotingServices.GetLifetimeService管理服务的生存周期,应该是生效了,后面几天都没有看到类似断开连接或不在服务器上的异常。