AppDomain和MarshalByRefObject的生命周期:如何避免RemotingException?

Gui*_*ume 56 .net c# remoting appdomain object-lifetime

当MarshalByRef对象从AppDomain(1)传递到另一个(2)时,如果你在第二个AppDomain(2)中调用方法之前等待6分钟,你将得到一个RemotingException:

System.Runtime.Remoting.RemotingException:对象[...]已断开连接或在服务器上不存在.

有关此问题的一些文档:

如果我错了,请纠正我:如果InitializeLifetimeService返回null,那么当AppDomain 2被卸载时,该对象只能在AppDomain 1中收集,即使收集了代理?

有没有办法禁用生命周期并保持代理(在AppDomain 2中)和对象(在AppDomain1中)保持活动状态,直到代理完成为止?也许与ISponsor ......?

woo*_*hoo 41

看到答案:

http://social.msdn.microsoft.com/Forums/en-US/netfxremoting/thread/3ab17b40-546f-4373-8c08-f0f072d818c9/

基本上说:

[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
  return null;
}
Run Code Online (Sandbox Code Playgroud)

  • 嗯......有几个人没有说出他们为什么这样做而对此表示赞同.虽然这可能毫无意义,但知道原因(从文明的角度来看......)会很好.此外,这个解决方案在现实生活中的商业应用非常有效,我不只是把它从帽子里拉出来. (7认同)
  • 我想downvotes是因为你的解决方案非常极端.当然它适用于*您的*现实生活中的商业应用程序,但仅仅是因为您不是一遍又一遍地创建新对象.我对1个对象使用相同的解决方案,我知道必须永远存在,直到应用程序关闭.但是,如果每次客户端自身连接时都会创建这样的对象,那么该解决方案将无法工作,因为它们永远不会被GC并且您的内存消耗会上升,直到您停止服务器或崩溃,因为它没有更多的内存. (7认同)

Gui*_*ume 13

