WCF 的 NullTransport






4.91/5 (46投票s)
2007年10月1日
12分钟阅读

194724

1743
本文描述了用于Microsoft Windows Communication Foundation (WCF)模型的自定义进程内传输的设计、实现和用法。
目录
注意:工作流测试用例需要安装.NET Framework 3.5 (Orcas) Beta 2 版本。
特点
- 输入/输出(单向)
- 请求/回复
- 双工
- Session
- 事务性
- 同步和异步连接方式
- 无编码器
引言
Microsoft Windows Communication Foundation (WCF) 代表了一个逻辑连接模型,其中服务与其使用者之间的连接被抽象为物理传输层之上的通道堆栈。基于连接两端的绑定堆栈元素和物理传输,我们可以根据消息交换模式 (MEP) 来定义消息如何在业务层之间流动。请注意,业务层不需要相互了解,例如它们是如何托管、定位和连接的,因此我们可以说,业务层是逻辑连接的。
由客户端(代理)和服务表示的业务连接层可以位于同一个应用程序域中,或者位于同一进程内的不同应用程序域中,也可以位于进程外或跨机器。在应用程序架构中拥有逻辑连接模型可以将其业务层与由元数据驱动的连接隔离开来。
WCF 模型引入了多种常见的传输和绑定,用于通过 TCP、HTTP、MSMQ、Pipe 等不同的物理传输创建同步和异步连接,并支持输入/输出、请求/回复、会话、双工等各种消息交换模式。它是一个出色的开放式连接范例,能够根据业务需求定制和扩展所有元素。
基于物理传输,业务可以存在于同一层级,也可以根据面向服务架构 (SOA) 的原则跨越企业网络进行解耦。
本文重点介绍同一应用程序域内的 WCF 连接模型。当前版本的 .netfx 3.0 以及即将发布的 3.5 版本 (Orcas) 使用命名管道进行服务与其使用者之间的进程内通信。
下图显示了 netNamedPipeBinding WCF
连接

业务层之间的逻辑连接被映射到 WCF 模型,并通过其服务契约进行描述。此描述代表连接的元数据,如地址、绑定和契约 (ABC)。基本上,客户端和服务使用相同的通道堆栈,由协议、编码器和传输层绑定。客户端通道通过代理(使用透明代理模式)连接到业务层,在服务方面,分派器将调用服务操作方法。有关此出色通信范例的更多详细信息,请参阅 MSDN 文档和.NET 论坛。我假设您已经阅读了文档并具有 WCF 编程的工作知识和经验。
正如我之前提到的,本文重点介绍进程内通信,因此我们将继续以此为重点。当客户端调用透明 CLR 代理上的服务操作时,该操作会被协议层(取决于消息交换模式)映射到消息对象,然后将消息传递给编码器进行序列化。
传输层将通过特定的物理介质(在我们的例子中是运行在内核模式下的本地命名管道,可以在同一台机器上的任何进程中访问)在应用程序域上传输该二进制流或格式化的文本。服务端的编码器必须将传入的流/文本解码为消息对象,并将其传递给协议通道。最后,分派器的操作调用器将调用服务实例上的方法。
到目前为止一切顺利,netNamedPipeBinding
允许通过内核模式下的本地命名管道资源连接和传输二进制消息对象,在服务和客户端之间以快速可靠的同步/异步方式进行通信。那么,重点是什么?
重点在下图所示:如果客户端和服务位于同一个应用程序域中。我们是否需要外出再返回同一个应用程序域,通过进程间内核资源来处理域内通信?
下图可以回答关于进程内连接的问题

如您所见,连接的物理层由 NULL 传输表示,它将克隆的消息对象直接传递给服务协议层。这个逻辑类似于路由器,当输出通道将消息路由到正确的输入通道时。请注意,此 NULL 连接在服务和客户端之间是完全透明的,并且服务在具有不同 ABC 的绑定方面没有任何限制。
例如:一个具有两个端点的服务,假设第一个端点是 netTcpBinding
,而另一个端点是自定义绑定,其 Null Transport 可以非常高效地处理公共和私有连接。
NullTransport
用法的另一个例子是即将到来的 netfx 3.5 版本(目前处于 beta2),其中 WCF 和 WF 模型集成到一个通用的 WorkflowService
模型中。该模型通过新的 Receive 和 Send 活动,能够在上下文中与基于服务契约的工作流进行通信。封装在类型/XOML 工作流中的业务可以在逻辑模型中根据连接元数据进行编排,而无需了解它们实际托管的位置。
下图显示了 WCF 和 WorkflowService
在应用程序域内的连接。在这种情况下,服务和客户端逻辑上连接到由 NULL Transport 层表示的应用程序域总线。

