.NET Remoting 和跨域封送






4.80/5 (11投票s)
一篇关于如何将远程客户端请求从一个 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 对象,将调用重定向到另一个对象/方法。如果您重新部署服务器但无法更新包含通常保留在共享程序集中的接口定义到客户端,则这是一项有用的技术。
- 您可以选择性地对单个方法参数的粒度应用加密/解密。
- 您可以通过计算和计时方法调用来监视您认为有价值的任何内容。
可能性的列表仅受您的想象力的限制。希望本文能得到您的欣赏。