.NET Remoting 自定义化,轻松实现:自定义 Sink






4.86/5 (76投票s)
2003 年 5 月 21 日
20分钟阅读

223449

2182
.NET Remoting 的自定义化——并非难事!
引言
您是否觉得 .NET 开发很简单?我个人认为,与 COM / DCOM 在 C++ 中的实现方式相比,.NET 框架使我们开发人员的生活更加简便。毫无疑问,.NET Remoting 是这种简便性的绝佳例证。在阅读了一篇详尽的文章或您喜欢的 Remoting 书籍中的几个章节后,您就可以立即开始创建强大的分布式应用程序。 .NET Remoting 还提供了出色的灵活性和各种自定义选项。然而,简单到此为止。在我看来,.NET Remoting 非常易于使用,但并不那么容易自定义。要做到这一点,您必须熟悉 .NET Remoting 基础设施的内部机制,并且需要编写大量您根本不在乎的代码。请不要误会我的意思。我确实认为,一个更具洞察力的开发人员,一个理解底层开发基础设施的开发人员,本质上是一个更好的开发人员。我只是不认为这意味着需要理解和实现 .NET Remoting 自定义化有时要求的每一个细节。在本文中,我想介绍一个小型的库,它简化了 .NET Remoting 自定义化最重要的方面之一:自定义 Sink。
请不要被这篇文章的长度吓倒。在您读完一半之前,您就能掌握所需的一切,在几分钟内创建自己的自定义 Sink。其余部分是额外的、更高级的功能。
如果您已经了解了自定义 Sink 的所有知识,并迫不及待地想开始实现,请随时跳到“基本自定义 Sink”部分。无论如何,请务必阅读本文底部的免责声明。
什么是 Sink?
当您处理远程对象时,您并不持有该对象的引用,而是持有代理的引用。代理是一个对象,它看起来和感觉上都与远程对象完全相同,并且可以将基于堆栈的方法调用转换为消息,然后将消息发送到远程对象。为了将消息发送到远程对象,代理使用一个 Sink 链。它调用链中的第一个 Sink 并向其提供消息。Sink 可以选择性地修改消息,然后将其传递给下一个 Sink,依此类推。Sink 链中的一个 Sink 是 格式化 Sink。这个特殊 Sink 的任务是将消息序列化到流中。格式化 Sink 之后的 Sink 操作流,因为此时消息已不再相关(并且仅作为信息提供给 Sink)。流中的最后一个 Sink 是 传输 Sink,它负责将数据发送到服务器并等待响应。当响应到达时,传输 Sink 将其返回给上一个 Sink,然后响应开始沿着路径返回给代理。在此过程中,响应经过格式化 Sink,格式化 Sink 将响应反序列化回响应消息。
服务器端会发生什么?您猜对了。服务器也持有一个 Sink 链。这次链条通向目标对象。第一个 Sink 是传输 Sink。沿途有格式化 Sink,最后还有 堆栈生成器,它做的与客户端代理完全相反。它将消息转换为对目标对象的基于堆栈的方法调用。当目标对象的方法返回时,信息(返回值、ref 参数等)会被打包成一个消息,该消息通过相同的 Sink 链返回,从堆栈生成器开始,以传输 Sink 结束。
实际上,Sink 的数量比上面图示的要多,但我希望保持简单,只展示与当前讨论相关的 Sink。否则,我就错过了本文的重点,不是吗?
自定义 Sink
如上图所示,可以在客户端和服务器端将自定义 Sink 添加到链中。那么,我们何时决定开发自己的自定义 Sink 呢?我们通常在希望检查或修改从代理发送到远程对象的数据以及/或从远程对象返回到代理的数据时这样做。
假设您想加密在客户端和服务器之间传输的数据。为此,您可以创建一个客户端自定义 Sink 来加密传出的请求数据并解密传入的响应数据。您还应该创建一个服务器自定义 Sink 来解密从客户端传入的请求数据,并加密发送回客户端的响应数据。
自定义 Sink 可以位于格式化 Sink 之前或之后,具体取决于它们是设计用于操作消息还是序列化流。加密自定义 Sink 希望操作流(它不关心消息的逻辑含义;它只需要对其进行混淆)。因此,客户端自定义 Sink 应位于格式化 Sink 之后(消息已序列化到流之后),而服务器自定义 Sink 应位于格式化 Sink 之前(在流被反序列化回消息之前)。
再举个例子,假设您希望客户端发送用户名和密码信息,而服务器应该在允许访问目标对象之前检查这些信息。您可以将用户名和密码作为参数添加到目标对象的每个方法中。这会将所需信息有效地添加到消息中,但会非常麻烦。作为替代方案,您可以创建一个客户端自定义 Sink,它将用户名和密码添加到每个传出的消息中,并创建一个服务器自定义 Sink,它检索这些信息,并在用户名或密码无效时抛出异常(该异常将传播回客户端)。由于这些自定义 Sink 操作的是消息而不是序列化流,因此客户端自定义 Sink 应位于格式化 Sink 之前(在消息序列化到流之前),而服务器自定义 Sink 应位于格式化 Sink 之后(在流被反序列化回消息之后)。
太棒了!我该怎么做?
问题来了。为了实现您自己的自定义 Sink,您将付出巨大的努力!您必须定义一个类,该类实现至少一个 IMessageSink
、IClientChannelSink
和 IServerChannelSink
接口,具体取决于您定义的是客户端自定义 Sink 还是服务器自定义 Sink,以及您的 Sink 是位于格式化 Sink 之前还是之后。您应该确保将每个调用转发到下一个 Sink。您必须为同步调用和异步调用实现不同的逻辑。此外,异步调用处理方式对于上述三个接口中的每一个来说都是一个完全不同的故事。等等!还有更多。作为甜点,您还应该定义一个 Sink 提供程序 类,该类实现另一个接口(IClientChannelSinkProvider
或 IServerChannelSinkProvider
),并且能够根据 .NET Remoting 基础设施的请求创建您的自定义 Sink 实例。
这些任务当然是可行的,但非常繁琐、耗时且容易出错。当我第一次意识到这里要做的一切时,我问自己——他们不能提供一个基类来处理所有这些细节吗?我不能只通过实现自定义 Sink 的相关部分来处理我自己的业务逻辑吗?好吧,我在类库中没有找到这样的类,所以我决定自己编写一个。诚然,这个类并不涵盖所有自定义 Sink 的场景。通过简化事物,有时会失去一些灵活性。但是,我相信这个类对于大多数实际的自定义 Sink 场景都有效。BaseCustomSink
类及其相关类包含在 CustomSink
类库中。您可以下载该库,以及示例派生的自定义 Sink 和具有完整源代码的示例客户端和服务器。
特点
顾名思义,BaseCustomSink
类是自定义 Sink 的基类。
主要功能
- 支持客户端和服务器 Sink。
- 支持同步和异步调用。
- 可在多线程应用程序中安全使用。
- 自动从配置文件读取数据。
- 允许派生类在运行时决定是否要添加到 Sink 链。
- 调用派生类的静态初始化方法(如果存在)。
这些功能将在以下各节中进一步描述和演示。
基本自定义 Sink
为了演示 CustomSink
库的用法,让我们实现加密客户端/服务器 Sink。这些 Sink 的源代码可以在附带的 SampleSinks
类库中找到。由于我想专注于实现 Sink,而不是深入研究密码学,因此我将展示 LameEncpriptionClientSink
和 LameEncryptionServerSink
的实现,它们只是简单地向流中的每个字节添加/减去一个增量,以此模拟加密。将来我可能会发布基于此处提供的库的实际加密 Sink。
在实现 Sink 之前,让我们创建一个帮助类 LameEncryptionHelper
,它将处理实际的加密。它将有一个接受增量值的构造函数,以及两个方法:Encrypt
和 Decrypt
。Encrypt
方法如下所示。
public Stream Encrypt(Stream source)
{
byte tempByteData;
int tempIntData;
MemoryStream encrypted = new MemoryStream();
while ((tempIntData = source.ReadByte()) != -1)
{
tempByteData = (byte)tempIntData;
tempByteData += this.delta;
encrypted.WriteByte(tempByteData);
}
encrypted.Position = 0;
return encrypted;
}
Decrypt
方法几乎相同,但有一个区别——它只是减去增量而不是添加它。
BaseCustomSink
类包含两个虚拟方法:ProcessRequest
和 ProcessResponse
。ProcessRequest
方法在请求数据到达目标对象时被调用,而 ProcessResponse
方法在响应数据返回给代理时被调用。您可以重写这些方法以添加自己的处理。
protected virtual void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
protected virtual void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
可以看出,这两个方法的参数列表几乎相同。
- message – 这是正在传输的消息。
- headers – 由格式化 Sink 创建,允许在消息序列化后添加逻辑信息。
- stream – 包含序列化消息的流。我们可以修改流或分配新流。
- state – 在 ProcessRequest 中,我们可以将状态分配给任何对象,以便稍后在 ProcessResponse 中使用。
检查这些方法在 LameEncryptionClientSink
中的实现。
protected override void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
{
stream = this.encryptionHelper.Encrypt(stream);
headers["LamelyEncrypted"] = "Yes";
}
protected override void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
{
if (headers["LamelyEncrypted"] != null)
{
stream = this.encryptionHelper.Decrypt(stream);
}
}
这两个方法都使用 LameEncryptionHelper
类型的成员字段来执行实际的加密。ProcessRequest
还向头部添加信息,指明流已被“简陋地”加密。以下是 LameEncryptionServerSink
的实现。
protected override void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
{
if (headers["LamelyEncrypted"] != null)
{
stream = this.encryptionHelper.Decrypt(stream);
state = true;
}
}
protected override void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
{
if (state != null)
{
stream = this.encryptionHelper.Encrypt(stream);
headers["LamelyEncrypted"] = "Yes";
}
}
服务器 Sink 的代码设计为能够与任何客户端协同工作,无论它们是否使用 LameEncryptionClientSink
。因此,ProcessRequest
方法会检查头部以确定流是否已被“简陋地”加密。在这种情况下,该方法会执行两项操作:它会解密方法并将 true 分配给状态,以指示 ProcessResponse
加密响应。这样,只有发送了加密请求流的客户端才会收到加密的响应流。ProcessResponse
方法会检查状态对象,看它是否已被分配(实际值无关紧要,因为它只能是 true
或 null
),如果是,则加密响应流。
就是这样!我们的自定义 Sink 现在可以使用了。我将通过配置文件演示客户端和服务器如何利用 Sink。在此之前,我必须对 提供程序 说句话。.NET Remoting 基础设施不直接创建自定义 Sink。相反,它会创建一个 Sink 提供程序 类,该类能够按需创建自定义 Sink。CustomSinks
类库包含两个提供程序:CustomClientSinkProvider
和 CustomServerSinkProvider
,它们能够提供 BaseCustomSink
派生的类。您无需担心这些类。只需在配置文件中指定适当的类(客户端或服务器)。
文件名:Basic_SampleClient.exe.config
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http">
<clientProviders>
<formatter ref="soap" />
<provider
type="CustomSinks.CustomClientSinkProvider, CustomSinks"
customSinkType="LameEncryption.LameEncryptionClientSink, SampleSinks" />
</clientProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
文件名:Basic_SampleServer.exe.config
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" port="7878">
<serverProviders>
<provider
type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.LameEncryptionServerSink, SampleSinks" />
<formatter ref="soap" />
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
如上所示,提供程序通过 customSinkType 属性获得自定义 Sink 类型。类型以“命名空间.类, 程序集”的格式指定。在我的示例中,自定义 Sink 类位于名为 LameEncryption
的命名空间中,位于名为 SampleSinks
的程序集中。客户端和服务器应用程序将调用 RemotingConfiguration.Configure
并以参数形式传递配置文件名。请注意,Sink 在客户端配置文件的格式化程序之后出现,在服务器配置文件的格式化程序之前出现。这对 Sink 的正常运行至关重要,因为它们必须操作流。
注意:我在示例中使用了 HTTP 通道,但您可以轻松切换到 TCP。
您现在可以运行提供的示例客户端和服务器应用程序,以查看自定义 Sink 的实际运行情况。实际上,您不会看到太多内容,因为加密将不显眼地完成(毕竟,这就是全部的目的)。但是,我添加了一些控制台输出,这将证明 Sink 确实有效……
在本节中,我演示了使用 CustomSinks
库创建简单自定义 Sink 的相对容易程度。在简化了事物之后,我感觉应该稍微复杂化一下……对于许多自定义 Sink,本节中描述的基本功能就足够了。但是,为了避免在我们只需要一点额外功能时诉诸于“手动”实现自定义 Sink,我在 CustomSink 库中添加了一些更高级的功能。如果这已经满足了您的需求,您可以就此停止,并在将来参考本文。
在接下来的几节中,我将进一步开发 Lame Encryption Sink,以演示 CustomSinks 库的其他功能。由于我想保留最简单的示例不变,任何进一步的开发都将集成到 EnhancedLameEncryptionServerSink
和 EnhancedLameEncryptionClientSink
中(好像原始名称不够长一样……)。如果您想测试增强型 Sink,请确保打开 SampleClient.cs 和 SampleServer.cs 并取消注释相关行。
访问配置数据
要设计更通用的自定义 Sink,我们有时可能需要访问配置文件中的数据。例如,我们的 Lame Encryption Sink 可能希望读取增量值(添加到/减去流中每个字节的值)。当派生 Sink 自 BaseCustomSink
时,可以轻松完成此操作。如果配置文件在 provider 元素下包含 customData
元素,则自定义 Sink 可以通过其构造函数检索它。让我们回顾一下修改后的客户端和服务器配置文件的以下摘录(为简洁起见,此处仅展示 clientProviders
和 serverProviders
元素)。
Enhanced_SampleClient.exe.config
<clientProviders> <formatter ref="soap" /> <provider type="CustomSinks.CustomClientSinkProvider, CustomSinks" customSinkType="LameEncryption.EnhancedLameEncryptionClientSink, SampleSinks"> <customData delta = "15" /> </provider> </clientProviders>
Enhanced_SampleServer.exe.config
<serverProviders>
<provider type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.EnhancedLameEncryptionServerSink, SampleSinks">
<customData delta = "15" />
</provider>
<formatter ref="soap" />
</serverProviders>
为了检索此数据,Sink 应有一个接受 SinkCreationData
类型参数的构造函数。在这种情况下,将调用此构造函数,并通过此参数提供 customData
元素。请注意,如果存在这样的构造函数,则永远不会调用无参构造函数(即使对于相应的 Sink 没有 customData
元素)。
让我们回顾一下修改后的自定义客户端 Sink(EnhancedLameClientEncryptionSink
)的构造函数。服务器 Sink 的构造函数是相同的。
public EnhancedLameEncryptionClientSink(SinkCreationData creationData)
{
byte delta = 1;
if (creationData.ConfigurationData.Properties["delta"] != null)
{
delta = byte.Parse(
creationData.ConfigurationData.Properties["delta"].ToString());
}
this.encryptionHelper = new LameEncryptionHelper(delta);
}
Sink 链的自我排除
客户端 Sink 和服务器 Sink 的主要区别之一是它们的创建时机。服务器 Sink 在通道配置时创建一次。客户端 Sink 在每次创建新代理时创建(每个代理可能有不同的 Sink 链)。因此,客户端 Sink 可能会被创建多次。
您的自定义 Sink(无论是客户端还是服务器端)都可以在运行时阻止其被添加到 Sink 链中。如果出于某种原因(在检查了实例的 customData
后),您的自定义 Sink 决定它不应成为链的一部分,它可以在其构造函数中抛出 ExcludeMeException
。提供程序将捕获此异常并采取相应措施。此外,如果您的自定义 Sink 决定它永远不再创建,它可以更具体。它不必每次调用构造函数时都抛出 ExcludeMeException
,而是可以向 ExcludeMeException
的构造函数提供 true,以表明它希望被永久排除。之后,提供程序甚至不会尝试创建提供程序的实例。
注释
excludeMePermanently
(ExcludeMeException
构造函数的参数)仅与客户端 Sink 相关。当服务器 Sink 抛出它时会被忽略。excludeMePermanently
基于每个提供程序运行。如果同一客户端 Sink 在配置文件中出现多次,例如在 HTTP 和 TCP 通道中,并且自定义 Sink 的 HTTP 通道构造函数抛出异常指定其被永久排除,那么它仅对 HTTP 通道被排除。提供程序仍将尝试为 TCP 通道创建此自定义 Sink 的实例。
获取其他 Sink 创建参数
您的自定义 Sink 在创建时可以获取额外信息。这是通过具有接受 ClientSinkCreationData
或 ServerSinkCreationData
类型参数的构造函数来实现的(具体取决于您的自定义 Sink 类型)。两者都派生自 SinkCreationData
,我在上一节中已介绍过。此构造函数具有其他任何构造函数的优先级。如果存在,它将是唯一被调用的构造函数。
ClientSinkCreationData
类包含以下字段:ConfigurationData
– 前面讨论过的 SinkProviderData
对象,channel – 创建 Sink 的通道,Url – 远程对象的 URL,以及 RemoteChannelData
– 关于服务器端通道的数据(如果适用)。您可以参考 .NET Framework SDK 文档中 IClientChannelSinkProvider.CreateSink
的参数以获取更多详细信息。
ClientSinkCreationData
类包含以下字段:ConfigurationData
– 前面讨论过的 SinkProviderData
对象,以及 channel – 创建 Sink 的通道。您可以参考 .NET Framework SDK 文档中 IServerChannelSinkProvider.CreateSink
的参数以获取更多详细信息。
回到我们的 Lame Encryption 示例,假设当目标对象位于“localhost”时,我们不需要加密。回顾以下构造函数(它也支持之前开发的功能)。EnhancedLameClientEncryptionSink
的新构造函数如下。
public EnhancedLameEncryptionClientSink(ClientSinkCreationData creationData)
: this((SinkCreationData)creationData)
{
Uri uri = new Uri(creationData.Url);
if (uri.IsLoopback)
{
throw new ExcludeMeException();
}
}
注释
- 由于
EnhancedLameEncryptionServerSink
(以及基础的LameEncryptionServerSink
)已经设计为支持加密和非加密通信,因此不需要进行任何修改。 - 第一个构造函数,即接受
SinkCreationData
作为参数的那个,现在是多余的(此处未列出,但仍然存在于代码中)。但是我保留了它,因为它已在前面的部分中介绍过。既然我已经保留了它,我就重定向到它,而不是再次编写从配置文件中检索增量值的代码。但是,出于效率原因,您通常应该首先决定是否抛出ExcludeMeException
,然后再执行额外的构造逻辑。
静态初始化
由于客户端自定义 Sink 可能会被创建多次,因此有时您可能希望您的类尽可能多地初始化静态信息。这样,您可以避免为每个新创建的实例处理相同的信息。通常,当您需要静态初始化时,您会在类中添加一个静态构造函数。您当然可以为您的自定义 Sink 采用这种方法。但是它有一个主要的缺点。如果您的静态构造函数抛出异常,该异常通常无法被捕获和处理。
这是我的解决方案。在您的自定义客户端 Sink 中定义以下方法。
public static void Init(SinkProviderData data, ref object perProviderState) { }
此方法保证在创建您的自定义 Sink 的任何实例之前都会被调用。您现在可以将对 RemotingConfiguration.Configure
的调用放在 try/catch 块中,并捕获此方法可能抛出的任何异常。此外,此方法会收到来自配置文件中 customData 元素的。注意:虽然此方法与客户端 Sink 关系更大,但它也适用于服务器 Sink。
重要:此方法与静态构造函数之间存在重大区别。如果您的自定义 Sink 在配置文件中出现不止一次,Init
方法将被调用多次,每次出现一次。在这种情况下,您应该避免将数据分配给静态字段,因为每次调用 Init 都会覆盖先前分配给这些字段的值。要解决此问题,请使用 perProviderState
。
perProviderState
允许您按提供程序保存数据。如果您的 Sink 在配置文件中出现多次,.NET Remoting 基础设施将创建多个 CustomClientSinkProvider
实例,每个实例一次。您可以利用此行为,将一个您选择的数据对象分配给 Init 方法的 perProviderState
参数。这样,您的对象将被保存在提供程序对象中,并且多次调用 Init(来自不同的提供程序实例)不会覆盖之前设置的数据。您的自定义 Sink 可以通过继承的 BaseCustomSink.PerProviderData
属性访问数据。
Lame Encryption 示例中并未演示 Init 方法。但是 SampleSinks 项目包含另一个示例:Credentials Sinks,它使用了此功能。Credentials Sinks 如下所述。* *
深入了解 ProcessRequest 和 ProcessResponse
ProcessRequest
和 ProcessResponse
的前三个参数的行为有所不同,具体取决于您的自定义 Sink 是服务器端还是客户端 Sink,以及它位于格式化 Sink 之前还是之后。以下是完整的分析。
ProcessRequest
客户端,格式化 Sink 之前
- message – 请求消息。
- headers – null。
- stream – null。请勿将此参数分配给另一个流。
客户端,格式化 Sink 之后
- message – 请求流消息。请勿修改消息,因为它已序列化。
- headers – 请求传输头部。
- stream – 请求流。
服务器,格式化 Sink 之前
- message – null。
- headers – 请求传输头部。
- stream – 请求流。
服务器,格式化 Sink 之后
- message – 请求消息。
- headers – 请求传输头部。
stream – null。请勿将此参数分配给另一个流。
ProcessReponse
客户端,格式化 Sink 之前
- message – 响应消息。
- headers – null。
- stream – null。请勿将此参数分配给另一个流。
客户端,格式化 Sink 之后
- message – null。
- headers – 响应传输头部。
- stream – 响应流。
服务器,格式化 Sink 之前
- message – 响应消息。请勿修改消息,因为它已序列化。
- headers – 响应传输头部。
- stream – 响应流。
服务器,格式化 Sink 之后
- message – 响应消息。
- headers – null。
stream – null。请勿将此参数分配给另一个流。
Credentials Sinks
本文中介绍的 Lame Encryption Sink 操作请求和响应的流。为完整起见,我还包含了 Credential Sinks,它们演示了基于消息的处理。
客户端 Sink 的任务是将用户名和密码添加到每次传出的通信中。服务器 Sink 会验证这些凭据,并在发现无效时抛出异常。服务器端从配置文件中检索凭据。客户端 Sink 也从配置文件中检索凭据。但是,不同的服务器甚至端口可以分配不同的凭据。
欢迎您在 SampleSinks 项目中查看 DemoCredentialServerSink
和 DemoCredentialClientSink
。
最后说明
我已尽一切努力使本文尽可能没有错误,代码尽可能没有 bug。但是,如果我遗漏了什么,我非常想知道。此外,对于 CustomSinks 库的进一步改进建议和一般性评论,都非常欢迎。
免责声明
本文及附带的代码按原样提供。您可以随意使用它(我正在成为一名诗人……)。您不得因阅读本文或使用代码而对您、您的公司、您的邻居或任何其他人造成的任何损害承担责任。您使用本文及附带代码的风险自负。
享受。