[C#] 基于IpcChannel进程通信开发插件,移除MAF约束
MAF的进程隔离是MEF、IOC无法实现的,好处在于可以立即结束进程,比如遇到耗时长的任务,无法立刻取消,白白地浪费了CPU、占用了资源,但是MAF的隔离性太强了,多个项目分开文件夹存放,导致了DLL文件无法共享,这无疑增加了安装包的大小,且至少需要7个项目(加主程序),改一个接口协议需要改7个项目代码,实在繁琐,参考官方的IpcChannel通信,只需要三个项目就可以搭建:IpcServerChannel、IpcClientChannel、RemoteObject,这里是先启动服务端,再启动客服端发送请求,改进一下,由客服端主动启动服务端,根据guid生成url,便可生成一个唯一的服务,另外服务端不需要事先引用DLL,而是加载由客服端指定的程序集和远程类,如此,服务端便只是一个空壳,类似MAF的AddInProcess,可能有些小伙伴到了这一步,发现客服端在第二次调用同一个远程类的RegisterWellKnownClientType方法时抛出“远程处理配置失败,异常为“RemotingException: 试图重定向类型“MySC.MyComponent, MyServiceComponent”的激活,而该类型已被重定向”,这是合理的,不使用重定向的方式便可,使用RealProxy直接拿到远程类的实例,使用该实例的方法是被代理的(Ipc通信有效),而使用直接创建远程类的方法是由客服端亲自执行的(非代理的),客服端退出时需要保证服务端退出,使用CreateJobObject解决(看了MAF源码,没发现如何保证插件退出的)
服务端:
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;
namespace IpcServerChannelApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(String.Join(" ", args));
var guid = args[0];
var list = from a in args.Skip(1)
let txts = a.Split('|')
select new
{
AssemblyName = txts[0],
TypeName = txts[1]
};
// Create and register an IPC channel
IpcServerChannel serverChannel = new IpcServerChannel(guid);
ChannelServices.RegisterChannel(serverChannel, false);
foreach (var item in list)
{
var assembly = Assembly.LoadFrom(item.AssemblyName);
var type = assembly.GetType(item.TypeName, true, false);
// Expose an object
RemotingConfiguration.RegisterWellKnownServiceType(type, type.Name, WellKnownObjectMode.Singleton);
Console.WriteLine("RegisterWellKnownServiceType: {0}", item.TypeName);
}
var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + guid);
readyEvent.Set();
// Wait for calls
Console.WriteLine("Listening on {0}", serverChannel.GetChannelUri());
Console.ReadLine();
}
}
}
客服端:
using IpcChannelModel;
using System;
using System.Data;
using System.Data.SQLite;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Security.Permissions;
using System.Threading;
namespace IpcClientChannelApp
{
class Program
{
static void Main(string[] args)
{
var path = @"E:\AppAnalyserFiles\data\DataBrowse.db";
var connectString = ConnectionStringFactory(path);
using (var proxy = new IpcProcessProxy())
{
if (proxy.Start())
{
var sqliteContract = proxy.Contract;
if (sqliteContract.Open(connectString))
{
Console.WriteLine("connect sqlite :" + path);
var sql = "select count(*) from DataBrowse";
var count = sqliteContract.ExecuteScalar(sql);
Console.WriteLine(sql);
Console.WriteLine(count);
}
}
}
Thread.Sleep(1000);
using (var proxy = new IpcProcessProxy())
{
if (proxy.Start())
{
var sqliteContract = proxy.Contract;
if (sqliteContract.Open(connectString))
{
Console.WriteLine("connect sqlite :" + path);
var sql = "select count(*) from DataBrowse";
var count = sqliteContract.ExecuteScalar(sql);
Console.WriteLine(sql);
Console.WriteLine(count);
}
}
}
Thread.Sleep(1000);
{
var sqliteContract = new SqliteContract();
if (sqliteContract.Open(connectString))
{
Console.WriteLine("connect sqlite :" + path);
var sql = "select count(*) from DataBrowse";
var count = sqliteContract.ExecuteScalar(sql);
Console.WriteLine(sql);
Console.WriteLine(count);
}
}
Console.ReadLine();
}
private static string ConnectionStringFactory(string path)
{
var builder = new SQLiteConnectionStringBuilder
{
DataSource = path,
Version = 3,
UseUTF16Encoding = true,
Pooling = false,
CacheSize = 100,
DateTimeFormat = SQLiteDateFormats.CurrentCulture,
JournalMode = SQLiteJournalModeEnum.Wal,
DefaultDbType = DbType.String,
SyncMode = SynchronizationModes.Normal,
BusyTimeout = 11
};
return builder.ToString();
}
}
class IpcProcessProxy : IDisposable where T : MarshalByRefObject, new()
{
public bool Start(int startUpTimeout = 10000)
{
var type = typeof(T);
var guid = Guid.NewGuid().ToString();
var startInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = "IpcServerChannelApp.exe",
Arguments = $"{guid} {type.Assembly.Location}|{type.FullName}"
};
process = Process.Start(startInfo);
if (process != null && !process.HasExited)
{
hJob = NativeMethods.CreateJobObjectW(lpName: "Job:" + guid);
if (hJob != IntPtr.Zero)
{
// 设置数据以便用于增加限制
var size = Marshal.SizeOf(typeof(NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
var newStructBasic = new NativeMethods.JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = 0x00002000
};
var newStructExtend = new NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = newStructBasic
};
var pStructAddr = Marshal.AllocHGlobal(size + 1);
Marshal.StructureToPtr(newStructExtend, pStructAddr, false);
// 设置限制
NativeMethods.SetInformationJobObject(hJob, 9, pStructAddr, size);
Marshal.FreeHGlobal(pStructAddr);
NativeMethods.AssignProcessToJobObject(hJob, process.Handle);
}
// wait until it's ready
using (var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + guid))
{
if (readyEvent.WaitOne(startUpTimeout, false))
{
clientChannel = new IpcClientChannel();
ChannelServices.RegisterChannel(clientChannel, false);
var url = $"ipc://{guid}/{type.Name}";
var messageSink = clientChannel.CreateMessageSink(url, null, out _);
var myProxy = new RealProxyBase(typeof(T), url, messageSink);
t = (T)myProxy.GetTransparentProxy();
return true;
}
}
}
Dispose();
return false;
}
public void Dispose()
{
if (clientChannel != null)
{
ChannelServices.UnregisterChannel(clientChannel);
clientChannel = null;
}
if (process != null)
{
if (!process.HasExited)
process.Kill();
process = null;
}
if (hJob != IntPtr.Zero)
NativeMethods.TerminateJobObject(hJob, 0);
}
public T Contract => t;
private Process process;
private IpcClientChannel clientChannel;
private T t;
private IntPtr hJob;
}
class RealProxyBase : RealProxy
{
private string url;
private IMessageSink messageSink;
[PermissionSet(SecurityAction.LinkDemand)]
public RealProxyBase(Type myType, string url, IMessageSink messageSink) : base(myType)
{
this.url = url;
this.messageSink = messageSink;
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
public override IMessage Invoke(IMessage message)
{
message.Properties["__Uri"] = url;
return messageSink.SyncProcessMessage(message);
}
}
static class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public int LimitFlags;
public uint MinimumWorkingSetSize;
public uint MaximumWorkingSetSize;
public int ActiveProcessLimit;
public long Affinity;
public int PriorityClass;
public int SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public uint ProcessMemoryLimit;
public uint JobMemoryLimit;
public uint PeakProcessMemoryUsed;
public uint PeakJobMemoryUsed;
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS
{
public long ReadOperationCount;
public long WriteOperationCount;
public long OtherOperationCount;
public long ReadTransferCount;
public long WriteTransferCount;
public long OtherTransferCount;
}
///
/// 创建任务对象
///
/// 指向安全相关属性结构体的指针,通常不用
/// 任务名称
///
[DllImport("Kernel32.dll")]
public static extern IntPtr CreateJobObjectW([In, Optional] IntPtr lpJobAttributes, [In, Optional] string lpName);
///
/// 添加进程到任务对象中
///
/// 任务句柄
/// 进程指针
///
[DllImport("Kernel32.dll")]
public static extern bool AssignProcessToJobObject([In] IntPtr hJob, [In] IntPtr hProcess);
///
/// 设置任务限制
///
/// 任务句柄
/// 设置的分类
/// 设置的参数结构体地址指针
/// 设置的参数结构体长度
///
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool SetInformationJobObject([In] IntPtr hJob, [In] int jobObjectInformationClass, [In] IntPtr lpJobObjectInformation, [In] int cbJobObjectInformationLength);
///
/// 添加进程到任务对象中
///
/// 任务句柄
/// 进程指针
///
[DllImport("Kernel32.dll")]
public static extern bool TerminateJobObject([In] IntPtr hJob, [In] UInt32 uExitCode);
}
}
远程类:
using System;
using System.Collections.Generic;
namespace IpcChannelModel
{
public class SqliteContract : MarshalByRefObject, IDisposable
{
private SqliteClient client = new SqliteClient();
public bool Open(string connectString) => client.Open(connectString);
public int Execute(string sql) => client.Execute(sql);
public object ExecuteScalar(string sql) => client.ExecuteScalar(sql);
public List Query(string sql) => client.Query(sql);
public void Dispose() => client.Dispose();
}
}
using System;
using System.Collections.Generic;
using System.Data.SQLite;
namespace IpcChannelModel
{
class SqliteClient : IDisposable
{
private SQLiteConnection conn;
public bool Open(string connectString)
{
conn = new SQLiteConnection(connectString).OpenAndReturn();
return conn != null;
}
public int Execute(string sql) => conn.Execute(sql);
public object ExecuteScalar(string sql) => conn.ExecuteScalar(sql);
public List Query(string sql) => throw new NotImplementedException();
public void Dispose() => conn.Dispose();
}
}