在RPC如果需要使用事件,相對是比較難的。本文告訴大家如何在 .net remoting 使用事件。
在我這個博客WPF 使用RPC調用其他進程已經有告訴大家如何簡單使用。
但是對於事件的使用還是沒有詳細告訴大家。
先來寫一個簡單的代碼,需要創建三個項目,一個存放的是其他進程,一個是庫,另一個是呆磨。
如果只是想快速使用,請看本文下面的開發建議。
在上個文章告訴大家的時候沒有告訴大家使用的 Channel 的方式,下面讓我來告訴大家如何使用 Channel
使用 Channel
實際上可以使用的 Channel 是有很多,可以自己定義,但是建議使用的有三個
- HttpChannel 功能比較強大,支持在廣域網使用,可以讓很多不是 .net 寫的程序使用,但是需要自己寫安全的代碼
- TcpChannel 速度更快的方式,一般在局域網使用
- IpcChannel 就在相同的機器內使用,速度最快,使用的是微軟系統系統的方法
所有的 Channel 都需要傳入 port ,但是不是所有的類型都是 int ,其中 HttpChannel 和 TcpChannel使用的都是 int ,一般給的空閒的端口。而 IpcChannel 需要的是一個字符串,可以給他一個隨機的字符串。
序列化
如果簡單寫一個類,使用了這個類裏的事件,那麼一般會出現異常
程序集“林德熙.RemoteProcess.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”中的類型“林德熙.RemoteProcess.Demo.MainWindow”未標記爲可序列化
爲了可以使用事件,需要先修改 Channel ,下面我使用的是 IpcChannel
寫一個方法來創建連接,寫在庫項目,這個方法在呆磨和其他進程需要使用,原來創建相同的方法進行連接
public static IChannel CreatChannel(string port = "") { if (string.IsNullOrEmpty(port)) { port = Guid.NewGuid().ToString("N"); } var serverProvider = new SoapServerFormatterSinkProvider(); var clientProvider = new SoapClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary props = new Hashtable(); props["portName"] = port.ToString(); return new IpcChannel(props, clientProvider, serverProvider); }
代碼需要使用 TypeFilterLevel 設置,默認使用的是Low,所以會出現事件無法序列化。
其實傳入的 serverProvider等 可以使用 BinaryServerFormatterSinkProvider 類型,一般推薦使用 SoapServerFormatterSinkProvider ,他的速度比較快。
這時呆磨使用的創建就不需要寫端口
_channel = Terminal.CreatChannel();//客戶端 ChannelServices.RegisterChannel(_channel, false);
其他進程需要指定一個端口,這時呆磨傳入的,因爲呆磨需要知道其他進程使用的纔可以
_channel = Terminal.CreatChannel(port); ChannelServices.RegisterChannel(_channel, false);
一般在 IpcChannel 都是說連接是不安全的,因爲有很多特殊的軟件都會發送一些信息讓軟件通信失敗
因爲序列化需要知道類的屬性,所以需要在獲得事件,重新使用一個類來獲得
需要在庫定一個兩個類,一個是 Foo ,也就是需要獲得事件的類,另一個是 F1 用於給呆磨轉消息
//庫 public class Foo : MarshalByRefObject { public event EventHandler F1; }
//其他進程 _channel = Terminal.CreatChannel(port); ChannelServices.RegisterChannel(_channel, false); var obj = new Foo(); ObjRef objRef = RemotingServices.Marshal(obj, temp.Name);
//呆磨 public void Connect() { //啓動遠程進程 ProcessId = Process.Start("林德熙.RemoteProcess.exe", "-p " + Port)?.Id ?? -1; _channel = Terminal.CreatChannel();//客戶端 ChannelServices.RegisterChannel(_channel, false); } public T GetObject<T>() { CheckProcess(); return (T) Activator.GetObject(typeof(T), "Ipc://" + Port + "/" + typeof(T).Name); } GetObject<Foo>().F1 += MainWindow_F1; //出現異常
因爲沒有把呆磨序列,只能再新建一個類 F1
// 庫 public delegate void F2(object obj, string str); [Remote] public class Foo : MarshalByRefObject { public event F2 F1; public virtual void OnF1() { F1?.Invoke(this, "cnblogs"); } } public class F1 : MarshalByRefObject { public event EventHandler<string> Foo; public void OnF1(object sender, string e) { Foo?.Invoke(sender, e); } }
運行的時候,兩個類所在的是 Foo 在其他進程,而 F1 在呆磨程序
使用的時候需要這樣寫
var f = GetObject<Foo>(); F1 f1 = new F1(); //創建一個類來直接獲得事件,不能直接添加呆磨程序中的函數,必須創建另一個類 f.F1 += f1.OnF1; f1.Foo += Foo; //這個類的事件給呆磨 private void Foo(object sender, string s2) { }
可以看到運行f.OnF1();
就可以讓呆磨Foo獲得值
從上面代碼看到,爲什麼不使用 EventHandler<string>
,自己定義委託,一般都是不建議自己定義,但是這裏需要自己定義的,因爲如果使用 EventHandler<string>
會出現異常
Soap 序列化程序不支持序列化一般類型: System.EventHandler`1[System.String]。
這就是用事件的方法,需要記得
在庫創建兩個類,一個類用於從其他進程發送事件給呆磨,另一個類用於接收這個事件,把事件轉發給呆磨
原因是在使用 +=
需要序列化右邊的這個類,而如何直接對 Foo 類進行添加事件,那麼需要序列化呆磨。然而呆磨沒有放在庫,而且其他進程沒有引用呆磨,所以其他進程無法序列呆磨的類型。但是在庫寫另一個類F1,其他進程可以序列化F1,所以可以獲得在呆磨創建的F1。把事件給在呆磨創建的F1,讓F1轉發事件給呆磨。
實際上使用的時候就比直接使用需要加一個新的類,而且不能直接使用EventHandler<string>
爲什麼不能使用 EventHandler<string>
原因是 SoapServerFormatterSinkProvider 不支持泛型,可以使用 BinaryServerFormatterSinkProvider 的方法
下面是總結的使用事件需要注意的點
- 最好不要使用辣麼大做委託
- 如果需要使用泛型的委託,請設置
BinaryServerFormatterSinkProvider
序列方法 - 最好使用一個本地類讓遠程進程可見的方法,將遠程進程的事件轉換爲本地的事件
雖然給了一些需要注意的點,但是如果可以按照下面方式進行開發,會少很多坑。
開發建議
如果已經在封裝好的框架進行開發,在很多的時候,就和使用本地的代碼一樣。但是對於事件和委託就需要做一層處理。
所以這時就建議開發時寫一對類,抽出功能接口的方法。
寫一對類的意思就是原來例如是 Xx 類,現在就需要抽出 IXx 接口,使用這個接口來代替原有的類。
例如最簡單的功能,我需要通過一個方法觸發一個事件,請看下面
public class XxEventHandle { public void CallHandle() { Progress?.Invoke(null,"123"); } public event EventHandler<string> Progress; }
現在覺着的方法不清真,想要將這個方法放在另一個進程運行,就需要先將這個類抽出接口
public interface IRemoteEventHandle { void CallHandle(); event EventHandler<string> Progress; }
然後將這個類拆爲兩個類,一個是 Remote 的運行在遠程進程,另一個是 Native 運行在本機。但是對於遠程進程是完全知道 Remote 和 Native 的。
這時需要先將這幾個類都移動到一個新項目,然後右擊這個項目屬性生成,讓生成序列化程序集爲開
如果打開了序列化程序集之後還出現下面異常
System.Runtime.Remoting.RemotingException:“權限被拒絕: 無法遠程調用非公共或靜態方法。”
出現這個異常有幾個原因,如果只是爲了解決這個異常來看本文,請看下方。
建議新建的兩個類是寫在一個文件,而且需要讓兩個類繼承 MarshalByRefObject
和接口 IRemoteEventHandle
,並且只允許本地的NativeEventHandle
在構造傳入遠程的類。
在RemoteEventHandle
需要添加特性Serializable
,而另一個特性Remote
是我自己寫的,用來判斷這個類是在另一個進程運行,在另一個進程運行就會加載這些類
在用戶使用的都是 IRemoteEventHandle
而這個接口實例是 NativeEventHandle
類,在拿到的事件需要先使用 NativeEventHandle
的公開方法去監聽 RemoteEventHandle
的事件。
[Remote] [Serializable] public class RemoteEventHandle : MarshalByRefObject, IRemoteEventHandle { public void CallHandle() { Console.WriteLine("調用事件"); Progress?.Invoke(null, "歡迎訪問我博客 http://blog.csdn.net/lindexi_gd"); } public event EventHandler<string> Progress; // 如果不重寫,可能這個對象發送到遠程時,在遠程被回收,於是事件就無法調用 // 如果剛好寫了 OneWay 特性,那麼連異常都沒有。遠程調用了事件,發現調用成功,但是本地沒有收到任何的事件 public override object InitializeLifetimeService() { // 返回null值表明這個遠程對象的生命週期爲無限大 return null; } } public class NativeEventHandle : MarshalByRefObject, IRemoteEventHandle { /// <inheritdoc /> public NativeEventHandle(RemoteEventHandle remoteJesteRinoowi) { RemoteEventHandle = remoteJesteRinoowi; } public void CallHandle() { // 使用 NativeEventHandle 的公開方法去拿到 RemoteEventHandle 的事件 // 原因 事件需要將代碼發送到另一個進程,這就需要讓遠程支持這個方法的序列化 // 如果直接讓上層的代碼 += 方法就會因爲另一個進程不知道上層的代碼的序列化出現異常 // 爲了解決這個問題,就需要先使用這個類定義的方法,這樣就可以序列化這個類,讓遠程知道調用的事件是哪個函數 // 然後在這個類的方法再次調用這個類的事件,這時在上層的代碼使用了這個類的事件也是沒問題,因爲這時代碼已經是在本地運行,就和原來的事件一樣 // 原理是使用序列化方法調用,所以需要讓方法爲公開 RemoteEventHandle.Progress += RemoteEventHandle_Progress; RemoteEventHandle.CallHandle(); } public void RemoteEventHandle_Progress(object sender, string e) { // 如果這個方法是 private 的,就會出現 System.Runtime.Remoting.RemotingException:“權限被拒絕: 無法遠程調用非公共或靜態方法。” Progress?.Invoke(sender, e); } public event EventHandler<string> Progress; private RemoteEventHandle RemoteEventHandle { get; } // 如果不重寫,可能這個對象發送到遠程時,在遠程被回收,於是事件就無法調用 // 如果剛好寫了 OneWay 特性,那麼連異常都沒有。遠程調用了事件,發現調用成功,但是本地沒有收到任何的事件 public override object InitializeLifetimeService() { // 返回null值表明這個遠程對象的生命週期爲無限大 return null; } }
對於剛纔的Remote
特性請看下面,建議使用WPF 封裝 dotnet remoting 調用其他進程
/// <summary> /// 共享使用的類,這個類會在遠程進程創建 /// </summary> [AttributeUsage(AttributeTargets.Class)] public class RemoteAttribute : Attribute { }
那麼如何在 remoting 使用回調?
原來的開發可能有一些委託回調,如果在 remoting 是不支持使用委託回調的方法,只能通過事件的方法。如果要作爲委託,需要寫很多代碼,這裏我就不說了。所有的回調都可以使用事件的方法轉換。
如原來的類是有函數回調
public void SetCallBack(EventHandler callback)
那麼如何使用這個回調,實際上在 Remote 將回調轉事件就可以
修復異常
如果發現 System.Runtime.Remoting.RemotingException
就需要找是否出現下面的問題
第一個問題是調用了非公共的方法,包括靜態或非靜態的方法。這個過程是發生在序列化的過程。序列化無法調用非公共的方法。
出現的異常請看下面
System.Runtime.Remoting.RemotingException:“權限被拒絕: 無法遠程調用非公共或靜態方法。”
很多時候在觸發事件時會出現這個異常,原因是如果出現了事件的回調,那麼就可能因爲回調使用的是本地私有的方法讓回調無法使用。
如下面的代碼
[Serializable] public class RemoteEventHandle : MarshalByRefObject, IRemoteEventHandle { public void CallHandle() { Console.WriteLine("調用事件"); Progress?.Invoke(null, "歡迎訪問我博客 http://blog.csdn.net/lindexi_gd"); } public event EventHandler<string> Progress; public override object InitializeLifetimeService() { return null; } } public interface IRemoteEventHandle { void CallHandle(); event EventHandler<string> Progress; } public class NativeEventHandle : MarshalByRefObject, IRemoteEventHandle { /// <inheritdoc /> public NativeEventHandle(RemoteEventHandle remoteJesteRinoowi) { RemoteEventHandle = remoteJesteRinoowi; RemoteEventHandle.Progress += RemoteEventHandle_Progress; } public void CallHandle() { // 使用 NativeEventHandle 的公開方法去拿到 RemoteEventHandle 的事件 // 原因 事件需要將代碼發送到另一個進程,這就需要讓遠程支持這個方法的序列化 // 如果直接讓上層的代碼 += 方法就會因爲另一個進程不知道上層的代碼的序列化出現異常 // 爲了解決這個問題,就需要先使用這個類定義的方法,這樣就可以序列化這個類,讓遠程知道調用的事件是哪個函數 // 然後在這個類的方法再次調用這個類的事件,這時在上層的代碼使用了這個類的事件也是沒問題,因爲這時代碼已經是在本地運行,就和原來的事件一樣 // 原理是使用序列化方法調用,所以需要讓方法爲公開 RemoteEventHandle.CallHandle(); } public void RemoteEventHandle_Progress(object sender, string e) { // 如果這個方法是 private 的,就會出現 System.Runtime.Remoting.RemotingException:“權限被拒絕: 無法遠程調用非公共或靜態方法。” Progress?.Invoke(sender, e); } public event EventHandler<string> Progress; private RemoteEventHandle RemoteEventHandle { get; } public override object InitializeLifetimeService() { return null; } }
在本地的事件監聽,使用了本地的代碼 RemoteEventHandle_Progress
很多時候寫事件的監聽都使用私有的方法,如下面代碼
private void RemoteEventHandle_Progress(object sender, string e)
如果將 public 修改爲 private 就會出現 System.Runtime.Remoting.RemotingException:“權限被拒絕: 無法遠程調用非公共或靜態方法。”
原因是事件需要序列化方法。
因爲在 NativeEventHandle 是將 RemoteEventHandle_Progress
序列化傳到 RemoteEventHandle
使用事件,在事件觸發時通過序列化動態代理調用 RemoteEventHandle_Progress
方法。如果這個方法不是公開的,那麼動態代理調用就會因爲沒有訪問權限無法調用,這時就出現了 權限被拒絕: 無法遠程調用非公共或靜態方法
所以解決方法就是所有事件的函數都需要設置爲 public 纔可以。
修復事件斷開
有時候會發現一個程序放着過很久,遠程和本地的事件就斷開,也就是遠程的事件觸發正常,但是本地沒有收到。
在上面代碼的基礎,添加 CallHandle 調用事件前後的輸出
[Serializable] public class RemoteEventHandle : MarshalByRefObject, IRemoteEventHandle { public void CallHandle() { Console.WriteLine("調用事件"); Progress?.Invoke(null, "歡迎訪問我博客 http://blog.csdn.net/lindexi_gd"); Console.WriteLine("調用事件完成"); } // 忽略代碼 }
這時可以看到遠程輸出了
調用事件 調用事件完成
但是本地沒有收到任何的事件,原因就是本地監聽的代碼是將 NativeEventHandle 序列化發送到遠程,但是序列化的 NativeEventHandle和本地的連接可能被回收,於是調用 Progress 雖然能成功,而且可以看到裏面有對象,但是裏面的對象是不存在和本地的連接。
所以這時本地就沒有收到任何的事件。解決這個問題的方法就是重寫 InitializeLifetimeService 方法,返回 null ,這樣就可以設置遠程對象不回收。
這個問題有最簡單的例子,請看下面代碼,保持遠程的代碼不變
public class NativeEventHandle : MarshalByRefObject, IRemoteEventHandle { /// <inheritdoc /> public NativeEventHandle(RemoteEventHandle remoteJesteRinoowi) { RemoteEventHandle = remoteJesteRinoowi; RemoteEventHandle.Progress += RemoteEventHandle_Progress; } public void CallHandle() { RemoteEventHandle.CallHandle(); } public void RemoteEventHandle_Progress(object sender, string e) { Progress?.Invoke(sender, e); } public event EventHandler<string> Progress; private RemoteEventHandle RemoteEventHandle { get; } public override object InitializeLifetimeService() { ILease currentLease = (ILease) base.InitializeLifetimeService(); if (currentLease.CurrentState == LeaseState.Initial) { currentLease.InitialLeaseTime = TimeSpan.FromSeconds(5); currentLease.RenewOnCallTime = TimeSpan.FromSeconds(1); } return currentLease; }
上面的代碼就是通過重寫 InitializeLifetimeService 設置回收時間是 1 秒,這個方法不要在遠程對象重寫,否則調用回調方法就會出現下面異常
System.Runtime.Remoting.RemotingException:“對象“RemoteEventHandle”已經斷開連接或不在服務器上。” HResult -2146233077
關於 dotnet remoting 的對象回收請看Microsoft .Net Remoting系列專題之二:Marshal、Disconnect與生命週期以及跟蹤服務 - 張逸 - 博客園 裏面詳細解釋了這個問題。
參見:Microsoft .Net Remoting系列專題之三:Remoting事件處理全接觸 - 張逸 - 博客園
Microsoft .Net Remoting系列專題之二:Marshal、Disconnect與生命週期以及跟蹤服務 - 張逸 - 博客園
Ingo Rammer,《Advanced .NET Remoting》
.NET Remoting程序開發入門篇-博客-雲棲社區-阿里雲
.NET Remoting中的事件處理(.NET Framework 2.0)(一) - 大壞蛋 - 博客園