WCF 4.0 进阶系列 – 第十四章 检测服务和路由消息(第三部分)
实现路由
路由适用于处理与监测服务稍微不同的场景。
有时候需要从一个服务推送消息至另一个完全不同的服务以处理该消息。比如,当客户端程序发送请求至企业内部不同的WCF服务,但是所有这些请求实际上都首先通过前端的服务,该服务相当于WCF服务的防火墙。前端服务可以运行在企业外围网络的计算机上,而实际上处理请求的WCF服务可以寄宿在位于企业内部受保护的网络中。前端服务可以实现一个路由机制,通过检查消息的行为或地址以推送请求到真正的服务;该项技术就是基于地址的路由。前端服务还可以过滤消息,如果消息是非法请求那么阻塞该消息,消息过滤的效果取决于前端服务上所实施的智能检查的程度。
另外一套类似的机制就是基于消息的内容路由消息;这项技术就是基于内容的路由。比如,如果你寄宿一个商业服务,你可能根据用户支付的费用为用户提供不同的服务。高级用户可以向高性能的服务发送请求以快速地获取响应,而普通用户只能从较低的性能的服务得到响应。所有的用户均使用相同客户端程序向相同的前端服务发送请求,但前端服务检查消息的某个方面,比如发送请求的用户身份,然后根据请求身份消息推送消息到对应的目的地。
前端服务还可以提供其他特性,比如负载均衡。客户端的请求达到单个前端服务,前端服务使用负载均衡算法分发请求到真正运行WCF服务的服务器。
WCF提供两个主要机制实现路由,选择哪一个机制取决于需求的复杂程度。System.ServiceModel.Routing命名空间下的RoutingService类通过检查消息内容和消息地址,为路由消息到其他的服务提供了配置信息。相似地,如果需要实现更动态的或低级别的方式(如同负载均衡路由器的需求),你可以基于条件(比如当前服务的负载)手动地路由消息。
在研究如何使用RoutingService之前,有必要解释一下当WCF服务从客户端接收到消息后到底发生了什么,以及如何使用这些信息实现自定义的路由服务。
手动路由消息
一个服务可以对外提供多个端点,每个端点都配置了相同的或者不同的服务合约。当WCF服务接收到一条消息,它必须检查该消息以决定哪个服务端的应当处理该消息。你可以自定义WCF选择端点的方式,这就为你提供了在服务内部更改WCF路由消息的方式。
再次回顾ChannelDispatcer和EndpointDispatcher对象
在第十一章"编写代码控制配置和通信"中,你已经了解到服务端的WCF运行时为每个不同的地址和绑定的组合创建一个通道堆栈,然后使用该堆栈与服务进行通讯。每个通道堆栈都有一个ChannelDispatcher对象,并且该对象对应一个或者多个EndpointDispatcher对象。ChannelDispatcher对象的目标是决定哪一个EndpointDispatcher对象应该处理消息。EndpointDispatcher对象的角色是转化消息请求为方法的调用并调用服务对应的方法。
服务对外暴露的每个地址和绑定的组合可被多个端点共享。比如第六章ProductsServiceLibrary解决方案中的ProductsServiceHost项目中,为服务合约Products.IProductsService和Products.IProductsServcieV2定义了下列的服务和端点:
<services>
<service name="Products.ProductsService">
<endpoint address="http://localhost:8010/ProductsService/Service.svc"
binding="ws2007HttpBinding" bindingConfiguration="" name="WS2007HttpBinding_IProductsService"
contract="Products.IProductsService" />
<endpoint address="http://localhost:8010/ProductsService/Service.svc"
binding="ws2007HttpBinding" bindingConfiguration="" name="WS2007HttpBinding_IProductsService"
contract="Products.IProductsServiceV2" />
</service>
</services>
<service name="Products.ProductsService">
<endpoint address="http://localhost:8010/ProductsService/Service.svc"
binding="ws2007HttpBinding" bindingConfiguration="" name="WS2007HttpBinding_IProductsService"
contract="Products.IProductsService" />
<endpoint address="http://localhost:8010/ProductsService/Service.svc"
binding="ws2007HttpBinding" bindingConfiguration="" name="WS2007HttpBinding_IProductsService"
contract="Products.IProductsServiceV2" />
</service>
</services>
注意上述配置定义了两个端点,但是它们共享相同的地址和绑定组合;唯一的区别是这两个端点的服务合约不一样。上述配置将导致WCF运行时创建单个通道堆栈和ChannelDispatcher对象。但是通道堆栈可能关联两个的端点;每个服务合约使用一个服务端点。因此,WCF运行时为通道堆栈创建两个EndpointDispatcher对象,并将这两个对象添加到与ChannelDispatcher对象关联的 EndpointDispatcher对象集合中。如果ProductsServiceHost项目还为ProductsService提供TCP端点,那么WCF运行时将创建两个通道堆栈(一个用于HTTP端点,另外一个用于TCP端点),每个通道都有用自己的ChannelDispatcher对象。TCP端点拥有自己的EndpointDispatcher对象。
下图展示了端点,通道堆栈和分发对象之间的关系
当服务从通道堆栈接收到一条消息,通道堆栈顶部的ChannelDispatcher对象查询与之关联的EndpointDispatcher对象以决定哪一个端点可以处理该条消息。如果没有发现对应的端点,WCF运行时将触发一个UnknownMessageReceived事件。
EndpointDispatcher对象与过滤器
EndpointDispatcher对象如何指明其是否可以处理一条消息? 端点分发器对外提供可供ChannelDispatcher查询的两个属性AddressFilter和ContractFilter。AddressFilter属性是EndpointAddresMessageFilter类的一个实例变量。EndpointAddressMessageFilter类提供一个名为Match的方法,该方法接收一个消息对象为输入参数并返回一个Boolean值以指明EndpointDispatcher对象能否识别消息头部中包含的地址。ContractFilter属性是ActionMessageFilter类的一个实例变量。该类同样提供一个Match方法,该方法接受一个消息对象为输入参数,并返回一个boolean值以指明EndpointDispatcher对象能否处理消息头部中包含的行为。请记住消息头部包含的行为用于识别EndpointDispatcher所调用服务实例中接受请求的方法。在内部,ActionMessageFilter对象包含一个动作表,该表用于确认一个方法是否匹配,进行匹配时把该方法与表中所保存的行为逐个进行比较。其结果是要么发现一个匹配的行为,要么检查完所有行为但没有匹配的行为。
上述两个过滤器的Match方法都必须对向其发送消息的ChannelDispatcher对象返回True。同样还有可能多于一个EndpointDispatcher对象可以处理消息。在这种情况下,EndpointDispatcher类提供FilterPriority属性。该属性返回一个整数类型。一个EndpointDispatcher对象可以与另外一个EndpointDispatcher对象进行比较然后返回一个较高或者较低的值。如果两个都匹配的端点返回同样的优先级别值,那么WCF运行时抛出MuitipleFilterMatchesException异常。
基于服务配置文件中端点的定义,WCF运行时为每个ChannelDispatcher对象创建EndpointAddressFilterMessage和ActionMessageFilter对象。但是你可以重写这些过滤器--这需要使用自定义的地址和行为表创建对象实例,并在服务运行时之前插入到WCF运行时中。这种实现方式需要你创建自定义的行为,关于如果创建自定义行为请查考第十一章。
默认情况下,EndpointDispatcher调用服务合约中行为对应的方法。但是,你可以修改EndpointDispatcher处理一个请求操作的方式:首先创建一个实现IDispatchOperationSelector接口的类;然后将该类赋值给DispatcherRuntime对象的OperationSelector属性。因为EndpointDispatcher对象的DispatcherRumtime属性引用DispatcherRuntime对象。接口IDispatchOperationSelector包含了方法SelectionOperation。你可以使用SelectionOperation方法来检查消息并返回EndpointDispatcher对象应调用的方法。如果你希望手动控制哪一个分发机制进行工作那么上述方式非常有用。
总而言之,分发机制提供了高度定制化的机制以确定哪一个端点应该处理消息。你可以使用它来构建服务以透明地路由消息至其他的服务。
下图展示了上述的关系:
路由消息至其他服务
在WCF中,构建一个接受特定的消息服务并发送消息到另外一个服务处理进行处理,是相当简单的。你所需要做的是使用目标服务的镜像服务合约定义一个前端服务。前端服务中定义的方法能执行所需的预处理,比如检查发送请求的用户身份或者检查发送的数据,然后将这些请求推送至合适的目标服务。
但是,创建一个通用的、可接受任何消息的服务;而且该服务还可以路由消息到运行在其他计算机上的其他服务。那么要构建该服务则需要考虑更多。下面三点就是你需要处理的问题:
- 服务合约。WCF服务描述了可由服务合约执行的操作。如果一个服务接受消息,那么这些消息必须能够被一个或多个EndpointDispatcher对象的ContractFilter识别;然后将这些消息推送至实现了与目标服务相同的服务合约的服务。当路由消息到单个服务时是非常容易完成地,当你为多个不同的服务创建一个前端服务时,路由消息就会迅速地变得难以掌控,这是因为前端服务必须实现所有服务的服务合约。
- 消息内容。这个问题与上述服务合约相关。当一个前端服务必须实现大量服务的服务合约时,它还必须实现这些服务所使用的数据合约,这些数据合约用以描述数据结构如何序列化成消息体部分。同样地,这也会迅速地变成一件耗费精力的任务。
- 消息头的内容。除了消息体之外,一条消息还包含消息头。这些消息头包含的信息包括:加密密钥,事务标识,以及其他一些控制数据流和管理消息一致性的项目。前端服务必须小心地管理这些信息以使这些信息对发送请求的客户端和接受服务处理请求的服务是透明的。
幸运的是,已经有很多好的解决方案来解决上述的一些问题,你将在后续的练习中研究这些解决方案。在这些练习中,你将看到如何为ShoppingCartService服务创建一个简单的负载均衡路由器。你将运行两个ShoppingCartService服务实例,负载均衡路由器将分发来自客户端的请求到两个服务实例。负债均衡路由将实现一个简单的算法,发送交替的请求到ShoppingCartService服务。尽管练习中的三个服务运行在同一台电脑上,安排它们运行在不同的机器上同样也很简单,并且这样并允许使用不同的处理器扩展负载。
你将重新熟悉ShoppingCartService服务并修改该服务运行在传统的因特网环境中。
练习:回顾持续性ShoppingCartService服务
1. 使用Visual Studio,打开\WCF\Step by step\Chapter14\LoadBalancingRouter文件夹下的ShoppingCart.sln文件。
该方案包含修改后的第七章的持续性服务ShoppingCartService项目,ShoppingCartServiceHost项目,和ShoppingCartGUIClient项目。
2. 打开ShoppingCartService项目下的IShoppingCartService.cs文件,并检查该文件。回顾第七章,ShoppingCartService服务实现了AddItemToCart,RemoveItemFromCart,GetShoppingCart和Checkout操作。
3. 打开ShoppingCartService.cs文件并查看该文件的代码。请注意服务使用了PerSession实例模式并标注了DurableService特性。会话状态在两次调用之间保存到WCFPersistence SQL Server数据库。
4. 打开ShoppingCartHost项目的Programm.cs文件。这是服务寄宿程序。它所做的就是使用ServiceHost对象启动服务运行,然后等待用户按下ENTER键关闭宿主程序。
5. 打开ShoppingCartHost项目的app.config文件。请注意服务的宿主创建一个HTTP端点使用http://localhost:90000/ShoppingCartService/ShoppingCartService.svc地址。在这个版本的应用程序中,该端点使用basicHttpContextBinding绑定,该绑定使用默认的配置。在因特网环境中,你很可能实现了传输级别的安全以保护消息在客户端和服务之间的传输。使用WCF,你可以使用基于HTTPS的basicHttpBinding绑定配置传输级别安全。basicHttpContextBinding绑定是basicHttpBinding的意见简单扩展,它把客户端程序希望与之通讯的会话实例的ID作为Web请求头的Cookie在网络中传输。
6. 打开ShoppingCartGUIClient项目的app.config文件。验证客户端程序使用的端点与服务有相同的URI和绑定配置。
7. 在非调适模式下运行解决方案,以使你熟悉客户都程序。
在Shopping Cart GUI客户端窗口,在产品编号处输入WB-H098,然后点击添加商品。经过短暂的延时后,一个水瓶将添加到购物车并显示在客户端窗口中。输入BK-M38S-46,再次点击添加商品按钮,你将在购物车中发现新添加的银色山地自行车。
8. 关闭Shopping Cart GUI客户端窗口,并且关闭服务宿主控制台窗口。
到目前,你已经拥有一个版本的可供客户端程序直接连接的ShoppingCartService。下一步就是运行服务的多个实例并创建另外一个服务路由消息,根据路由服务实现的负载均衡算法,把客户端路由消息到这些服务实例的其中一个实例。在下面的练习中,你将发送逐个发送请求到两个服务实例。
练习:创建ShoppingCartRouter服务
1. 使用WCF服务类库模板,添加一个新的项目到ShoppingCart解决方案。该项目名为ShoppingCartServiceRouter,存放位置为\WCF\Step by step\Chapter14\LoadBalancingRouter文件夹。
2. 在ShoppingCartServiceRouter项目中,重命名Service1.cs为Router.cs,并重命名IService1.cs为IRouter.cs。并允许Visual Studio更新与Service1有关的引用。
3. 打开IRouter.cs文件,然后添加下面的代码到文件的顶部。
4. 删除IRouter接口前的注释,并添加如下的特性。
5. 在IRouter接口内,删除GetData和GetDataUsingDataContract操作的定义。然后添加下面的操作:
理解上面的代码而不是查看服务合约是理解路由是如何工作的关键。
在前面的讨论中,你已经了解到设计一个通用的前端服务推送消息到其他服务时,需要关注服务合约和消息内容等问题。服务合约定义了服务可以处理的操作。在常规环境下,一个操作的WSDL描述由Namespace和Name属性组成,这两个属性来自ServiceContract特性的Namespace和Name属性,它们和操作的名字一起生成一个唯一标识符,这个标识为就是我们所说的行为(action),它定义了客户端程序为调用操作应发送请求消息;同样定义了服务回传的响应消息中的回复行为。比如,ShoppingCartService服务中的AddItemToCart操作将生产如下的标识:
http://adventure-works.com/2010/06/04/ShoppingCartService/AddItemToCart
当WCF运行时为服务创建EndpointDispatcher对象时,它添加对应端点可以接收的行为到ContractFilter属性引用的动作表中。
如果你在定义操作时,在操作的特性中显示地为Action属性的指定了一个值,那么WCF运行时将使用你定义的值替代操作的名字。如果你指定Action属性的值为"*",WCF运行时将自动地路由发送至该操作的所有消息,无论客户端发送消息的头部中指定行为的值。在内部,服务的WCF运行时替换ContractFilter属性引用的ActionMessageFilter对象为MatchAllMessageFilter对象。MatchAllMessageFilter对象的Match方法将对传送至该方法的所有非空消息返回true,因此EndpointDispatcher将自动地识别它是否可以接收向其发送的所有的请求。在本练习中,当ShoppingCartClient程序发送AddItemToCart,RemoveItemFromCart,GetShoppingCart,和Checkout消息至ShoppingCartServiceRouter服务,该服务将接受所有这些消息并调用ProcessMessage方法。
你还应当注意ProcessMessage方法的签名。客户端的WCF运行时打包传递至操作的参数到SOAP消息的消息体中。在常规环境下,服务端的WCF时把SOAP消息的消息体转换回可以传递到实现服务操作的方法中去的一组参数。如果方法有返回值,服务端的WCF运行时打包返回值到消息中然后回传该消息到客户端的WCF运行时,WCF客户端的运行时把响应消息的消息体转化成客户端程序期望的类型。
ProcessMessage方法则有所不同,因为该方法接收一个消息对象为其输入参数。在第十一章,你已经了解到Message类提供了传输和接受原始的SOAP消息。当服务端的WCF运行时接收到来自客户端程序的一条消息,它不拆包消息获取参数而是传递整个SOAP消息至ProcessMessage方法。现在由ProcessMessage方法自己转化和翻译消息对象的内容。
相似地,ProcessMessage方法的返回值同样也是一个Message对象。ProcessMessage方法必须创建一个包含客户端期望的数据格式的完整SOAP消息并返回该消息对象。该响应消息的消息头必须包含一个ReplyAction,其对应于客户端WCF运行时期望的ReplyAction。通常,服务端的WCF运行时基于服务和操作的名字添加一个ReplyAction。比如,ShoppingCartService服务返回值客户端程序的AddItemToCart响应消息将生成如下的标识:
http://adventure-works.com/2010/06/04/ShoppingCartService/AddItemToCartResponse
如果你设置了OperationContract特性的ReplyAction属性为"*",那么服务端的WCF运行时期望你在代码中提供适当的ReplyAction,然后WCF运行时在你创建响应消息时把该ReplyAction添加到消息的头部。在这种情况下,你将从ShoppingCartService服务返回的ReplyAction不做任何更改然后回传到客户端程序。
6. 移除CompositeType类,包括IRouter.cs文件中的DataContract特性。
7. 打开Router.cs文件,添加如下的声明到文件的头部。
8. 删除Router类的注释,GetData方法和GetDataUsingDataContract方法。
9. 添加ServiceBehavior特性到Router类
Router类将提供ProcessMessage的实现。如果你熟悉SOAP协议,你将会注意到你可以在消息头包含接收服务可以识别并处理的信息。在本练习中,ShoppingCartServiceRouter服务自己不会处理消息,它只是简单的推送消息到ShoppingCartServcie服务的一个实例。因此它不需要检查或者理解消息头部,并且在不做任何改动的情况下推送它们至服务实例。设置ServiceBehavior特性类的ValidateMustUnderstand属性为false以关闭该服务对消息头信息进行任何的识别或验证。
10. 添加下面的私有变量到Router类
ShoppingCartServiceRouter服务将扮演一个客户端程序访问两个ShoppingCartServcie服务实例,向每个实例发送消息并等待响应。ProcessMessage方法的各种特性要求使用在第十一章中描述的底层技术而不是通过代理对象连接到ShoppingCartService服务。你将使用IChannelFactory对象创建爱你一个通道工厂,该工厂基于IRequestChannel以打开与ShoppingCartService服务每个实例的通道。
EndpointAddress对象指定了每个ShoppingCartService服务实例的URI。在后续的步骤中你将配置ShoppingCartServiceHost程序使用这两个地址运行两个ShoppingCartService服务实例。
ProcessMessage方法将使用routeBalancer变量确定向ShoppingCartService的哪个实例发送消息。
11. 添加如下代码到Router类的构造函数中
ProcessMessage方法将使用一个ChannelFactory对象打开与适当的ShoppingCartService服务实例的通道。ChannelFactory对象的创建和销毁非常耗费资源,而我们练习中的场景是单个服务,即所有的请求都会重用相同的ChannelFactory对象;所以在静态方法构造器中创建ChannelFactory对象保证了该对象仅仅创建一次。
此外,请注意ChannelFactory对象在创建时使用BasicHttpContextBinding对象。该绑定匹配http地址,以及两个ShoppingCartService服务实例的需求(两个服务都是持续性服务,并且通过SOAP消息的头部信息传递上下文信息)。
12. 添加ProcessMessage方法到Router类
该方法包含多个Console.WriteLine语句,这些语句可以让你在服务端的控制台窗口追踪服务的运行。
Try代码片段中If语句实现了负载均衡算法;如果routeBalencer变量的值为偶数,那么上述方法将创建一个通道并将推送请求至address1(https://localhost:9010/ShoppingCartService/ShoppingCartService.svc);否则,上述方法将为address2创建一个通道。上述方法然后增加变量routeBalancer的值(+1)。使用这种方式,ProcessMessage方法交替地发送所有的请求至ShoppingCartService服务的两个实例。
上述方法中有一个细微之处值得注意,那就是绑定所实现的上下文协议。请记住当你使用WSHttpContextBinding,BasicHttpContextBinding,或NetTcpContextBinding绑定时,消息可以包含上下文信息。持续性服务使用这些上下文信息关联客户端的请求并将它们直接传送至合适的会话中。在默认情况下,接受消息的通道在内部缓存这些上下文信息,当调用其他服务时同样遵循相同的上下文信息。然而接收到的消息同样包含相同的上下文消息,因此,当消息被推送至服务实例时,相同的上下文消息将被发送两次,当路由发送消息时将在服务内部产生错误。解决方法是在推送消息之前移除入栈消息的上下文信息,这可以通过message.Properties.Remove("ContextMessageProperty")语句来完成。
IRequestChannel类的Request方法通过通道发送Message对象至目的服务。返回值是另外一个Message对象,其包含服务的响应。然后ProcessMessage方法传递该消息至客户都程序(不对该消息做任何更改)。
请记住客户端发送消息对象可能包含安全或者其他信息。与上下文数据不一样,ProcessMessage方法并不检查或更改这些信息,因为目的服务也不关心消息是否由ShoppingCartServiceRouter服务推送。相似地,ProcessMessage方法也不会更改响应消息,路由直接传输响应消息至客户端程序。但是,这并不能阻止你在传递消息之前添加代码以修改请求消息或者响应消息的内容。这就带来了一些安全方面的考虑,因此你应该确保在部署ShoppingCartServiceRouter服务时应当将其放置在一个安全的环境中。
在生产环境中,一般地你使用IIS寄宿路由服务,因为IIS便于外界访问。为了简化,在下面的练习中,我们将使用寄宿ShoppingCartService服务的宿主程序寄宿ShoppingCartServiceRouterService服务 。你将修改宿主的配置文件以使其为ShoppingCartService服务提供两个端点,而这两个端点正式ShoppingCartServiceRouterService服务所期望的端点。
练习:配置ShoppingCartHost程序以寄宿ShoppingCartRouterServcie服务。
1. 使用WCF服务配置管理工具编辑ShoppingCartHost项目下的app.config文件
2. 在配置面板,在服务文件夹上点击右键,然后选择创建新的服务。在右边面板中,在命名文本框中,输入ShoppingCartServiceRouter.Router
3. 在配置面板,在ShoppingCartServiceRouter.Router服务的端点文件夹点击右键,然后选择创建新的服务端点。在服务端点面板,按照下表的值指定端点相应的属性
属性 | 属性值 |
地址 | http://localhost:9000/ShoppingCartService/ShoppingCartService.svc |
绑定 | basicHttpContextBinding |
合约 | ShoppingCartServiceRouter.Router |
请注意路由服务的地址与ShoppingCartService服务的地址,这样现有的客户端将直接连接到路由服务而不需要更新配置。
4. 在配置面板,选中服务文件夹,展开ShoppingCartService.ShoppingCartServiceImpl服务,展开端点文件夹,然后点击未命名端点。在服务端点面板,设置该端点的名字为ShoppingCartServiceHttpEndpoint1,然后更改地址为http://localhost:9010/ShoppingCartService/ShoppingCartService.svc
5. 在配置面板,在ShoppingCartServcie.ShoppingCartServiceImpl服务的端点上点击右键,然后选择创建新的服务端点以添加第二个服务端点。按照下表的值指定端点相应的属性
属性 | 属性值 |
名字 | ShoppingCartServiceHttpEndpoint2 |
地址 | http://localhost:9020/ShoppingCartService/ShoppingCartService.svc |
绑定 | basicHttpContextBinding |
合约 | ShoppingCartServcie.IShoppingCartService |
6. 保存配置文件,然后退出WCF服务配置管理工具。
7. 在解决方案窗口,为项目ShoppingCartHost项目添加ShoppingCartServiceRouter引用。
8. 打开ShoppingCartHost项目的Programm.cs文件,然后添加如下代码(红色方框内)
上面语句为ShoppingCartServiceRouter服务创建并打开一个新的ServiceHost对象。
9. ShoppingCartHost程序使用9010和9020端口为ShoppingCartService服务的端点。你不许保留这两个端口以使ShoppingCartHost程序可以访问这两个端口。使用管理员身份运行Visual 命令提示符窗口,然后输入下面的命令:
10. 关闭Visual Studio命令提示符窗口,然后返回到Visual Studio
练习:测试ShoppingCartRouter服务
1. 在非调适模式下运行解决方案。在ShoppingCartGUI客户端窗口的产品编码处输入PU-M044,然后点击添加按钮。这将添加一个水瓶至购物车。然后再添加另外一个产品WB-H098;然后点击结算按钮。
客户端程序应该与之前的练习一样地工作。但是,如果你检查服务控制台窗口,你将看到路由服务按照顺序推送请求消息至ShoppingCartService服务的两个实例;服务端点的地址在9020和9010之间替换。下图展示了两天请求消息经由路由服务后分别推送至两个不同的服务实例。
2. 关闭Shopping CartGUI窗口,然后在服务控制台窗口中按ENTER键盘停止服务。
参考