先前項目太忙了,沒時間寫博客,發現了一個有趣的東西,匆匆忙忙就寫完了,先描述一下需求背景:客戶端有幾張百萬級別的表需要聯合統計(如果是最大權限的賬號),改變查詢條件又要重新統計,因此常常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管理服務的生存周期,應該是生效了,后面幾天都沒有看到類似斷開連接或不在服務器上的異常。