65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET Remoting 和跨域封送

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (11投票s)

2006年4月10日

CPOL

6分钟阅读

viewsIcon

93177

downloadIcon

1337

一篇关于如何将远程客户端请求从一个 AppDomain 封送到另一个 AppDomain 的文章。

引言

考虑编写一个基于 .NET Remoting 的服务器应用程序,该应用程序需要为每个客户端会话在单独的 AppDomain 中提供服务。您可以通过分配多个 AppDomain 并为每个 AppDomain 配置 .NET Remoting 来采取简单直接的方法。但是,每个 AppDomain 都必须配置为使服务器的侦听通道分配不同的端口。TCP 不允许每个端口有多个侦听器。

在客户端,每个应用程序都必须知道要连接到哪个端点(主机名:端口)才能在指定的 AppDomain 中建立会话。将某个会话绑定到特定 AppDomain 并通过特定端点将客户端应用程序引导到那里的概念当然可以实现,但这既不实用也不传统。客户端通常会请求连接到一个众所周知的端点,而不管服务器希望在哪个 AppDomain 中托管该会话。

本文是关于在单独的 AppDomain 中托管客户端会话,但允许所有客户端连接到同一个众所周知的端点。其思想是服务器在默认 AppDomain 中接收客户端请求,然后拦截并将请求转发到另一个 AppDomain。

如何拦截并将客户端的请求从一个 AppDomain 封送到另一个 AppDomain

远程方法调用本质上是将 Message 对象通过一个汇集器链从客户端传递到服务器端。方法调用被转换为一个可序列化的对象,通过客户端的汇集器链,被序列化到流中,并通过网络传输。在服务器端,它被接收、反序列化为 Message 对象,并通过一个汇集器链向上传递,最终重构为对服务的调用。我们希望在 Message 对象被重构为正确的调用的点之前拦截它。一旦拦截,我们就可以将 Message 对象重定向到合适的 AppDomain,在那里将发生重构为最终方法调用的过程。

幸运的是,.NET Remoting 基础结构为我们提供了进行这些拦截的机制。关键在于 ContextBoundObject。您需要从 ContextBoundObject 派生,而不是从 MarshalByRefObject 派生服务对象。

当然,客户端永远无法直接连接到在单独的 AppDomain 中实例化的实际服务对象。它们连接到的对象必须是某种跨域封送器,该封送器可以拦截方法调用并将其转发到合适的 AppDomain。一个被安装为拦截器的 ContextBoundObject CrossDomainMarshaller 必须是 ContextBoundObject

// this define the interceptor
[CrossDomainContextAttribute]
class CrossDomainMarshaller : ContextBoundObject
{
    // code ommitted for brevity
}

// this installs the interceptor in the server's default AppDomain
CrossDomainMarshaller marshaller = new CrossDomainMarshaller();
RemotingServices.Marshal(marshaller, "PrintService.rem");

CrossDomainMarshaller 假定客户端感兴趣的实际服务的对象 URI 是 "PrintService.rem"。客户端的代码通常如下所示:

string url = "tcp://myhost:1234/PrintService.rem"
IPrintService service = 
  (IPrintService)Activator.GetObject(typeof(IPrintService), url);
service.PrintMessage("Hello World");

CrossDomainMarshaller 完全没有实现 IPrintService 接口。那么,这怎么可能有效呢?答案在于 ContextBoundObject。我不会解释如何实现 ContextBoundObjet,而是推荐您查阅 MSDN 或关于 .NET Remoting 的优秀书籍,例如 Ingo Rammer 等人撰写的《Advanced .NET Remoting》,由 Apress 出版。本文还包含可供下载和检查的源代码。

重要的是,将 CrossDomainMarshaller 实现为 ContextBoundObject 要求我们同时实现一个 MessageSink 对象。这为我们提供了机会,可以在 Message 对象进入重构为最终方法调用的点之前拦截它。

class MessageSink : IMessageSink
{
    // other methods ommitted for brevity
    
    // here is where we must intercept redirect the client's method call
    public IMessage SyncProcessMessage(IMessage msg)
    {
        // if we just let this stand the msg object will just be
        // dispatched to the CrossDomainMarshaller
        // which does not have an implementation
        // of the IPrintService
        return this.nextSink.SyncProcessMessage(msg);    
    }
}

正如我之前提到的,Message 会沿着消息汇集器链传输,直到到达服务器端并转换为方法调用。如代码片段所示,方法调用绑定到 IPrintService 接口,而 CrossDomainMarshaller 并没有实现它。因此,将其传递给下一个汇集器将导致失败。我们需要将调用重定向到合适 AppDomain 中的合适服务对象。

问题在于找到当前调用客户端的正确 AppDomain。CrossDomainMarshaller 拦截所有客户端发送的所有消息,但我们无法识别每个客户端。Message 对象不包含有关客户端身份的任何特定信息。为了改进这一点,我们需要让客户端配合。客户端同意通过 CustomProxy 进行连接。以下是客户端代码的外观。

string url = "tcp://myhost:1234/PrintService.rem"
// IPrintService service = 
//  (IPrintService)Activator.GetObject(typeof(IPrintService), url);
IPrintService service = (IPrintService)new CustomProxy(url, 
                         typeof(IPrintService)).GetTransparentProxy();
service.PrintMessage("Hello World");

这并不是无法忍受的麻烦。但是客户端必须实现一个 CutomProxy,该代理必须如下所示:

class CustomProxy : RealProxy
{
    string url;
    string clientID;
    IMessageSink messageSink;