我终于找到了一种方法来做客户端激活的实例,但它涉及到Finalizer中的托管代码:(我将我的类专门用于CrossAppDomain通信,但你可以修改它并尝试其他远程处理.如果你发现任何错误,请告诉我.

以下两个类必须位于所涉及的所有应用程序域中加载的程序集中.

  /// <summary>
  /// Stores all relevant information required to generate a proxy in order to communicate with a remote object.
  /// Disconnects the remote object (server) when finalized on local host (client).
  /// </summary>
  [Serializable]
  [EditorBrowsable(EditorBrowsableState.Never)]
  public sealed class CrossAppDomainObjRef : ObjRef
  {
    /// <summary>
    /// Initializes a new instance of the CrossAppDomainObjRef class to
    /// reference a specified CrossAppDomainObject of a specified System.Type.
    /// </summary>
    /// <param name="instance">The object that the new System.Runtime.Remoting.ObjRef instance will reference.</param>
    /// <param name="requestedType"></param>
    public CrossAppDomainObjRef(CrossAppDomainObject instance, Type requestedType)
      : base(instance, requestedType)
    {
      //Proxy created locally (not remoted), the finalizer is meaningless.
      GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Initializes a new instance of the System.Runtime.Remoting.ObjRef class from
    /// serialized data.
    /// </summary>
    /// <param name="info">The object that holds the serialized object data.</param>
    /// <param name="context">The contextual information about the source or destination of the exception.</param>
    private CrossAppDomainObjRef(SerializationInfo info, StreamingContext context)
      : base(info, context)
    {
      Debug.Assert(context.State == StreamingContextStates.CrossAppDomain);
      Debug.Assert(IsFromThisProcess());
      Debug.Assert(IsFromThisAppDomain() == false);
      //Increment ref counter
      CrossAppDomainObject remoteObject = (CrossAppDomainObject)GetRealObject(new StreamingContext(StreamingContextStates.CrossAppDomain));
      remoteObject.AppDomainConnect();
    }

    /// <summary>
    /// Disconnects the remote object.
    /// </summary>
    ~CrossAppDomainObjRef()
    {
      Debug.Assert(IsFromThisProcess());
      Debug.Assert(IsFromThisAppDomain() == false);
      //Decrement ref counter
      CrossAppDomainObject remoteObject = (CrossAppDomainObject)GetRealObject(new StreamingContext(StreamingContextStates.CrossAppDomain));
      remoteObject.AppDomainDisconnect();
    }

    /// <summary>
    /// Populates a specified System.Runtime.Serialization.SerializationInfo with
    /// the data needed to serialize the current System.Runtime.Remoting.ObjRef instance.
    /// </summary>
    /// <param name="info">The System.Runtime.Serialization.SerializationInfo to populate with data.</param>
    /// <param name="context">The contextual information about the source or destination of the serialization.</param>
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      Debug.Assert(context.State == StreamingContextStates.CrossAppDomain);
      base.GetObjectData(info, context);
      info.SetType(typeof(CrossAppDomainObjRef));
    }
  }
Run Code Online (Sandbox Code Playgroud)

现在是CrossAppDomainObject,你的远程对象必须从这个类而不是MarshalByRefObject继承.

  /// <summary>
  /// Enables access to objects across application domain boundaries.
  /// Contrary to MarshalByRefObject, the lifetime is managed by the client.
  /// </summary>
  public abstract class CrossAppDomainObject : MarshalByRefObject
  {
    /// <summary>
    /// Count of remote references to this object.
    /// </summary>
    [NonSerialized]
    private int refCount;

    /// <summary>
    /// Creates an object that contains all the relevant information required to
    /// generate a proxy used to communicate with a remote object.
    /// </summary>
    /// <param name="requestedType">The System.Type of the object that the new System.Runtime.Remoting.ObjRef will reference.</param>
    /// <returns>Information required to generate a proxy.</returns>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public sealed override ObjRef CreateObjRef(Type requestedType)
    {
      CrossAppDomainObjRef objRef = new CrossAppDomainObjRef(this, requestedType);
      return objRef;
    }

    /// <summary>
    /// Disables LifeTime service : object has an infinite life time until it's Disconnected.
    /// </summary>
    /// <returns>null.</returns>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public sealed override object InitializeLifetimeService()
    {
      return null;
    }

    /// <summary>
    /// Connect a proxy to the object.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void AppDomainConnect()
    {
      int value = Interlocked.Increment(ref refCount);
      Debug.Assert(value > 0);
    }

    /// <summary>
    /// Disconnects a proxy from the object.
    /// When all proxy are disconnected, the object is disconnected from RemotingServices.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void AppDomainDisconnect()
    {
      Debug.Assert(refCount > 0);
      if (Interlocked.Decrement(ref refCount) == 0)
        RemotingServices.Disconnect(this);
    }
  }
Run Code Online (Sandbox Code Playgroud)

  • 这是错的.您应该使用父AppDomain中的ISponsor来管理子AppDomain中实例的生命周期.那就是MBRO的设计目标.这是一个受COM启发的黑客攻击. (3认同)
  • @Guillaume:嗯,你必须做你必须做的事情.寻找这个答案的人们了解正在发生的事情非常重要.*总是*从MBRO.ILS返回null就像总是捕捉和吞咽异常.是的,有时您应该这样做,但只有当您确切知道自己在做什么时才会这样做. (3认同)
  • @Guillaume:它实际上很容易实现.您在父域中的代理上调用InitializeLifetimeService.它返回一个转换为ILease的对象.然后,您在通过ISponsor的租约中呼叫注册.框架将经常在ISponsor上调用Renewal,您所要做的就是确定是否要续订代理并返回适当的TimeSpan长度. (2认同)
  • @Will:谢谢,我几乎从你的评论中提取了一个解决方案.但是你为什么不给出一个完整,正确的答案呢? (2认同)

小智 6

不幸的是,当AppDomains用于插件时,此解决方案是错误的(插件的程序集不能加载到您的主appdomain中).

构造函数和析构函数中的GetRealObject()调用会导致获取远程对象的实际类型,从而导致尝试将远程对象的程序集加载到当前的AppDomain中.这可能会导致异常(如果无法加载程序集)或者加载了以后无法卸载的外部程序集的不良影响.

如果您使用ClientSponsor.Register()方法在主AppDomain中注册远程对象(不是静态的,那么您必须创建客户端发起人实例),这是一个更好的解决方案.默认情况下,它将每2分钟更新一次远程代理,如果您的对象具有默认的5分钟生命周期就足够了.


cdi*_*ins 6

这里有两种可能的解决方案。

单例方法:覆盖 InitializeLifetimeService

正如Sacha Goldshtein在原始海报链接的博客文章中指出的那样,如果您的封送对象具有单例语义,您可以覆盖InitializeLifetimeService

class MyMarshaledObject : MarshalByRefObject
{
    public bool DoSomethingRemote() 
    {
      // ... execute some code remotely ...
      return true; 
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
    public override object InitializeLifetimeService()
    {
      return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,正如 user266748 在另一个答案中指出的那样

如果每次客户端连接自己时都创建了这样的对象,那么该解决方案将不起作用,因为它们永远不会被 GC 处理,并且您的内存消耗会不断增加,直到您停止服务器或由于它没有更多内存而崩溃

基于类的方法:使用 ClientSponsor

一个更通用的解决方案是使用ClientSponsor来延长类激活的远程对象的寿命。链接的 MSDN 文章有一个有用的起始示例,您可以遵循:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Lifetime;
namespace RemotingSamples
{

   class HelloClient
   {
       static void Main()
      {
         // Register a channel.
         TcpChannel myChannel = new TcpChannel ();
         ChannelServices.RegisterChannel(myChannel);
         RemotingConfiguration.RegisterActivatedClientType(
                                typeof(HelloService),"tcp://localhost:8085/");

         // Get the remote object.
         HelloService myService = new HelloService();

         // Get a sponsor for renewal of time.
         ClientSponsor mySponsor = new ClientSponsor();

         // Register the service with sponsor.
         mySponsor.Register(myService);

         // Set renewaltime.
         mySponsor.RenewalTime = TimeSpan.FromMinutes(2);

         // Renew the lease.
         ILease myLease = (ILease)mySponsor.InitializeLifetimeService();
         TimeSpan myTime = mySponsor.Renewal(myLease);
         Console.WriteLine("Renewed time in minutes is " + myTime.Minutes.ToString());

         // Call the remote method.
         Console.WriteLine(myService.HelloMethod("World"));

         // Unregister the channel.
         mySponsor.Unregister(myService);
         mySponsor.Close();
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

在 Remoting API 中生命周期管理的工作方式毫无价值,MSDN 上对此进行了很好的描述。我引用了我认为最有用的部分:

远程终身服务将租用与每个服务相关联,并在其租用时间到期时删除服务。终身服务既可以承担传统分布式垃圾收集器的功能,也可以在每台服务器的客户端数量增加时进行很好的调整。

每个应用程序域都包含一个租用管理器,负责控制其域中的租用。定期检查所有租用是否已过期。如果租约已到期,则会调用一个或多个租约的发起人并给予机会续订租约。如果没有发起者决定续订租约,则租约管理器删除租约,垃圾收集器可以收集对象。租用管理器维护一个租用列表,其中的租用按剩余租用时间排序。剩余时间最短的租约存储在列表的顶部。远程终身服务将租用与每个服务相关联,并在其租用时间到期时删除服务。