WCF 的通信选项 - 第三部分






4.82/5 (21投票s)
WCF 的通信选项 - 第三部分
第 3 部分简介
在第 1 部分中,我们定义了服务是什么,并创建了一个简单的 WCF 服务和客户端来演示在 WCF 下创建和使用服务的过程。第 2 部分继续介绍了一些可以使用 WCF 轻松实现的各种通信模式的示例。在本文中,我们将继续完成对异步通信的探讨,然后介绍 WCF 支持的第四种传输方式:消息队列。请注意,示例中提供的所有源代码都在第 1 部分的下载内容中。
七月 CTP 更新
我最近有机会安装了七月的 CTP。我编译了示例源代码,看看是否有任何意外。看起来 svcutil 发生了一些变化。并且有一些命名更改,如果您按原样编译示例源代码,将会产生一些语法错误。您应该能够轻松地识别和修复它们,因为如今 IntelliSense 的工作效果要好得多。
异步通信(续)
因此,在上篇文章中,我们基于 .Net 异步模式开发了一个异步通信的示例。该功能完全由客户端提供,并主要由 .Net 基础结构实现。现在,我们将提供相同的功能(异步通信),但这次我们将使用双工通信模式。在这种情况下,客户端和服务共同分担工作。其机制与我们看到的发布/订阅模式基本相同,只是在这里请求和响应之间存在一对一的关联。而在发布/订阅模式中,一个请求(订阅)会产生多个响应(发布)。并且对于发布/订阅模式,可以有任意数量的客户端订阅一个发布。但在两种情况下,请求都与响应断开连接。仅为说明起见,请求是一个消息,而响应是单独的消息。虽然每个消息都遵循基于 IsOneWay 属性设置的请求/回复模式。这是否让事情更不清楚了?好吧,我最好就此打住。
对于双工通信,客户端向服务发出请求。然后,服务将在未来某个时间将带有结果的消息发回给客户端。服务还通过提供回调接口定义来指定客户端需要“实现”的内容。并且在处理响应时,客户端的行为几乎就像一个服务。让我们通过定义 AGVController 的一个新版本(称为 AGVControllerD),该版本支持双工通信,以便我们可以检查这些要点。以下是修改后的服务代码。
[ServiceContract(Session = true, CallbackContract = typeof(IMoveToCallback))]
public interface IAGVControl
{
...
}
//What the client needs to implement
public interface IMoveToCallback
{
[OperationContract(IsOneWay = true)]
void MoveDone(int location);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class AGVControl : IAGVControl
{
private IMoveToCallback callback = null;
public AGVControl()
{
callback = OperationContext.Current.GetCallbackChannel();
}
...
public void MoveTo(int location)
{
//This takes a long time...
Thread.Sleep(5000);
//Send a response
callback.MoveDone(location);
}
...
}
您首先注意到的是 ServiceContract 现在指定了一个回调接口。服务需要这个接口才能知道调用什么。接下来是客户端必须实现的actual回调接口的定义。既然我们已经知道调用什么,我们只需要知道调用谁。这就是构造函数中语句的目的。服务对象的每个实例(我们指定了“每个会话”的实例化,因此 ServiceHost 将为每个客户端会话创建一个服务类的实例)都记住它正在与谁通信。这一点很重要,因为客户端假设服务在方法调用之间维护着某种状态(例如车辆)。然后,当适当的时候,服务就可以回电给客户端。
有一点需要注意,回调接口不限于单个方法。我们在这里拥有与编辑代理类时异步客户端相同的灵活性。在那里,我们只为一个方法实现了一对 Begin/End。在此示例中,我们可以将服务方法的任何或所有方法定义为也具有回调方法。为了从相同的角度比较这两个异步示例,我们只为 MoveTo 操作定义了一个回调。但我们当然也可以为每个服务方法定义一个回调方法。
编译服务 dll,然后对它运行 svcutil 以生成代理类。这次我们不指定“/a”选项。宿主与以前相同。您要做的就是将对 AGVController 的引用更改为 AGVControllerD(配置文件中的命名空间也一样)。
现在让我们构建双工客户端。主要区别在于,我们需要提供一个实现回调接口的类,以便服务能够回电给我们。svcutil 会自动将回调接口的定义包含在代理中。就像我们在发布/订阅示例中看到的那样,我们需要客户端쪽에某种宿主才能接收来自服务的消息。该功能由 InstanceContext 类提供,与以前一样。这是双工客户端特定的代码,其他所有内容都与异步客户端相同。
public partial class Form1 : Form
{
AGVControlProxy proxy = null;
InstanceContext siteAGV = null;
bool gotVehicle = false;
public bool gotResponse = false;
...
private void Form1_Load(object sender, EventArgs e)
{
siteAGV = new InstanceContext(new MoveCallbackHandler(this));
proxy = new AGVControlProxy(siteAGV);
}
...
}
public class MoveCallbackHandler : IAGVControlCallback
{
Form1 parent;
public MoveCallbackHandler(Form1 parent)
{
this.parent = parent;
}
public void MoveDone(int location)
{
parent.gotResponse = true;
}
}
如果您运行此版本的客户端(请确保服务正在运行),您应该会看到两个实现的结果相同。因此,如果服务提供了异步通信,那很棒,但即使它没有,您仍然可以通过少量代码和相应地修改代理来实现它。还有一点,如果需求合适,您可以在服务中提供两种通信形式,让客户端选择。
消息队列
WCF 支持的第四种通信传输机制是消息队列(公平地说,一些文献将点对点通信描述为另一种传输类型)。消息队列对 WCF 来说并不是什么新鲜事物。它一直是 Windows 的一部分,但并非所有版本都可用,我认为这不好。我认为消息队列是一种优秀的通信机制,适用于正确的应用程序。何时使用消息队列?当您需要它的时候!一个典型的情况是对于可能并非随时都能访问服务的应用程序。它们有时可能离线。
另一个应用可能是生产者/消费者模式,其中两者可能有不同的处理速度。也就是说,假设有多个数据生产者,每个生产者以不规律的周期产生数据。所有生产者可能同时发送数据,这可能会压垮消费者。因此,通过使用消息队列,我们实际上是在随时间分散处理数据。一个典型的例子可能是投票应用程序。投票数据可能会呈爆发式增长,因为您无法控制人们何时投票。换句话说,生产者可以随心所欲地做任何事情/随时做,消费者永远不会不堪重负(它只会花费更长的时间)。
消息队列的另一个重要用途是将两个不同的系统连接在一起。两个应用程序可能不仅在时间上和空间上分开,而且在平台和语言上也可以分开,但它们仍然可以使用消息队列进行通信。您可能会想:“服务就是为此而提供的”。但请记住,队列的另一端不一定是一个服务。不用说,队列的使用应该非常符合应用程序的要求。
为了完成 WCF 之旅,让我们来看一个消息队列的实现。该应用程序是一个操作员控制台实用程序,它显示构成系统的各种模块的状态。下图代表了一个物料处理系统,其中包含一些机器人、AGV、传送带等。系统中的每个模块都将其状态信息发送到一个中央监控应用程序,以便操作员可以随时了解每个模块正在做什么。或者,如果出了问题,他/她可以看到什么紧随故障之前。客户端模块本身可能运行在不同的位置,并且可能以不同的语言实现。
实际上,每个模块都将状态信息发送到一个队列,然后监控服务处理队列中的消息。正如您将看到的,客户端代码没有提供任何指示消息实际上是发送到队列的。冒着过度强调的风险……对我来说,这就是 WCF 的魅力。此外,由于消息队列的使用仅仅被定义为端点,将来系统可以更改为不使用队列而是直接调用服务,而客户端代码将完全不变。所有需要更改的将是配置文件,以指定不同的端点。
因此,消息队列的行为就像电子邮件,有人给您发消息,消息会暂时保存在某处,直到您去取。以前,如果您实现了消息队列,您有两种选择如何处理消息。您可以设置一个事件处理程序,当队列中有消息时,系统会调用您的委托;或者,如果您正在查找特定类型的消息,您可以轮询消息队列。在 WCF 中,使用消息队列是完全透明的。因为除了设置实际的队列之外,与其它传输类型没有语法上的区别。在客户端,您只需像往常一样进行方法调用。在服务端,您可以定义一个接口和一个实现该接口的类,就像您为任何其他传输类型所做的那样。这种传输与其他传输类型的唯一区别是,为了使用消息队列,它必须存在。好了,让我们看一些代码。
QState Monitor 是一个 MDI 应用程序,它为每个发送消息的模块创建一个窗口。来自同一模块的任何后续消息都将在同一个窗口中显示。消息只是两个字符串,第一个是模块的名称,第二个字符串是要显示的状态信息。下载中包含所有代码,所以我们只看 WCF 方面的内容。首先,让我们定义服务。
[ServiceContract]
public interface IQSMonitor
{
[OperationContract(IsOneWay = true)]
void ModState(string strMod, string strState);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class QSMonitor : IQSMonitor
{
QStateWnd parent = null;
public QSMonitor(QStateWnd parent)
{
this.parent = parent;
}
public void ModState(string strMod, string strState)
{
parent.ProcessMessage(strMod, strState);
}
}
如您所见,代码中没有任何内容表明正在使用消息队列。实际的消息队列可以以编程方式创建,也可以使用计算机管理控制台(在管理工具中)手动创建。如果您手动创建队列,则可以更改传输而根本不影响代码。这是启动服务的代码。
...
ServiceHost host = null;
QSMonitor handler;
public QStateWnd()
{
// To create the queue programmatically uncomment these lines.
// string queueName = ConfigurationManager.AppSettings["queueName"];
// if (!MessageQueue.Exists(queueName))
// MessageQueue.Create(queueName, false);
handler = new QSMonitor(this);
// Get base address for Msmq
string baseAddress = ConfigurationManager.AppSettings["baseAddress"];
host = new ServiceHost(handler, new Uri(baseAddress));
// Open the ServiceHostBase to create listeners and start listening for
// messages.
host.Open();
...
}
这里没有什么不同于我们之前使用的其他传输所需的代码。handler 是服务类。我们将 handler 传递给主窗口(QStateWnd)的引用,以便它可以在接收到消息时通知主窗口。为每条消息执行的功能实际上是在 QStateWnd 类中定义的,即显示消息。最后,我们只需要创建一个 ServiceHost 来宿主服务,就像以前一样。
其余代码涉及应用程序的机制,相当直接。当服务类接收到消息时,它只是将其传递给主窗口(MDI 框架)。MDI 框架类维护一个它们拥有的子窗口列表。我们使用该功能来跟踪哪些模块已经发送了消息。基本上,我们只是遍历子窗口列表并检查它们的标题栏文本。当创建 MDI 子窗口时,我们将模块名称分配给窗口标题。这样,我们就可以将发送消息的模块与特定窗口匹配起来。
就是这样,如果模块名称在任何当前窗口中都不存在,我们就创建一个新的子窗口;如果已经存在一个窗口,我们就将状态信息添加到该窗口。
最后,这是使用消息队列时配置文件看起来的样子。
<configuration>
<appSettings>
<!-- use appSetting to configure MSMQ queue name -->
<add key="queueName" value=".\private$\QSMonitor" />
<add key ="baseAddress" value="net.msmq:///private/QSMonitor"/>
</appSettings>
<system.serviceModel>
<services>
<service
name="QSMonitor">
<!-- Define NetMsmqEndpoint -->
<endpoint address="net.msmq:///private/QSMonitor"
binding="netMsmqBinding"
bindingConfiguration="Binding1"
contract="IQSMonitor" />
</service>
</services>
<bindings>
<netMsmqBinding>
<binding name="Binding1" exactlyOnce="false">
<security mode="None" >
</security>
</binding>
</netMsmqBinding>
</bindings>
</system.serviceModel>
</configuration>
关于上面的配置文件有一些注释,因为它非常强大。我没有过多提及绑定类,因为那里的东西太多了,而且 WCF 提供的默认类已经足够了。但是,您也可以创建自己的自定义绑定,以及自定义默认绑定类以满足您的需求。以编程方式提供该功能非常灵活,但并不令人意外。真正令人称道的是,在使用配置文件路由时,您也拥有相同的功能。在上面的文件中,我们指定了我们想要使用默认的 netMsmqBinding 绑定。您会注意到文件中还有一个额外的 bindings 部分,并且在端点定义中还有一个名为 bindingConfiguration 的额外属性。这里发生的情况是,我们正在说‘使用默认的 netMsmqBinding 绑定,但进行以下更改’。因此,如果某个特定绑定的默认选项不合适,您可以从配置文件中修改它!为什么这对我来说如此神奇?可能是因为我惊叹于简单的事物,也可能是因为它为我们的解决方案提供了更多一点的灵活性。如果我们可以在不重新编译的情况下更改某些内容,那就很方便了。如果已经部署,它就是救命稻草。最后,这些选项背后有大量的功能,而我们不必编写一行代码。
那么,为什么我需要修改默认行为呢?嗯,它不起作用。我尝试了几种选项,然后在网上搜索了一下。事实证明,此绑定默认启用了安全。那时安全对我来说不是必需的(我相信我很快就必须面对这个问题)。无论如何,我只需要覆盖默认设置来关闭安全。我们大多数时候就是这样学习新事物的,不是吗?通过导致发现的错误。
总之,回到当前的示例。在客户端端,只需要一个简单的代理和配置文件中的相应条目。由于我们没有服务 dll,并且我们的服务没有 MEX 端点,我们唯一的选择就是手动编写自己的代理。顺便说一句,您甚至可以使用 ChannelFactory 类动态创建代理。通常,WCF 中不止一种做事方式。这有什么灵活性?
快速提示。如果您确实编辑了代理,请确保您的参数名称在两端都匹配。参数是通过名称标识的,所以即使有最轻微的拼写错误,您也会花一些时间来查找为什么消息没有被处理。即使它们被正确发送并且没有异常。使用消息队列时,一个免费的调试选项就是消息队列本身!如果您不运行服务,您可以检查消息队列中的消息。对于其他传输,您需要一些其他实用工具才能看到消息在电线上传输。
这是客户端所需的代理和配置文件。下载源代码中有一个示例客户端。
[ServiceContract]
public interface IQSMonitor
{
[OperationContract(IsOneWay = true)]
void ModState(string strMod, string strState);
}
public interface IQSMonitorChannel : IQSMonitor,
System.ServiceModel.IClientChannel
{
}
public partial class QSMonitorProxy : System.ServiceModel.ClientBase,
IQSMonitor
{
public QSMonitorProxy()
{
}
public void ModState(string strMod, string strState)
{
base.InnerProxy.ModState(strMod, strState);
}
}
<configuration>
<system.serviceModel>
<client>
<endpoint name=""
address="net.msmq:///private/QSMonitor"
binding="netMsmqBinding"
bindingConfiguration="Binding1"
contract="IQSMonitor" />
</client>
<bindings>
<netMsmqBinding>
<binding name="Binding1" exactlyOnce="false">
<security mode="None">
</security>
</binding>
</netMsmqBinding>
</bindings>
</SYSTEM.SERVICEMODEL>
</configuration>
如果您想尝试一下,请对服务和客户端的配置文件进行以下更改。
<add key ="baseAddress" value="net.pipe:///QSMonitor"/>
...
<endpoint address="net.pipe:///QSMonitor"
binding="netNamedPipeBinding"
contract="IQSMonitor" />
...
重新启动服务和客户端。您应该不会看到结果有任何差异,但幕后发生的事情却发生了很大变化。
嘿,银箭向前冲!
好了,这就是 WCF 的所有内容,足以让您变得稍微危险。还有很多内容有待探索,包括通信的安全方面、所有绑定选项、点对点和广播消息,甚至可能定义自定义传输。您永远不知道什么时候会出现一个特殊的要求,它就是不符合常规。所以,下次再见,Kemo Sabe...