[C#] 基于IpcChannel双向通信开发插件
需求:将主程序的一些工作抛给子进程处理,代码尽量小得改动,不需要新建项目,主程序的大部分代码与子进程通用
解决方案:基于上一章的基于IpcChannel进程通信开发插件,移除MAF约束,只要实现双向通信即可,首先要明白IpcChannel通信其实跟http请求很类似,调方法相当于发送请求,所以给远程对象MarshalByRefObject添加事件是没用的(无法监听远程对象的事件),正确的做法是在主程序和子进程各建一个IpcServerChannel,相互给对方发请求(IpcServerChannel每个进程只能注册一个,因此这里使用单例)
远程对象:
public class CallRemoteObject : MarshalByRefObject { public CallRemoteObject(string url) { this.url = url; } public void Call(string method, params object[] objs) { CallFrom(url, method, objs); } public void CallFrom(string url, string method, params object[] objs) { Called?.Invoke(url, method, objs); } public delegate void CallEventHandler(string url, string method, params object[] objs); public event CallEventHandler Called; private string url; } public class RemoteObject : CallRemoteObject, IRemoteObject { public RemoteObject(string url) : base(url) { } public int Add(int a, int b) => a + b; public void TestCall() { Call("OnCallback", "123456"); } }
服务端:
public class IpcProcessProxy { protected void Start() { lock (locker) { if (ChannelServices.GetChannel("ipcClient") != null) return; ipcClientDic = new Dictionary<string, string> { ["name"] = "ipcClient", ["portName"] = Guid.NewGuid().ToString("N") }; // Create and register an IPC channel channel = new IpcServerChannel(ipcClientDic, new BinaryServerFormatterSinkProvider()); ChannelServices.RegisterChannel(channel, false); // Expose an object callRemoteObj = new CallRemoteObject(ipcClientDic["portName"]); callRemoteObj.Called += OnCalled; objRef = RemotingServices.Marshal(callRemoteObj, "CallRemoteObject"); } } 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); } } private void OnCalled(string url, string methodName, object[] parameters) { if (!urls.TryGetValue(url, out Dictionary<string, Delegate> callbacks) || !callbacks.TryGetValue(methodName, out Delegate callback) || callback == null) return; callback.DynamicInvoke(parameters); } 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(); } [SecurityCritical] [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] public sealed class IpcProcessProxy: IpcProcessProxy, IDisposable { [SecurityCritical] public bool Start(int startUpTimeout = 3000) { var type = typeof(T); if (type.IsInterface) { if (IpcProxyDictionary.TryGetValue(type, out IReadOnlyDictionary<string, string> dic)) return Start(dic["assemblyName"], dic["typeName"], dic["objectUri"], Boolean.Parse(dic["callRemote"]), startUpTimeout); return false; } return Start(type.Assembly.GetName().Name, type.FullName, type.Name, typeof(CallRemoteObject).IsAssignableFrom(type), startUpTimeout); } [SecurityCritical] public bool Start (int startUpTimeout = 3000) where TProxy : class, T { var type = typeof(TProxy); return Start(type.Assembly.GetName().Name, type.FullName, type.Name, typeof(CallRemoteObject).IsAssignableFrom(type), startUpTimeout); } [SecurityCritical] private bool Start(string assemblyName, string typeName, string objectUri, bool callRemote, int startUpTimeout) { ipcServerDic = new Dictionary<string, string> { ["name"] = "ipcServer", ["portName"] = Guid.NewGuid().ToString("N") }; var contractDic = new Dictionary<string, string> { ["assemblyName"] = assemblyName, ["typeName"] = typeName, ["objectUri"] = objectUri }; if (callRemote) base.Start(); var arg1 = String.Join("&", ipcClientDic.Select(q => $"{q.Key}={q.Value}")); var arg2 = String.Join("&", ipcServerDic.Select(q => $"{q.Key}={q.Value}")); var arg3 = String.Join("&", contractDic.Select(q => $"{q.Key}={q.Value}")); var startInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, FileName = "IpcProcess.exe", Arguments = $"{arg1} {arg2} {arg3}" }; process = Process.Start(startInfo); process.Exited += Exited; if (process != null) { hJob = process.AssignProcessToJobObject("Job:" + ipcServerDic["portName"]); // wait until it's ready 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); return true; } } } Dispose(); return false; } public void WaitForExit(int milliseconds = -1) { process?.WaitForExit(milliseconds); } public void RegisterCallback(string methodName, Delegate callback) { base.RegisterCallback(ipcServerDic["portName"], methodName, callback); } public void Dispose() { UnRegisterCallback(ipcServerDic["portName"]); if (hJob != IntPtr.Zero) { NativeMethods.TerminateJobObject(hJob, 0); hJob = IntPtr.Zero; } if (process != null) { if (!process.HasExited) process.Kill(); process = null; } } public T Proxy => proxy; public event EventHandler Exited; private Process process; private T proxy; private IntPtr hJob; private Dictionary<string, string> ipcServerDic; }
客服端:
public class IpcProcessProxy { protected void Start() { lock (locker) { if (ChannelServices.GetChannel("ipcClient") != null) return; ipcClientDic = new Dictionary<string, string> { ["name"] = "ipcClient", ["portName"] = Guid.NewGuid().ToString("N") }; // Create and register an IPC channel channel = new IpcServerChannel(ipcClientDic, new BinaryServerFormatterSinkProvider()); ChannelServices.RegisterChannel(channel, false); // Expose an object callRemoteObj = new CallRemoteObject(ipcClientDic["portName"]); callRemoteObj.Called += OnCalled; objRef = RemotingServices.Marshal(callRemoteObj, "CallRemoteObject"); } } 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); } } private void OnCalled(string url, string methodName, object[] parameters) { if (!urls.TryGetValue(url, out Dictionary<string, Delegate> callbacks) || !callbacks.TryGetValue(methodName, out Delegate callback) || callback == null) return; callback.DynamicInvoke(parameters); } 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(); }
解决方案结构:
测试代码:
class Program { static void Main(string[] args) { Test(2); Console.ReadLine(); } private static void Test(int count) { var type = typeof(RemoteObject); IpcProxyDictionary.Set(typeof(IRemoteObject), type.Assembly.GetName().Name, type.FullName, type.Name, typeof(CallRemoteObject).IsAssignableFrom(type)); using (var processProxy = new IpcProcessProxy()) { if (!processProxy.Start()) { Console.WriteLine("启动代理进程失败!"); return; } var proxy = processProxy.Proxy; processProxy.RegisterCallback("OnCallback", new Action<string>(n => Console.WriteLine(n))); Console.WriteLine(proxy.Add(1, 2)); proxy.TestCall(); if (--count > 0) Test(count); } } }
输出结果:
测试结论:需要回调哪个方法,就调Call,参数传回调方法的参数,同时支持多个子进程运行。