WorkflowServices
是分布式工作流模型的重要一步,其中工作流可以在内部或公共企业服务总线上以上下文消息交换模式调用另一个工作流。
以上就是 Null Transport 的介绍,希望您能理解在同一个应用程序域内,当正确地放置业务层时,可以使用与跨层通信相同的通信模型来进行进程内通信。
我假设您了解 WCF 及其可扩展性;因此,我将更侧重于传输层的概念和实现,而不是自定义通道的层次结构。好的,让我们从概念开始,然后跟进它的实现。
概念和实现
NullTransport
的概念基于根据 EndpointAddressMessageFilter
数据将克隆的输出消息对象路由(分派)到特定的侦听器。每个打开的侦听器都会将其 EndpointAddressMessageFilter
订阅到存储在进程数据槽中的 MessageFilterTable
。基于此键,MessageFilterTable
将返回侦听器及其私有的 InputQueueChannel
的引用,用于入队消息对象。一旦消息到达输入异步队列,通道将像由编码器层生成的消息一样处理它。
下图显示了具有四个与 MEP 相关的侦听器的 NullTransport
概念。

构造 Listener
时,会为完整的本地终结点地址创建 EndpointAddressMessageFilter
,然后将在 AcceptChannel
过程中使用它。
以下代码片段显示了 NullInputChannelListener
实现的这些部分。
public NullInputChannelListener(NullTransportBindingElement element,
BindingContext context): base(context.Binding)
{
_element = element;
_context = context;
Uri listenUri = new Uri(context.ListenUriBaseAddress,
context.ListenUriRelativeAddress);
_localAddress = new EndpointAddress(listenUri);
_filter = new EndpointAddressMessageFilter(_localAddress);
}
protected override IInputChannel OnAcceptChannel(TimeSpan timeout)
{
if (base.State == CommunicationState.Opened)
{
if (_currentChannel != null)
{
// we are supporting only one channel in the listener
_waitChannel.WaitOne(int.MaxValue, true);
lock (ThisLock)
{
// re-open channel
if (_currentChannel.State == CommunicationState.Closed &&
base.State == CommunicationState.Opened)
{
_currentChannel =
new NullInputChannel(this, _localAddress);
_currentChannel.Closed +=
new EventHandler(OnCurrentChannelClosed);
}
}
}
else
{
lock (ThisLock)
{
// open channel at first time
_currentChannel = new NullInputChannel(this, _localAddress);
_currentChannel.Closed +=
new EventHandler(OnCurrentChannelClosed);
NullListeners.Current.Add(_filter, this);
}
}
}
return _currentChannel;
}
NullListener.Current
是一个 static
方法,用于将特定的 Listener
订阅/取消订阅到 MessageFilterTable
,并将消息分派到 Listener
的 InputQueueChannel
。
一旦 Listener
被订阅,消息就可以被分派。以下代码片段显示了从 OutputChannel
发送消息。
public void Send(Message message, TimeSpan timeout)
{
// double check
base.ThrowIfDisposedOrNotOpen();
// add remote address to the message header
base.RemoteAddress.ApplyTo(message);
// create buffered copy
MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
Message message2 = buffer.CreateMessage();
// dispatch to the listener
ThreadPool.QueueUserWorkItem
(new WaitCallback(this.DispatchToListener), message2);
}
protected virtual void DispatchToListener(object state)
{
try
{
NullListeners.Current.Dispatch<NullInputChannelListener>
(state as Message);
}
catch (Exception ex)
{
(state as Message).Close();
}
}
如您所见,克隆的消息以异步方式发送到分派器,而不等待其响应。这是输入和输出通道表示的“即发即忘”消息交换模式的设计。
构建自定义 WCF 通道/传输是一项直接的任务,分为多个层,具有接口管道模式和一些位于 CommunicationObject
基类中的公共行为。
下图显示了用于消息交换模式(如输入/输出、输入会话/输出会话、请求/回复和请求会话/回复会话)的自定义 NullChannel
的基本样板。

样板的第一部分与自定义传输插件化到绑定集合有关。NullTransportElement
必须在配置文件中的 <bindingElementExtensions>
元素中添加,或者在实际使用此自定义绑定之前以编程方式添加。一旦我们在扩展中拥有了自定义绑定,它就可以被终结点识别,并可以调用如上图所示的管道模式。
NullTransportBindingElement
负责根据服务契约构建 Listener
(服务)和 Factory
(代理)通道。例如:如果服务契约有一个单向操作,那么将应用输入/输出消息交换模式来构建通道。
下图显示了 NullChannel
的类图。这些通道由 Factory
和 Listener
层启动。

