[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,参数传回调方法的参数,同时支持多个子进程运行。

C