在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与生命周期以及跟踪服务 - 张逸 - 博客园
Ingo Rammer,《Advanced .NET Remoting》
.NET Remoting程序开发入门篇-博客-云栖社区-阿里云