简单的 MEP,例如将消息单向发送到输入通道,可以通过将克隆的消息对象直接入队到输入通道队列中来完成,实现即发即忘——请参阅上面的代码片段。管道层由 IOuputChannel
和 IInputChannel
模式表示。
为了在请求/回复 MEP 中描述的请求上接收响应,管道层必须由 IRequestChannel
和 IReplyChannel
接口以及 RequestContext
对象驱动。
RequestContext
对象代表了在请求和回复任务之间处理消息对象的“载体”,用于它们的同步和消息传递。下图显示了 NullTransport
对 RequestContext
抽象类的实现。

NullAsyncRequestContext
对象旨在为 WCF 通道协议的内部通道层(如 NullRequestChannel
和 NullReplyChannel
,以及 NullRequestSessionChannel
和 NullReplySessionChannel
)提供管道。RequestContext
对象可以由双方异步或同步(以阻塞方式)处理。
以下代码片段显示了 NullRequestChannel
中的 Request
实现。
public IAsyncResult BeginRequest(Message message,
AsyncCallback callback, object state)
{
return this.BeginRequest
(message, base.DefaultSendTimeout, callback, state);
}
public IAsyncResult BeginRequest(Message message, TimeSpan timeout,
AsyncCallback callback, object state)
{
base.ThrowIfDisposedOrNotOpen();
base.RemoteAddress.ApplyTo(message);
NullAsyncRequestContext request =
new NullAsyncRequestContext(message,timeout,callback,state);
this.DispatchToListener(request);
base.PendingRequests.Add(request);
return request;
}
public Message EndRequest(IAsyncResult result)
{
return NullAsyncRequestContext.End(result);
}
public Message Request(Message message)
{
return this.Request(message, base.DefaultSendTimeout);
}
public virtual Message Request(Message message, TimeSpan timeout)
{
base.ThrowIfDisposedOrNotOpen();
base.RemoteAddress.ApplyTo(message);
NullAsyncRequestContext request =
new NullAsyncRequestContext(message, timeout);
base.PendingRequests.Add(request);
try
{
ThreadPool.QueueUserWorkItem
(new WaitCallback(this.DispatchToListener), request);
return request.WaitForReply();
}
finally
{
base.PendingRequests.Close(request);
}
}
protected virtual void DispatchToListener(object state)
{
try
{
NullListeners.Current.Dispatch<NullReplyChannelListener>
(state as NullAsyncRequestContext);
}
catch (Exception ex)
{
(state as NullAsyncRequestContext).Abort(ex);
}
}
在 BeginRequest
的情况下,该方法将创建一个 RequestContext
对象,并带有异步参数,这些参数会被分派到特定的 ReplyChannel
,而无需等待其回复。当服务操作完成后,会通过调用以下方法将回复消息发送到 RequestContext
。
public override void Reply(Message message, TimeSpan timeout)
{
lock (ThisLock)
{
ThrowIfInvalidReply();
if (message != null)
{
MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
_response = buffer.CreateMessage();
}
else
{
_response = null;
}
_replySent = true;
if (_waitForReply != null)
{
_waitForReply.Set();
}
}
if (_callback != null)
{
// call callback for reply is done
_callback(this);
}
}
上述方法的责任是克隆消息对象并调用异步回调,以指示操作已完成并且响应已准备好在 RequestContext
对象中。此过程由 EndRequest
方法完成,其中将回复消息返回给请求者(具有匿名 ReplyTo
地址的 NullRequestChannel
)。
下图显示了被 MessageInspector
捕获的请求和回复消息,用于 <reliableSession>
在 <nullTransport>
绑定之上。

测试
下图显示了 NULL Transport 实现的解决方案,包括 .netfx 3.0 和 3.5 版本的测试项目。请注意,.netfx 3.5 的解决方案仅用于 WorkflowService
测试用例,并且需要安装 .netfx 3.5 版本(目前是 beta 2)。此解决方案引用了在 netfx 3.0 版本下构建的 NullChannelLib.dll
程序集。
如您所见,NullChannelLib
项目包含多个文件,这些文件位于基于通信层和 MEP 的文件夹中。

为了测试目的,每个控制台测试应用程序都有自己的配置文件。下图显示了 nullTransport
自定义绑定的配置示例。请注意,使用此传输需要 bindingElementExtension
。