    public CustomProxy(string url, Type type) : base(type)
    {
        this.url = url;
        // create a unique client identifier
        this.clientID = Guid.NewGuid().ToString();

        foreach (IChannel channel in ChannelServices.RegisteredChannels)
        {
            if (channel is IChannelSender)
            {
                IChannelSender sender = (IChannelSender)channel;
                if (string.Compare(sender.ChannelName, "tcp") == 0)
                {
                    string objectUri;
                    this.messageSink = 
                         sender.CreateMessageSink(this.url, 
                         null, out objectUri);
                    if (this.messageSink != null)
                        break;
                }
            }
        }

        if (this.messageSink == null)
            throw new Exception("No channel found for " + this.url);
    }

    public override IMessage Invoke(IMessage msg)
    {
        msg.Properties["__Uri"] = this.url;
        
        // pass the client's id as part of the call context
        LogicalCallContext callContext = 
                 (LogicalCallContext)msg.Properties["__CallContext"];
        callContext.SetData("__ClientID", this.clientID);
        
        return this.messageSink.SyncProcessMessage(msg);
    }
}

CustomProxy 允许创建并随后传输一个唯一的客户端标识符与每次方法调用。Message 对象允许将其 LogicalCallContext 作为一部分传递额外的带外数据,从而在服务器端标识每个客户端。

class MessageSink : IMessageSink
{
    // other methods ommitted for brevity
    
    // here is where we must intercept redirect the client's method call
    public IMessage SyncProcessMessage(IMessage msg)
    {
        LogicalCallContext callContext = 
              (LogicalCallContext)msg.Properties["__CallContext"];
        string clientID = (string)callContext.GetData("__ClientID");
        if(clientID != null)
            return CrossDomainMarshaller.GetService(clientID).Marshal(msg);
        else
            return new ReturnMessage(new ApplicationException("No __ClientID"), 
                       (IMethodCallMessage)msg);
    }
}

return CrossDomainMarshaller.GetService(clientID).Marshal(msg); 是一个简写,表示“根据客户端标识符获取正确服务并调用其 Marshal 方法”。让我们看一下 CrossDomainMarshaller.GetService 的实现。

[CrossDomainContextAttribute]
class CrossDomainMarshaller : ContextBoundObject
{
    public static Dictionary<string, ICrossDomainService> 
           Dictionary = new Dictionary<string, ICrossDomainService>();

    public static ICrossDomainService GetService(string clientID)
    {
        // we created an AppDomain per client only once
        if (Dictionary.ContainsKey(clientID))
            return Dictionary[clientID];

        AppDomain appDomain = AppDomain.CreateDomain(clientID);
        ICrossDomainService service = 
            (ICrossDomainService)appDomain.CreateInstanceAndUnwrap(
            "ContextBoundRemoting.Service",
            "ContextBoundRemoting.PrintService");

        Dictionary.Add(clientID, service);

        return service;
    }
}

您可以清楚地看到,我们每位客户端仅创建一个 AppDomain,而不是每次调用都创建一个。然后,我们在该 AppDomain 中实例化 PrintService,并将其存储起来,以便在后续任何调用中检索。然而,我们返回的不是 PrintService 的某种接口,而是 ICrossAppDomainService 接口,该接口允许我们在 AppDomain 之间封送 Message。因此,该 PrintService 也需要实现 ICrossDomainService 接口。剩下的一个有趣的问题是如何将 Message 对象传递给 PrintService,从而能够恰当地调用 PrintService 中的方法。简短的答案在于另一个 CustomProxy。快速看一下 PrintService 类的完整实现。

public class PrintService :
    CrossDomainService,
    IPrintService
{
    public string PrintMessage(string msg)
    {
        Console.WriteLine("{0} in AppDomain {1}", msg, 
                          AppDomain.CurrentDomain.FriendlyName);
        return "Ok " + msg;
    }
}

您可以看到 PrintService 类扩展了 CrossDomainService 类。将 Message 转换为 PrintService 上正确调用的秘密隐藏在那里。检查 CrossDomainService

public class CrossDomainService :
       MarshalByRefObject,
       ICrossDomainService
{
    class Proxy : RealProxy
    {
        string uri;

        public Proxy(MarshalByRefObject obj)
        {
            this.uri = RemotingServices.Marshal(obj).URI;
        }
        public override IMessage Invoke(IMessage msg)
        {
            msg.Properties["__Uri"] = this.uri;
            return ChannelServices.SyncDispatchMessage(msg);
        }
    }

    Proxy proxy;

    public CrossDomainService()
    {
        this.proxy = new Proxy(this);
    }
    
    public IMessage Marshal(IMessage msg)
    {
        return this.proxy.Invoke(msg);
    }
}

CrossDomainService 实现 ICrossDomainService 接口方法 Marshal(IMessage msg)。Message 对象被转发到一个自定义代理,该代理最终将调用解析为 IPrintService.PrintMessage。自定义代理的实现并不那么复杂。

结论

ContextBoundObject 是一个非常强大的拦截方法调用的工具。如果您对应用程序服务器技术感兴趣,并一直想自己构建应用程序服务器,那么您很幸运。应用程序服务器都涉及拦截和干预方法调用。

  • 您可以提供自己的身份验证和授权方案。
  • 您可以重写 Message 对象,将调用重定向到另一个对象/方法。如果您重新部署服务器但无法更新包含通常保留在共享程序集中的接口定义到客户端,则这是一项有用的技术。
  • 您可以选择性地对单个方法参数的粒度应用加密/解密。
  • 您可以通过计算和计时方法调用来监视您认为有价值的任何内容。

可能性的列表仅受您的想象力的限制。希望本文能得到您的欣赏。

© . All rights reserved.