WCF 路由服务 - 第 IV 部分:服务版本控制和多播






4.83/5 (10投票s)
本文介绍了服务版本控制的实现以及 RoutingService 的使用。
目录
所有文章
- WCF 路由服务 - 第 IV 部分:服务版本控制和多播
- WCF 路由服务 - 第 III 部分:故障转移和负载均衡
- WCF 路由服务 - 第 II 部分:基于上下文的路由和协议桥接
- WCF 路由服务 - 第一部分:基本概念、简单路由服务与基于内容的路由
引言
在本文中,我将探讨 RoutingService
的另外两个功能:服务版本控制和多播。我将在这篇文章中讨论一个非常简单的服务版本控制场景,并看看如何使用 RoutingService
来解决它。然后,我将使用 RoutingService
实现多播。您也可以使用 WCF 4.5 附带的 udpBinding 实现多播。那么,让我们深入了解多播和服务版本控制吧。
服务版本控制
在初步开发后,服务会被部署到生产服务器上,以便最终用户可以使用。但在未来,为了满足业务或技术需求,甚至为了解决某些问题,可能需要对服务进行一些更改。每次更改都会产生一个新版本的服务,并且您可能需要同时维护旧版本和新版本,以服务现有的最终用户。
那么,您将如何处理旧版本服务的现有最终用户呢?因为您不能强迫他们立即使用新版本。嗯,您应该对此有一个具体的计划。您可以考虑未来通过治理来限制并发旧版本服务的数量。您可以考虑通过平稳地淘汰旧版本,逐步将旧版本服务的现有最终用户迁移到新版本。根据服务最新版本中更改的类别,可能会有很多策略,您需要相应地采纳。这些超出了本文的范围。您可以通过浏览本文底部的参考资料部分提供的链接来了解详细信息。
以上是对服务版本控制的简要描述。在本文中,我们将仅限于使用 RoutingService
实现服务版本控制。借助 RoutingService
,可以轻松地使用基于内容或基于上下文的路由技术来实现服务版本控制。让我们从一个场景开始。
服务版本控制场景
我将从本系列前几篇文章中我们熟悉的 ComplexNumberCalculator
服务开始。假设它有两个版本:v1.0 和 v2.0。v1.0 版本的服务公开了以下二元运算-
- Add
- Subtract
- Multiply
- Divide
我将此版本称为“常规版”。v2.0 版本的服务分别公开了二元运算和一元运算,如下所示-
- Add
- Subtract
- Multiply
- Divide
- 求模
- 参数
- 共轭
- 倒数
我将此版本的服务称为“扩展版”。以下是这两个版本服务的类图-
所以,ComplexNumberCalculator
服务有两个版本(常规版和扩展版),我们的目标是同时支持这两个版本,以便为每个版本的最终用户提供服务。我们将使用 RoutingService
来解决这个服务版本控制问题。
但在结束本节之前,我想分享一些关于这两个版本 ComplexNumberCalculator
服务的简要细节。我已将每个版本的 ComplexNumberCalculator
服务托管在两个独立的控制台应用程序中,您可以在本文附带的示例代码中看到两者的实现细节。
我已将 ComplexNumberCalculator
服务 (v1.0) 配置为以下单个端点以及一个标准的 mex 端点-
<services>
<service name="RegularComplexNoCalc.ComplexNoCalc">
<endpoint address="Regular" binding="basicHttpBinding" contract="RegularComplexNoCalc.IComplexNumber" />
<endpoint address="mex" kind="mexEndpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8081/ComplexNumberCalculator" />
</baseAddresses>
</host>
</service>
</services>
我已将 ComplexNumberCalculator
服务 (v2.0) 配置为以下单个端点以及一个标准的 mex 端点-
<services>
<service name="ExtendedComplexNoCalc.ComplexNoCalc">
<endpoint address="Extended" binding="basicHttpBinding" contract="ExtendedComplexNoCalc.IComplexNumber" />
<endpoint address="mex" kind="mexEndpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8082/ComplexNumberCalculator" />
</baseAddresses>
</host>
</service>
</services>
配置 RoutingService
上一节中描述的问题可以通过使用 RoutingService
并采用以下两种技术来解决-
- 基于上下文的路由技术
- 基于内容的路由技术
在基于上下文的路由技术中,路由器会为每个版本的服务公开一个特定的端点,传入的消息会根据其到达的特定端点被唯一地路由到特定版本的服务。
因此,在我们的案例中,我们需要在路由器中为每个服务版本公开两个端点。但这种技术并不可行,因为我们将需要为每个更新的服务版本在路由器中公开更多的端点。
在基于内容的路由技术中,路由器会为所有版本的服务公开一个特定的端点,传入的消息会根据消息的内容被唯一地路由到特定版本的服务。
因此,在我们的案例中,我们需要在路由器中为两个版本的服务公开一个单一的端点。这种技术会是首选,因为它基于检查传入消息的内容来区分对不同服务版本的请求。
但是,在我们的案例中,如何通过检查传入消息的内容来区分对两个服务版本的请求呢?因为二元运算在两个版本的服务中是共有的,所以在这种情况下,对两个服务版本的请求在所有方面(它们的输入参数和返回类型相同)都是相同的。传入消息的内容不足以唯一地确定正确的目标服务版本。尽管在一元运算的请求中,我们可以轻松地确定正确的目标服务版本。但总的来说,我们不能使用 Action
或 XPath
过滤器类型来确定正确的目标服务版本,因为对二元运算的请求是相同的。您可以通过查看下面显示的每个服务版本的 Action
值来验证这一点-。
<!-- Action values of first version of the service-->
http://tempuri.org/IComplexNumber/Add
http://tempuri.org/IComplexNumber/Subtract
http://tempuri.org/IComplexNumber/Multiply
http://tempuri.org/IComplexNumber/Divide
<!-- Action values of second version of the service-->
http://tempuri.org/IComplexNumber/Add
http://tempuri.org/IComplexNumber/Subtract
http://tempuri.org/IComplexNumber/Multiply
http://tempuri.org/IComplexNumber/Divide
http://tempuri.org/IComplexNumber/Modulus
http://tempuri.org/IComplexNumber/Argument
http://tempuri.org/IComplexNumber/Conjugate
http://tempuri.org/IComplexNumber/Reciprocal
那么解决方案是什么呢?嗯,这个问题可以通过在请求消息头中插入服务版本信息来解决。每个客户端应用程序将在消息头中插入目标服务版本信息,路由器将使用消息头中包含的此版本特定信息将传入消息路由到服务的适当版本。
现在,在我们的案例中,我们将在请求消息头中插入一个自定义元素“Version”,以指示请求消息将被传输到的目标服务版本。自定义元素 Version 可以有两个可能的值- v1.0 和 v2.0。v1.0 的值表示请求消息必须路由到 ComplexNumberCalculator
服务的第一个版本进行处理,而 v2.0 的值表示请求消息将路由到 ComplexNumberCalculator
服务的第二个版本进行处理。
所以在我们的案例中,SOAP 消息将如下所示-
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Version xmlns="http://custom/namespace">v1.0</Version>
<To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">https://:8081/ComplexNumberCalculator/Regular</To>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IComplexNumber/Add</Action>
</s:Header>
<s:Body>
...
</s:Body>
</s:Envelope>
在上面,'http://custom/namespace' 是我为插入请求消息头的自定义元素定义的命名空间。
我将在下一节解释我们如何将自定义元素插入消息头。但在此之前,让我们为我们的服务版本控制场景配置 RoutingService
。
首先,我用以下虚拟端点配置了 RoutingService
-
<services>
<service name="System.ServiceModel.Routing.RoutingService">
<endpoint address="" binding="basicHttpBinding" contract="System.ServiceModel.Routing.IRequestReplyRouter" name="VirtualEndpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8080/RoutingService/Router" />
</baseAddresses>
</host>
</service>
</services>
接下来,我为每个服务版本定义了两个目标端点-
<client>
<endpoint address="https://:8081/ComplexNumberCalculator/Regular" binding="basicHttpBinding"
contract="*" name="firstVersion" />
<endpoint address="https://:8082/ComplexNumberCalculator/Extended" binding="basicHttpBinding"
contract="*" name="secondVersion" />
</client>
接下来,我启用了 RoutingBehavior
,然后指定了筛选器表的名称。我是通过定义如下默认行为来完成的-
<behaviors>
<serviceBehaviors>
<behavior name="">
<routing filterTableName="RoutingTable" />
</behavior>
</serviceBehaviors>
</behaviors>
接下来,我使用
<namespaceTable>
<add prefix="s" namespace="http://schemas.xmlsoap.org/soap/envelope/"/>
<add prefix="cn" namespace="http://custom/namespace"/>
</namespaceTable>
接下来,我借助自定义元素 'Version',使用 XPath
filterType
为 ComplexNumberCalculator
服务的每个版本定义了筛选器,如下所示-
<filters>
<filter name="firstVersionFilter" filterType="XPath" filterData="/s:Envelope/s:Header/cn:Version ='v1.0'"/>
<filter name="secondVersionFilter" filterType="XPath" filterData="/s:Envelope/s:Header/cn:Version ='v2.0'"/>
<filter name="noHeaderFilter" filterType="XPath" filterData="count(/s:Envelope/s:Header/cn:Version) = 0" />
</filters>
请注意,我上面定义了一个额外的过滤器,用于处理头部没有‘Version’元素的传入消息。
最后,我将每个筛选器映射到筛选器表 'RoutigTable
' 中各自的目标服务端点,如下所示-
<filterTables>
<filterTable name="RoutingTable">
<add filterName="firstVersionFilter" endpointName="firstVersion"/>
<add filterName="secondVersionFilter" endpointName="secondVersion"/>
<add filterName="noHeaderFilter" endpointName="secondVersion"/>
</filterTable>
</filterTables>
请注意,上面我已将 noHeaderFilter
过滤器映射到服务的最新版本。但根据需求也可以考虑其他策略。
就是这样。让我们进入下一节。
在传出消息头中插入自定义元素
为了模拟我们的服务版本控制演示,我创建了两个控制台应用程序来模拟客户端(最终用户),每个版本对应一个 ComplexNumberCalculator
服务。我为第一个客户端应用程序配置了以下端点-
<system.serviceModel>
<client>
<endpoint address="https://:8080/RoutingService/Router"
binding="basicHttpBinding" contract="RegularComplexNoCalc.IComplexNumber" name="firstVersionEndUsers" />
</client>
</system.serviceModel>
然后我为第二个客户端应用程序配置了以下端点-
<system.serviceModel>
<client>
<endpoint address="https://:8080/RoutingService/Router"
binding="basicHttpBinding" contract="ExtendedComplexNoCalc.IComplexNumber" name="secondVersionEndUsers" />
</client>
</system.serviceModel>
就这样,没什么特别的。
让我们检查一下客户端应用程序的代码,看看如何将自定义元素插入到传出消息的 SOAP 头部,以指示目标服务版本信息。以下是第一个客户端应用程序(服务第一个版本的最终用户)的代码-
var cfV1 = new ChannelFactory<IComplexNumber>("firstVersionEndUsers");
var channelV1 = cfV1.CreateChannel();
var z1 = new ComplexNumber();
var z2 = new ComplexNumber();
z1.Real = 3D;
z1.Imaginary = 4D;
z2.Real = 4D;
z2.Imaginary = 3D;
Console.WriteLine("*** Service Versioning: : end-users of the first version ***\n");
Console.WriteLine("\nPlease hit any key to run OR enter 'exit' to terminate.");
string command = Console.ReadLine();
while (command != "exit")
{
Console.WriteLine("Please hit any key to simulate firstVersionEndUsers: ");
Console.ReadLine();
using (new OperationContextScope((IContextChannel)channelV1))
{
OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader("Version", "http://custom/namespace", "v1.0"));
ComplexNumberArithmetics(channelV1, z1, z2);
}
Console.WriteLine("\nPlease hit any key to re-run OR enter 'exit' to terminate.");
command = Console.ReadLine();
}
((IClientChannel)channelV1).Close();
在上面的代码中,我首先在运行时使用 ChannelFactory
类创建了一个客户端代理,以便调用服务第一个版本的成员。现在,在执行此操作之前,我们需要将一个用于保存版本信息的自定义元素插入到传出消息的 SOAP 头部。
因此,接下来我创建了一个 OperationContextScope
类的实例,以根据客户端代理的 innerChannel
获取当前操作上下文作用域。请注意,这里的 (IContextChannel)channelV1
代表客户端代理的 innerChannel
。非常重要的一点是,如果您在编译时使用 svcutill 生成的代理类(在这种情况下是 ComplexNumberClient
类,ComplexNumberClient proxy = new ComplexNumberClient("firstVersionEndUsers")
)创建了客户端代理,那么您需要调用 proxy.InnerChannel
来获取客户端代理的 innerChannel
。另一点重要提示:必须先创建 OperationContextScope
类的实例,然后才能从上下文 (OperationContext.Current.OutgoingMessageHeaders
) 访问传出消息头元素。如果您不这样做,OperationContext
将为 null,您将遇到一个异常 - System.NullreferenceException: {"Object reference not set to an instance of an object."}
接下来,我通过调用 MessageHeader.CreateHeader
方法创建了一个新的自定义元素,并通过调用 OperationContext.Current.OutgoingMessageHeaders.Add
方法将其添加到传出消息头中。这里的 'Version'、'http://custom/namespace' 和 'v1.0' 分别代表自定义元素的名称、命名空间和值。请注意,这里我硬编码了目标服务的版本。您可以根据需要将其保存在客户端应用程序的配置文件、数据库甚至简单的文本文件中。
最后,我调用了 ComplexNumberArithmetics
方法来访问服务第一个版本的成员。您可以在本文附带的示例代码中找到 ComplexNumberArithmetics
方法的代码。
第二个客户端应用程序的代码类似,只是'Version'自定义元素的值(v2.0)不同。请参阅本文附带的示例。
使用 RoutingService 模拟服务版本控制
现在一切都完成了。让我们运行演示应用程序。以下是解决方案的屏幕截图-
只需将 ClientOfV1、ClientOfV2、HostExtendedCalc、HostRegularCalc 和 Router 项目设置为启动项目,然后按 Ctrl+F5 键运行这些项目。现在最小化 RoutingService
控制台窗口。
接下来,在第一个客户端应用程序窗口(服务第一个版本的最终用户)上按任意键;您可以看到,中介 RoutingService
在检查了传入消息头中包含的版本信息后,会将传入消息路由到 ComplexNumberCalculator
服务的第一个版本(常规版)。
接下来,在第二个客户端应用程序窗口(服务第二个版本的最终用户)上按任意键;您可以验证,中介 RoutingService
在检查了传入消息头中包含的版本信息后,会将传入消息路由到 ComplexNumberCalculator
服务的第二个版本(扩展版)。
现在,关闭所有正在运行的应用程序(客户端、服务和路由器),并从每个客户端应用程序(每个服务版本的最终用户)中注释掉以下代码行,然后重新生成解决方案,以模拟传入消息头中没有“版本信息”的情况。
...
using (new OperationContextScope((IContextChannel)channelV1))
{
//OperationContext.Current.OutgoingMessageHeaders.Add(MessageHeader.CreateHeader("Version", "http://custom/namespace", "v1.0"));
ComplexNumberArithmetics(channelV1, z1, z2);
}
...
请注意,我已经在“配置 RoutingService”部分定义了一个过滤器来处理这种情况。最后,将 ClientOfV1、ClientOfV2、HostExtendedCalc、HostRegularCalc 和 Router 项目设置为启动项目,然后按 Ctrl+F5 键运行项目并最小化 RoutingService
控制台窗口。
接下来,只需在第一个客户端应用程序窗口(服务第一个版本的最终用户)上按任意键,然后在第二个客户端应用程序(服务第二个版本的最终用户)上按任意键;您可以验证,在这两种情况下,传入的消息都由中介 RoutingService
路由到 ComplexNumberCalculator
服务的第二个版本(扩展版)。这符合我们的预期,因为我们已经在筛选器表中预设了这种情况。
所以您已经看到了如何使用 RoutingService
同时提供服务的不同版本。在这里,我们通过将服务版本信息插入到传出消息头中实现了服务版本控制,但还有其他服务版本控制技术,它们不需要客户端应用程序通过传出消息头传递额外信息。消息可以被路由到服务的最新或最兼容的版本。
多播
什么是多播?多播是用于描述通信的术语,其中一条信息从一个或多个点发送到一组其他点。在多播中,发送者 >= 1(即至少应有一个发送者),接收者 >= 0(即可能没有接收者)。
在多播中,客户端只有在先前订阅了特定的多播组后才会收到数据包流,并且组成员关系是动态的,由接收方控制。如果一组最终用户同时需要一组通用数据,多播非常有用,例如在证券交易所、多媒体内容分发网络、IPTV 应用(远程学习和电视公司会议)、无线网络和有线电视等。借助多播,您只需使用一个数据流就可以触达许多最终用户,从而减少带宽占用并节省资金。
在对多播进行简要描述之后,让我们看看如何使用 WCF RoutingService
实现多播。
多播演示服务
在本文中,我将使用一个简单的股票服务来演示使用 RoutingService
的多播。股票服务将在每个预定义的时间间隔后,同时向所有已订阅的接收方发送股票的简要信息(名称、类型及其瞬时价格)。
以下是该服务的类图-
我使用单向消息创建了一个 StockService
。以下是 DataContract
和 ServiceContract
的定义,以及 StockService
的实现细节-
[DataContract]
public class Stock
{
[DataMember]
public string Name;
[DataMember]
public string StockType;
[DataMember]
public double Price;
}
[ServiceContract]
public interface IStockService
{
[OperationContract(IsOneWay = true)]
void SendStockDetail(Stock stock);
}
public class StockService : IStockService
{
public void SendStockDetail(Stock stock)
{
Console.WriteLine(string.Format("Stock Name: {0}, Stock Type: {1}, Price: ${2:000.00}", stock.Name, stock.StockType, stock.Price));
}
}
最后,我将 StockService
托管在一个控制台应用程序中,并配置了一个单独的端点以及一个标准的 mex 端点,如下所示-
<services>
<service name="StockInformation.StockService">
<endpoint address="" binding="basicHttpBinding" contract="StockInformation.IStockService" />
<endpoint address="mex" kind="mexEndpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8081/StockService" />
</baseAddresses>
</host>
</service>
</services>
请注意,此服务的每个实例在我们的多播演示中都将充当接收者。让我们转到下一节,为多播配置 RoutingService
。
为多播配置 RoutingService
我将重新配置本文前面在“服务版本控制”演示中使用的 RoutingService
,以用于多播演示。因此,首先我用以下虚拟端点重新配置了 RoutingService
-
<services>
<service name="System.ServiceModel.Routing.RoutingService">
<endpoint address="" binding="basicHttpBinding"
contract="System.ServiceModel.Routing.ISimplexDatagramRouter" name="virtualendpoint" />
<host>
<baseAddresses>
<add baseAddress="https://:8080/RoutingService/Router" />
</baseAddresses>
</host>
</service>
</services>
请注意,我在这里使用了 ISimplexDatagramRouter
ServiceContract
,因为 StockService
被设计为只处理单向消息。接下来,我为每个服务实例重新定义了以下三个目标端点。这些实例将在我们的多播演示中充当接收方。实际上,我在这里只是订阅了接收方,以便从发送方接收多播信息。
<client>
<endpoint address="https://:8081/StockService1" binding="basicHttpBinding"
contract="*" name="stockservice1" />
<endpoint address="https://:8081/StockService2" binding="basicHttpBinding"
contract="*" name="stockservice2" />
<endpoint address="https://:8081/StockService3" binding="basicHttpBinding"
contract="*" name="stockservice3" />
</client>
接下来,我使用 MatchAll
过滤器类型重新定义了一个单一的过滤器,它将匹配所有传入的消息。
<filters>
<filter name="wildCardFilter" filterType="MatchAll" />
</filters>
由于我们的目标只是将所有来自发送方的传入消息多播到已订阅的接收方,因此接下来我通过将上面定义的 wildCardFilter
过滤器映射到所有目标端点(接收方)来重新配置过滤器表 'RoutingTable
',如下所示。
<filterTables>
<filterTable name="RoutingTable">
<add filterName="wildCardFilter" endpointName="stockservice1" />
<add filterName="wildCardFilter" endpointName="stockservice2" />
<add filterName="wildCardFilter" endpointName="stockservice3" />
</filterTable>
</filterTables>
这样就完成了用于多播演示的 RoutingService
配置。让我们进入下一节,以模拟多播的发送方。
模拟多播发送方
下一步是为我们的多播演示模拟发送方应用程序,以便在每个预定义的时间间隔后向已订阅的接收方发送股票信息。为此,我创建了一个 StockService
的控制台客户端应用程序,并为其配置了以下端点-
<client>
<endpoint address="https://:8080/RoutingService/Router" binding="basicHttpBinding"
contract="Sender.IStockService" name="stockDetails" />
</client>
最后,我们来看一下客户端应用程序的代码。
var cf = new ChannelFactory<IStockService>("stockDetails");
var channel = cf.CreateChannel();
Console.WriteLine("*** Multicasting using RoutingService Demo ***\n");
Console.WriteLine("Please hit any key to start: ");
string command = Console.ReadLine();
while (command != "exit")
{
var stock = GetStockDetails();
channel.SendStockDetail(stock);
Console.WriteLine("Details of the Stock: {0} has been sent; Sender: {1}", stock.Name);
System.Threading.Thread.Sleep(3000);
}
((IClientChannel)channel).Close();
在上面的代码中,我首先在运行时使用 ChannelFactory
类创建了一个客户端代理,以便调用 StockService
的唯一成员“SendStockDetail
”。接下来,我使用 GetStockDetails
方法生成了股票信息,并通过调用 SendStockDetail
方法将其多播出去。请注意,我使用 GetStockDetails
方法模拟了每 3 秒生成一次股票信息的过程。您可以在本文附带的示例代码中找到该方法的代码。
使用 RoutingService 模拟多播
在运行我们的演示之前,请先看一下本文提供的多播解决方案的屏幕截图-
现在只需将 Client 和 Router 项目设置为启动项目,然后按 Ctrl+F5 键运行项目。接下来,最小化 RoutingService 控制台窗口,并从Visual Studio 开发者命令提示符(以管理员模式)运行 WCFRoutingPart4\Multicasting\StockServices\StartAllServices.cmd 文件,以启动 StockService1
、StockService2
和 StockService3
服务。
接下来,在控制台客户端窗口(发送方)上按任意键;您可以验证所有传入的消息都正在由中介 RoutingService
多播给所有已订阅的接收方。
接下来,只需停止任何一个接收器;您可以验证现在传入的消息正由中介 RoutingService
多播给可用的(活动的)接收器。
接下来,停止所有活动的接收器;您可以验证传入的消息仍然由中介 RoutingService
进行多播,尽管没有接收器。这符合多播的规则,即可能没有接收器。
最后再做一个实验。只需启动客户端应用程序(发送方)的多个实例以及 StockService
(接收方)的多个实例;您可以验证在这种情况下,传入的消息正由中介 RoutingService
从多个发送方多播给所有已订阅的接收方。这再次符合多播的规则,即可以有多个发送方。
结论
实际上,在本文中,我将服务契约版本控制视为一个服务版本控制场景。但是,可能还有其他服务版本控制的场景,例如:数据契约版本控制、消息契约版本控制、地址和绑定版本控制等。在实施之前,您需要概述并处理每种服务版本控制场景的情况。
就多播而言,除了将传入消息多播给已订阅的接收者之外,您还可以将接收者分组,并根据某些定义的标准将传入消息多播到特定组。例如,假设 StockService
有两组已订阅的接收者:G1 和 G2,那么我们可以使用 RoutingService
将价格 > 500 美元的传入消息多播到 G1 组,将价格 <= 500 美元的传入消息多播到 G2 组。
所以您已经看到了如何使用 RoutingService
轻松实现服务版本控制和多播功能。希望您喜欢这篇文章。
参考资料
- 服务版本控制
- 多播 (Multicasting)
历史
- 2014年6月7日 -- 发布原始版本