可以通过自定义 NULL Transport 的消息交换模式,使用 Test_Duplex
、Test_Session
、Test_Tx (Transaction)
和 Test Workflow
等测试项目单独进行测试。解决方案还包含一个 Logger
,用于在控制台屏幕上显示 WCF 消息。
请注意,配置文件包含用于跟踪 WCF 模型中消息的诊断配置。请在配置文件中查找此跟踪的补丁,并使用 \Microsoft SDKs\Windows\V6.1\Bin\SvcTraceViewer.exe 实用程序来显示跟踪日志消息。
Test_Workflow
我选择此测试来描述 NULL Transport 在同一个应用程序域内的能力,其中 WCF 模型用于连接位于不同层级的业务对象。控制台应用程序代表了位于工作流和服务中的业务对象的 UI 和主机进程。通过配置文件,可以根据应用程序的需求,通过自定义 net.null
传输或其他传输来连接这些层级。

WorfklowService
中的第一个 ReceiveActivity
允许创建工作流实例,并且其 instanceId
将在有效会话期间在上下文中流动,因此来自控制台程序的第二个请求消息将在下一个 ReceiveActivity
期间被分派。
下图显示了第一个 ReceiveActivity
的属性。

下图所示的目的是与第一个 ReceiveActivity
进行消息交换。请求/回复消息交换模式通过将工作流实例 ID 的上下文头传递回调用者来演示。此外,屏幕上显示了工作流运行时事件,如已创建、已启动、已空闲和已持久化。

请注意,WorkflowService
会自动为您创建工作流实例 ID。当前版本的 netfx 3.5 中没有选项可以在第一次调用 WorkflowService
的请求中传递带有特定 ID 的上下文。但是,我实现了一个小型的解决方法(CreateWorkflowAttribute.cs
),基于自定义操作属性 [CreateWorkflow
]——请参阅以下代码片段。
[ServiceContract(SessionMode = SessionMode.Required)]
public interface ITest
{
[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
void Ping(int value);
[OperationContract(IsInitiating = true)]
[TransactionFlow(TransactionFlowOption.Allowed)]
[CreateWorkflow]
string Ping2(int value);
}
现在,客户端可以创建 instanceId
并通过请求消息将其传递给 WorkflowService
。以下代码片段显示了创建出站消息的上下文头的示例。
using (OperationContextScope scope =
new OperationContextScope((IContextChannel)channel))
{
string instanceId = Guid.NewGuid().ToString();
Dictionary<XmlQualifiedName, string> dic =
new Dictionary<XmlQualifiedName, string>();
dic.Add(new XmlQualifiedName("InstanceId",
"http://schemas.microsoft.com/ws/2006/05/context"), instanceId);
ContextMessageProperty mp = new ContextMessageProperty(dic);
OperationContext.Current.OutgoingMessageProperties.Add
("ContextMessageProperty", mp);
string response = channel.Ping2(loop);
// ...
}
出站请求消息将如下面的代码片段所示。
<s:Envelope
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/ITest/Ping2</a:Action>
<a:MessageID>urn:uuid:ce8e478c-ab6a-401d-958b-d1d3de2f972d</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<Context xmlns="http://schemas.microsoft.com/ws/2006/05/context">
<InstanceId>044a3c30-3558-47de-8647-2c0d9b4f9359</InstanceId>
</Context>
<a:To s:mustUnderstand="1">net.null:///gaga</a:To>
</s:Header>
<s:Body>
<Ping2 xmlns="http://tempuri.org/">
<value>0</value>
</Ping2>
</s:Body>
</s:Envelope>
Remoting 与 namedPipeTransport 与 nullTransport 的比较
WCF 模型中快速的标准传输是通过 namedPipeTransport
,使用在内核模式下运行的本地命名管道。如前所述,消息必须通过此传输进行编码。Test_Session
程序可用于比较命名管道和 null 传输之间的性能(在更新配置文件以支持 namedPipeTransport
后)。
在我的 Dell 多处理器(4 核)机器上,null 传输比管道更快,得分为 **2.462 毫秒到 3.062 毫秒。** 我还构建了一个类似的远程对象测试,结果表明 null 传输比 TCP 远程通道快约 8%。请注意,此测试针对非常小的消息体——请参阅 ITest
合约。对于大型消息,null
传输的性能会更好,因为通道中没有编码器层。
结论
本文描述了不使用编码器层的 WCF 进程内传输。这种高效的绑定使得在同一应用程序域中的业务层之间能够使用通用的通信 WCF 范例。通过配置文件;可以轻松地通过管理方式更改连接性以跨越边界。即将推出的新 .netfx 3.5 版本与面向连接系统的通用模型以及服务后面的工作流相结合,是 NullTransport
绑定的理想选择,其中业务模型可以根据业务需求在单个或多个进程中逻辑运行。