.net remoting 使用事件

原文: .net remoting 使用事件

在RPC若是須要使用事件,相對是比較難的。本文告訴你們如何在 .net remoting 使用事件。html

在我這個博客WPF 使用RPC調用其餘進程已經有告訴你們如何簡單使用。git

可是對於事件的使用仍是沒有詳細告訴你們。安全

先來寫一個簡單的代碼,須要建立三個項目,一個存放的是其餘進程,一個是庫,另外一個是呆磨。服務器

若是隻是想快速使用,請看本文下面的開發建議。框架

在上個文章告訴你們的時候沒有告訴你們使用的 Channel 的方式,下面讓我來告訴你們如何使用 Channelide

使用 Channel

實際上可使用的 Channel 是有不少,能夠本身定義,可是建議使用的有三個函數

  • HttpChannel 功能比較強大,支持在廣域網使用,可讓不少不是 .net 寫的程序使用,可是須要本身寫安全的代碼post

  • TcpChannel 速度更快的方式,通常在局域網使用ui

  • IpcChannel 就在相同的機器內使用,速度最快,使用的是微軟系統系統的方法this

全部的 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與生命週期以及跟蹤服務 - 張逸 - 博客園

In Depth .NET Remoting

Ingo Rammer,《Advanced .NET Remoting》

.net remoting 拋出異常

.NET Remoting程序開發入門篇-博客-雲棲社區-阿里雲

.NET Remoting中的事件處理(.NET Framework 2.0)(一) - 大壞蛋 - 博客園

WPF 使用RPC調用其餘進程


本文會常常更新,請閱讀原文: https://lindexi.gitee.io/lindexi/post/.net-remoting-%E4%BD%BF%E7%94%A8%E4%BA%8B%E4%BB%B6.html ,以免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。

知識共享許可協議 本做品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、從新發布,但務必保留文章署名林德熙(包含連接: https://lindexi.gitee.io ),不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。若有任何疑問,請 與我聯繫

相關文章
相關標籤/搜索